11 Commits
Author SHA1 Message Date
Michael e759a1a7d5 prepare v1.1.0 release 2026-05-31 10:53:40 +02:00
LogiFlowandClaude 3b5ede5872 Robustesse release : accepte les tags V*/v* et normalise la version (#3)
* Passe la version à 1.1.0 et publie automatiquement les releases

- Monte la version à 1.1.0 (tauri.conf.json, Cargo.toml, package.json)
  pour générer un nouvel installeur via la CI.
- Met releaseDraft à false dans le workflow : les releases étaient
  créées en brouillon, donc invisibles et jamais marquées 'Latest' sur
  la page GitHub. Elles sont désormais publiées et présentées comme la
  dernière version.

* Durcit le déclenchement de la release (tags V*/v*) et normalise la version

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-31 10:41:18 +02:00
LogiFlowandClaude a635aa29a4 Correction de la restauration du widget + release automatique v1.1.0 (#2)
- Monte la version à 1.1.0 (tauri.conf.json, Cargo.toml, package.json)
  pour générer un nouvel installeur via la CI.
- Met releaseDraft à false dans le workflow : les releases étaient
  créées en brouillon, donc invisibles et jamais marquées 'Latest' sur
  la page GitHub. Elles sont désormais publiées et présentées comme la
  dernière version.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-31 10:28:14 +02:00
LogiFlow fcac504693 Merge pull request #1 from R0m1k3/claude/amazing-ramanujan-56hE0
Corrige la restauration du widget masqué sans icône de barre des tâches
2026-05-31 04:16:22 +02:00
Claude 27d3fb7d50 Corrige la restauration du widget masqué sans icône de barre des tâches
Le bouton Réduire appelait minimize() alors que la fenêtre utilise
skipTaskbar:true : sans entrée dans la barre des tâches, la fenêtre
minimisée devenait irrécupérable. Le bouton masque désormais vers le
system tray.

La logique de bascule du tray gère maintenant correctement l'état
minimisé (is_minimized) et restaure la fenêtre via unminimize/show/
set_focus, faisant de l'icône de la barre d'état un point de
restauration fiable dans tous les cas.
2026-05-31 01:27:33 +00:00
Michael d9c1720523 preserve API key across container updates 2026-05-28 08:10:41 +02:00
Michael e4a56f2fc6 fix: pause widget polling while editing a task to prevent focus loss 2026-05-27 13:23:16 +02:00
Michael 3e6dbb5c76 feat: serve beautiful interactive glassmorphic documentation at /doc and redirect root to doc 2026-05-27 10:07:50 +02:00
Michael 1ca25917d4 docs: update AI agent integration guide with tool calling schemas and missing route 2026-05-27 10:05:23 +02:00
Michael 278f74326c Enable native window resizing and make the glassmorphic widget fully responsive with minimum bounds 2026-05-26 22:00:43 +02:00
Michael 8cebc3c2fa Redesign UI to be a stunning visual glassmorphic masterpiece and automate version synchronization in compiled exe metadata 2026-05-26 21:50:10 +02:00
12 changed files with 1010 additions and 323 deletions

No files matched your search

+40 -2
View File
@@ -3,7 +3,9 @@ name: Release Tauri App
on:
push:
tags:
# Accept lowercase and uppercase tags, for example v1.1.0.0 or V1.1.0.0.
- 'v*'
- 'V*'
workflow_dispatch:
jobs:
@@ -38,6 +40,39 @@ jobs:
cd widget
npm install
- name: Update app version from tag
shell: bash
run: |
# Keep Tauri/Cargo on valid SemVer while allowing release tags like V1.1.0.0.
RAW_VERSION=$(echo "${{ github.ref_name }}" | sed 's/^[vV]//')
APP_VERSION="1.1.0"
if [[ "$RAW_VERSION" =~ ^([0-9]+\.[0-9]+\.[0-9]+)(\.[0-9]+)?$ ]]; then
APP_VERSION="${BASH_REMATCH[1]}"
fi
echo "Updating app version to $APP_VERSION"
node -e "
const fs = require('fs');
const updateJson = (file, update) => {
const data = JSON.parse(fs.readFileSync(file, 'utf8'));
update(data);
fs.writeFileSync(file, JSON.stringify(data, null, 2) + '\n', 'utf8');
};
updateJson('./widget/package.json', (pkg) => {
pkg.version = '$APP_VERSION';
});
updateJson('./widget/src-tauri/tauri.conf.json', (config) => {
config.package.version = '$APP_VERSION';
});
const cargoFile = './widget/src-tauri/Cargo.toml';
const cargo = fs.readFileSync(cargoFile, 'utf8')
.replace(/^version = \".*\"$/m, 'version = \"$APP_VERSION\"');
fs.writeFileSync(cargoFile, cargo, 'utf8');
"
- name: Build the Tauri app
uses: tauri-apps/tauri-action@v0
env:
@@ -45,7 +80,10 @@ jobs:
with:
tagName: ${{ github.ref_name }}
releaseName: 'TodoFlow ${{ github.ref_name }}'
releaseBody: 'Release generated automatically by GitHub Actions.'
releaseDraft: true
releaseBody: |
Installeur Windows de TodoFlow ${{ github.ref_name }}.
Telechargez le fichier `.exe` (NSIS) ou `.msi` ci-dessous pour installer l'application.
releaseDraft: false
prerelease: false
projectPath: './widget'
+2 -1
View File
@@ -30,11 +30,12 @@ ENV PYTHONUNBUFFERED=1
RUN mkdir -p /app/data
# Création d'un utilisateur système non-privilégié pour des raisons de sécurité
RUN useradd -u 8888 appuser && chown -R appuser:appuser /app
RUN chmod +x /app/entrypoint.sh && useradd -u 8888 appuser && chown -R appuser:appuser /app
USER appuser
EXPOSE 8000
# Lancement d'Uvicorn
ENTRYPOINT ["/app/entrypoint.sh"]
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
+253 -2
View File
@@ -62,7 +62,23 @@ Permet à l'Agent IA d'ajouter un nouvel objectif pour l'utilisateur.
```
* **Réponse (201 Created) :** Renvoie l'objet créé avec son UUID unique généré par le serveur et son horodatage de création (`created_at`).
### C. Mettre à jour une tâche (PUT `/api/v1/tasks/{id}`)
### C. Récupérer les détails d'une tâche (GET `/api/v1/tasks/{id}`)
Permet à l'Agent IA d'obtenir les informations complètes d'une tâche spécifique à partir de son UUID.
* **Réponse (200 OK) :**
```json
{
"id": "e1a77ff0-2c32-4f37-baef-e68b903f806e",
"title": "Acheter du pain",
"description": "Prendre une tradition bien cuite",
"status": "A faire",
"created_at": "2026-05-26T14:15:30Z",
"updated_at": "2026-05-26T14:15:30Z"
}
```
* **Réponse (404 Not Found) :** Si aucune tâche ne correspond à l'identifiant fourni.
### D. Mettre à jour une tâche (PUT `/api/v1/tasks/{id}`)
Permet à l'Agent IA de marquer une tâche comme **"En cours"** ou **"Termine"**, ou d'en modifier le titre/description.
* **Request Body (JSON - Mise à jour partielle acceptée) :**
@@ -73,7 +89,7 @@ Permet à l'Agent IA de marquer une tâche comme **"En cours"** ou **"Termine"**
```
* **Réponse (200 OK) :** Renvoie l'objet mis à jour avec son nouvel horodatage `updated_at`.
### D. Supprimer une tâche (DELETE `/api/v1/tasks/{id}`)
### E. Supprimer une tâche (DELETE `/api/v1/tasks/{id}`)
Permet à l'Agent IA de nettoyer la liste en supprimant une tâche.
* **Réponse (204 No Content) :** Succès de la suppression (aucun corps de réponse).
@@ -125,6 +141,241 @@ if create_resp.status_code == 201:
---
## 🛠️ 5. Définition des Outils pour Agents IA (Function Calling)
Si vous configurez un agent IA externe utilisant les API d'OpenAI, Anthropic ou Gemini, vous pouvez copier-coller les schémas de fonctions ci-dessous pour lui donner un accès direct et natif à la Todo List.
### A. Format OpenAI / Gemini (JSON Schema)
```json
[
{
"type": "function",
"function": {
"name": "get_tasks",
"description": "Récupère la liste de toutes les tâches stockées dans TodoFlow. Permet de filtrer optionnellement par statut.",
"parameters": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": ["A faire", "En cours", "Termine"],
"description": "Filtre optionnel pour récupérer uniquement les tâches ayant ce statut ('A faire', 'En cours', 'Termine')."
}
}
}
}
},
{
"type": "function",
"function": {
"name": "get_task",
"description": "Récupère les détails précis d'une tâche existante à partir de son UUID.",
"parameters": {
"type": "object",
"properties": {
"task_id": {
"type": "string",
"format": "uuid",
"description": "L'UUID unique de la tâche à récupérer."
}
},
"required": ["task_id"]
}
}
},
{
"type": "function",
"function": {
"name": "create_task",
"description": "Crée une nouvelle tâche dans la Todo list.",
"parameters": {
"type": "object",
"properties": {
"title": {
"type": "string",
"minLength": 1,
"maxLength": 100,
"description": "Le titre court et descriptif de la tâche (ex: 'Acheter du pain')."
},
"description": {
"type": "string",
"description": "Description détaillée de la tâche ou consignes supplémentaires."
},
"status": {
"type": "string",
"enum": ["A faire", "En cours", "Termine"],
"default": "A faire",
"description": "Le statut initial de la tâche (par défaut 'A faire')."
}
},
"required": ["title"]
}
}
},
{
"type": "function",
"function": {
"name": "update_task",
"description": "Met à jour une tâche existante (modifier le titre, la description, ou changer de statut comme passer à 'En cours' ou 'Termine').",
"parameters": {
"type": "object",
"properties": {
"task_id": {
"type": "string",
"format": "uuid",
"description": "L'UUID unique de la tâche à modifier."
},
"title": {
"type": "string",
"minLength": 1,
"maxLength": 100,
"description": "Nouveau titre pour la tâche."
},
"description": {
"type": "string",
"description": "Nouvelle description pour la tâche."
},
"status": {
"type": "string",
"enum": ["A faire", "En cours", "Termine"],
"description": "Nouveau statut pour la tâche."
}
},
"required": ["task_id"]
}
}
},
{
"type": "function",
"function": {
"name": "delete_task",
"description": "Supprime définitivement une tâche de la liste à partir de son UUID.",
"parameters": {
"type": "object",
"properties": {
"task_id": {
"type": "string",
"format": "uuid",
"description": "L'UUID unique de la tâche à supprimer."
}
},
"required": ["task_id"]
}
}
}
]
```
### B. Format Anthropic / Claude (Messages API Tools)
```json
[
{
"name": "get_tasks",
"description": "Récupère la liste de toutes les tâches stockées dans TodoFlow. Permet de filtrer optionnellement par statut.",
"input_schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": ["A faire", "En cours", "Termine"],
"description": "Filtre optionnel pour récupérer uniquement les tâches ayant ce statut."
}
}
}
},
{
"name": "get_task",
"description": "Récupère les détails précis d'une tâche existante à partir de son UUID.",
"input_schema": {
"type": "object",
"properties": {
"task_id": {
"type": "string",
"format": "uuid",
"description": "L'UUID unique de la tâche à récupérer."
}
},
"required": ["task_id"]
}
},
{
"name": "create_task",
"description": "Crée une nouvelle tâche dans la Todo list.",
"input_schema": {
"type": "object",
"properties": {
"title": {
"type": "string",
"minLength": 1,
"maxLength": 100,
"description": "Le titre court et descriptif de la tâche."
},
"description": {
"type": "string",
"description": "Description détaillée de la tâche."
},
"status": {
"type": "string",
"enum": ["A faire", "En cours", "Termine"],
"default": "A faire",
"description": "Le statut initial de la tâche."
}
},
"required": ["title"]
}
},
{
"name": "update_task",
"description": "Met à jour une tâche existante par son UUID (modifier le titre, la description, ou changer de statut).",
"input_schema": {
"type": "object",
"properties": {
"task_id": {
"type": "string",
"format": "uuid",
"description": "L'UUID unique de la tâche à modifier."
},
"title": {
"type": "string",
"minLength": 1,
"maxLength": 100,
"description": "Nouveau titre pour la tâche."
},
"description": {
"type": "string",
"description": "Nouvelle description."
},
"status": {
"type": "string",
"enum": ["A faire", "En cours", "Termine"],
"description": "Nouveau statut."
}
},
"required": ["task_id"]
}
},
{
"name": "delete_task",
"description": "Supprime définitivement une tâche de la liste à partir de son UUID.",
"input_schema": {
"type": "object",
"properties": {
"task_id": {
"type": "string",
"format": "uuid",
"description": "L'UUID unique de la tâche à supprimer."
}
},
"required": ["task_id"]
}
}
]
```
---
## 💡 Conseils pour le Prompt System de l'Agent IA
Si vous intégrez cette API à un LLM (comme GPT-4 ou Gemini), vous pouvez ajouter cette consigne dans son **System Prompt** :
+259 -1
View File
@@ -1,10 +1,13 @@
from contextlib import asynccontextmanager
from datetime import datetime
import json
import os
from uuid import UUID
from typing import List, Optional
from fastapi import FastAPI, HTTPException, Depends, Security, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.security.api_key import APIKeyHeader
from fastapi.responses import HTMLResponse, RedirectResponse
from sqlmodel import Session, select
from app.config import settings
@@ -40,7 +43,7 @@ async def lifespan(app: FastAPI):
app = FastAPI(
title="TodoFlow API",
description="API centralisée pour la Todo list, connectée au widget de bureau et pilotable par Agent IA.",
version="1.0.0",
version="1.1.0",
lifespan=lifespan
)
@@ -53,6 +56,261 @@ app.add_middleware(
allow_headers=["*"],
)
# --- DOCUMENTATION ET REDIRECTIONS ---
@app.get("/", include_in_schema=False)
def redirect_root_to_doc():
"""Redirige les requêtes de la racine vers la page de documentation."""
return RedirectResponse(url="/doc")
@app.get("/doc", response_class=HTMLResponse, include_in_schema=False)
def get_html_documentation():
"""
Sert une page HTML interactive et magnifiquement stylisée (Glassmorphism)
rendant le guide d'intégration pour Agent IA (README_AI_AGENT.md).
"""
try:
# Chemin relatif vers la racine du serveur (où se trouve le README)
readme_path = "README_AI_AGENT.md"
if not os.path.exists(readme_path):
# Essayer un autre chemin au cas où
readme_path = "/app/README_AI_AGENT.md"
if not os.path.exists(readme_path):
raise FileNotFoundError("README_AI_AGENT.md introuvable.")
with open(readme_path, "r", encoding="utf-8") as f:
markdown_content = f.read()
escaped_content = json.dumps(markdown_content)
html_template = f"""<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documentation TodoFlow API</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Fira+Code:wght@400;500&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
:root {{
--bg-gradient: linear-gradient(135deg, #0f172a 0%, #1e1b4b 100%);
--glass-bg: rgba(30, 41, 59, 0.75);
--glass-border: rgba(255, 255, 255, 0.08);
--text-main: #f8fafc;
--text-muted: #94a3b8;
--primary: #6366f1;
--primary-glow: rgba(99, 102, 241, 0.15);
--accent: #10b981;
}}
body {{
font-family: 'Inter', sans-serif;
background: var(--bg-gradient);
color: var(--text-main);
margin: 0;
padding: 0;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: flex-start;
}}
.container {{
width: 100%;
max-width: 900px;
margin: 40px 20px;
background: var(--glass-bg);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid var(--glass-border);
border-radius: 24px;
padding: 40px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.05);
}}
/* Sleek scrollbar */
::-webkit-scrollbar {{
width: 8px;
}}
::-webkit-scrollbar-track {{
background: rgba(15, 23, 42, 0.5);
}}
::-webkit-scrollbar-thumb {{
background: rgba(99, 102, 241, 0.3);
border-radius: 4px;
}}
::-webkit-scrollbar-thumb:hover {{
background: rgba(99, 102, 241, 0.5);
}}
/* Markdown styling */
.markdown-body h1 {{
font-size: 2.2rem;
font-weight: 700;
margin-top: 0;
margin-bottom: 24px;
background: linear-gradient(135deg, #fff 0%, #cbd5e1 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
padding-bottom: 16px;
}}
.markdown-body h2 {{
font-size: 1.6rem;
font-weight: 600;
margin-top: 40px;
margin-bottom: 16px;
color: #e2e8f0;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
padding-bottom: 8px;
}}
.markdown-body h3 {{
font-size: 1.2rem;
font-weight: 600;
margin-top: 24px;
margin-bottom: 12px;
color: #f1f5f9;
}}
.markdown-body p {{
line-height: 1.7;
color: var(--text-muted);
margin-bottom: 16px;
}}
.markdown-body code {{
font-family: 'Fira Code', monospace;
font-size: 0.9em;
background: rgba(15, 23, 42, 0.6);
padding: 3px 6px;
border-radius: 6px;
color: #f43f5e;
border: 1px solid rgba(255, 255, 255, 0.05);
}}
.markdown-body pre {{
background: rgba(15, 23, 42, 0.8);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 12px;
padding: 16px;
overflow-x: auto;
margin-bottom: 24px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}}
.markdown-body pre code {{
background: none;
padding: 0;
color: #38bdf8;
border: none;
font-size: 0.92em;
}}
.markdown-body ul, .markdown-body ol {{
margin-bottom: 20px;
padding-left: 24px;
color: var(--text-muted);
line-height: 1.7;
}}
.markdown-body li {{
margin-bottom: 8px;
}}
.markdown-body a {{
color: var(--primary);
text-decoration: none;
transition: color 0.2s;
}}
.markdown-body a:hover {{
color: #818cf8;
text-decoration: underline;
}}
.markdown-body blockquote {{
border-left: 4px solid var(--primary);
background: var(--primary-glow);
margin: 20px 0;
padding: 16px 20px;
border-radius: 0 12px 12px 0;
}}
.markdown-body blockquote p {{
margin: 0;
color: #cbd5e1;
font-style: italic;
}}
.markdown-body hr {{
border: 0;
height: 1px;
background: rgba(255, 255, 255, 0.1);
margin: 32px 0;
}}
/* Premium Header Links */
.top-links {{
display: flex;
justify-content: flex-end;
gap: 16px;
margin-bottom: 24px;
}}
.top-link {{
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.08);
color: var(--text-main);
padding: 8px 16px;
border-radius: 30px;
font-size: 0.85rem;
font-weight: 500;
text-decoration: none;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.2s ease;
}}
.top-link:hover {{
background: var(--primary);
border-color: var(--primary);
box-shadow: 0 0 15px rgba(99, 102, 241, 0.4);
}}
</style>
</head>
<body>
<div class="container">
<div class="top-links">
<a href="/docs" class="top-link">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
Swagger UI (API Interactive)
</a>
<a href="/openapi.json" class="top-link">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
OpenAPI Spec (JSON)
</a>
</div>
<div class="markdown-body" id="content">Chargement de la documentation...</div>
</div>
<script>
const markdownText = {escaped_content};
document.getElementById('content').innerHTML = marked.parse(markdownText);
</script>
</body>
</html>"""
return HTMLResponse(content=html_template, status_code=200)
except Exception as e:
return HTMLResponse(
content=f"<h1>Erreur</h1><p>Impossible de charger la documentation : {str(e)}</p>",
status_code=500
)
# --- ENDPOINTS API ---
@app.get(
+51
View File
@@ -0,0 +1,51 @@
#!/bin/sh
set -eu
DEFAULT_API_KEYS_RAW="widget-token-secure-789,ai-agent-token-secure-101"
PERSISTED_ENV_FILE="${PERSISTED_ENV_FILE:-/app/data/.env}"
APP_ENV_FILE="${APP_ENV_FILE:-/app/.env}"
read_env_value() {
file="$1"
name="$2"
if [ ! -f "$file" ]; then
return 0
fi
sed -n "s/^[[:space:]]*${name}[[:space:]]*=[[:space:]]*//p" "$file" \
| tail -n 1 \
| sed 's/^"//; s/"$//; s/^'\''//; s/'\''$//'
}
write_env_value() {
file="$1"
name="$2"
value="$3"
mkdir -p "$(dirname "$file")"
if [ -f "$file" ] && grep -q "^[[:space:]]*${name}[[:space:]]*=" "$file"; then
escaped_value=$(printf '%s' "$value" | sed 's/[\/&]/\\&/g')
sed -i "s/^[[:space:]]*${name}[[:space:]]*=.*/${name}=${escaped_value}/" "$file"
else
printf '%s=%s\n' "$name" "$value" >> "$file"
fi
}
persisted_api_keys="$(read_env_value "$PERSISTED_ENV_FILE" "API_KEYS_RAW" || true)"
app_env_api_keys="$(read_env_value "$APP_ENV_FILE" "API_KEYS_RAW" || true)"
current_api_keys="${API_KEYS_RAW:-}"
if [ -n "$persisted_api_keys" ] && { [ -z "$current_api_keys" ] || [ "$current_api_keys" = "$DEFAULT_API_KEYS_RAW" ]; }; then
export API_KEYS_RAW="$persisted_api_keys"
elif [ -n "$app_env_api_keys" ] && { [ -z "$current_api_keys" ] || [ "$current_api_keys" = "$DEFAULT_API_KEYS_RAW" ]; }; then
export API_KEYS_RAW="$app_env_api_keys"
if [ "$app_env_api_keys" != "$DEFAULT_API_KEYS_RAW" ]; then
write_env_value "$PERSISTED_ENV_FILE" "API_KEYS_RAW" "$app_env_api_keys"
fi
elif [ -n "$current_api_keys" ] && [ "$current_api_keys" != "$DEFAULT_API_KEYS_RAW" ]; then
write_env_value "$PERSISTED_ENV_FILE" "API_KEYS_RAW" "$current_api_keys"
fi
exec "$@"
+17 -2
View File
@@ -4,10 +4,22 @@
Développement d'un widget Windows moderne et simple connecté à un serveur centralisé hébergé dans un conteneur Docker. L'API du serveur doit être accessible par un agent IA pour piloter la liste de tâches. Les tâches doivent afficher leur date de création et pouvoir être marquées comme "En cours" ou "Terminé".
## Focus Actuel
- Phase 3 : Développement du Widget Windows avec Tauri et design Glassmorphism.
- Mise en place du démarrage automatique et de l'option "Toujours au premier plan".
- Projet stable, fonctionnel et documenté.
## Master Plan
- [x] **Correction du Bug d'Édition du Widget**
- [x] Identifier la cause racine (le polling de 4s écrase le DOM et provoque un blur de l'input)
- [x] Modifier `fetchTasks()` dans `widget/src/app.js` pour suspendre la synchronisation durant l'édition
- [x] Tester et valider la correction localement
- [x] **Synchronisation avec le Dépôt Distant**
- [x] Mettre à jour `task.md` pour refléter la demande de l'utilisateur
- [x] Effectuer un `git pull` pour appliquer les 20 commits de retard sur `origin/main`
- [x] Valider l'état des fichiers locaux et s'assurer que la copie locale est propre et fonctionnelle
- [x] **Phase 6 : Mise à Jour de la Documentation pour Agent IA Externe (README)**
- [x] Concevoir le plan d'action simplifié (`implementation_plan.md`)
- [x] Documenter la route manquante `GET /api/v1/tasks/{task_id}` dans `README_AI_AGENT.md`
- [x] Ajouter les schémas d'outils (Function Calling) pour LLMs dans `README_AI_AGENT.md`
- [x] Vérifier la cohérence de la documentation avec le code réel de l'API
- [x] **Phase 1 : Conception & Architecture**
- [x] Créer le fichier `task.md`
- [x] Consulter le sous-agent `architecte` pour la structure de l'API et du conteneur
@@ -42,3 +54,6 @@ Développement d'un widget Windows moderne et simple connecté à un serveur cen
## Journal de Progression
- **2026-05-26** : Plan validé par l'utilisateur. Ajout des spécifications : configuration de l'URL API dans le widget, création directe de tâches, mode toujours au premier plan, et démarrage automatique. Début du développement du serveur API.
- **2026-05-26** : Implémentation du serveur API conteneurisé (FastAPI + SQLModel + Docker Compose) et du widget de bureau Tauri (HTML/CSS/JS, drag-region, always-on-top natif, autostart registre Windows, tray icon). Rédaction des tests unitaires et du guide d'intégration pour Agent IA. Début de la validation.
- **2026-05-27** : Récupération des dernières modifications du dépôt distant (`origin/main`). Synchronisation de 20 commits incluant des améliorations de l'interface (style glassmorphic, redimensionnement natif, icônes premium 3D) et des correctifs pour l'intégration Docker/Tauri. La copie locale est désormais à jour.
- **2026-05-27** : Mise à jour complète du guide d'intégration `README_AI_AGENT.md`. Ajout de la documentation pour la route `GET /api/v1/tasks/{task_id}` et création de schémas complets d'outils (Function Calling / Tools) pour OpenAI, Gemini et Anthropic Claude pour faciliter l'intégration immédiate d'un agent externe.
- **2026-05-27** : Correction du bug d'édition de tâches sur le widget. Ajout d'une condition pour suspendre temporairement le rafraîchissement périodique (polling de 4s) lorsque l'utilisateur édite le titre d'une tâche. Cela évite le rafraîchissement destructif du DOM et la perte de focus.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "todoflow-widget",
"version": "1.0.0",
"version": "1.1.0",
"private": true,
"scripts": {
"tauri": "tauri"
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "todoflow"
version = "1.0.0"
version = "1.1.0"
description = "Sleek Windows TodoFlow desktop widget"
authors = ["Michael"]
edition = "2021"
+28 -14
View File
@@ -67,6 +67,32 @@ fn get_autostart() -> Result<bool, String> {
Ok(val.is_ok())
}
// --- GESTION DE LA VISIBILITÉ DE LA FENÊTRE ---
fn toggle_window_visibility(app: &AppHandle) {
// Bascule l'affichage du widget depuis l'icône de la barre d'état.
// Comme la fenêtre est configurée avec skipTaskbar:true, l'icône du tray est le
// seul point de restauration : il doit donc fonctionner dans tous les états.
let window = match app.get_window("main") {
Some(w) => w,
None => return,
};
// Une fenêtre réduite est toujours rapportée comme "visible" par Tauri. On la
// considère donc comme masquée afin de la restaurer correctement (et non la re-masquer).
let is_minimized = window.is_minimized().unwrap_or(false);
let is_visible = window.is_visible().unwrap_or(false);
if is_visible && !is_minimized {
let _ = window.hide();
} else {
// Restauration robuste : dé-minimiser, ré-afficher, puis ramener au premier plan.
let _ = window.unminimize();
let _ = window.show();
let _ = window.set_focus();
}
}
// --- CONTEXTE ET INITIALISATION ---
fn main() {
@@ -84,26 +110,14 @@ fn main() {
.on_system_tray_event(|app, event| match event {
SystemTrayEvent::LeftClick { .. } => {
// Un simple clic gauche sur l'icône de la barre des tâches bascule la visibilité
let window = app.get_window("main").unwrap();
if window.is_visible().unwrap() {
window.hide().unwrap();
} else {
window.show().unwrap();
window.set_focus().unwrap();
}
toggle_window_visibility(app);
}
SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() {
"quit" => {
std::process::exit(0);
}
"toggle" => {
let window = app.get_window("main").unwrap();
if window.is_visible().unwrap() {
window.hide().unwrap();
} else {
window.show().unwrap();
window.set_focus().unwrap();
}
toggle_window_visibility(app);
}
_ => {}
},
+4 -2
View File
@@ -8,7 +8,7 @@
},
"package": {
"productName": "TodoFlow",
"version": "1.0.0"
"version": "1.1.0"
},
"tauri": {
"allowlist": {
@@ -58,9 +58,11 @@
{
"fullscreen": false,
"height": 520,
"resizable": false,
"resizable": true,
"title": "TodoFlow",
"width": 340,
"minWidth": 280,
"minHeight": 400,
"decorations": false,
"transparent": true,
"alwaysOnTop": false,
+9 -1
View File
@@ -88,7 +88,10 @@ document.addEventListener("DOMContentLoaded", async () => {
// Événements Tauri pour le contrôle de la fenêtre de bureau
if (appWindow) {
btnMinimize.addEventListener("click", () => appWindow.minimize());
// La fenêtre utilise skipTaskbar:true : elle n'apparaît jamais dans la barre des tâches.
// Un minimize() classique la rendrait donc irrécupérable. On masque plutôt vers le
// system tray, d'où elle peut toujours être restaurée via l'icône de la barre d'état.
btnMinimize.addEventListener("click", () => appWindow.hide());
btnClose.addEventListener("click", () => appWindow.hide()); // Masquer vers le tray au clic sur 'Fermer'
// Récupération de l'état réel du démarrage automatique depuis le registre Windows
@@ -176,6 +179,11 @@ async function saveSettings() {
// --- APPELS API (CRUD) ---
async function fetchTasks() {
// Suspendre la synchronisation si l'utilisateur est en train d'éditer une tâche pour éviter de perdre le focus et fermer l'édition
if (document.querySelector(".task-card.editing")) {
return;
}
// Récupération asynchrone des tâches avec la clé API
try {
const response = await fetch(`${state.apiUrl}/api/v1/tasks`, {
+345 -296
View File
@@ -1,37 +1,43 @@
/* --- SYSTÈME DE DESIGN & TOKENS FUTURISTES --- */
:root {
--widget-opacity: 0.70;
--bg-widget: rgba(18, 22, 33, var(--widget-opacity));
--bg-card: rgba(255, 255, 255, 0.03);
--bg-card-hover: rgba(255, 255, 255, 0.07);
--border-widget: 1px solid rgba(255, 255, 255, 0.09);
--border-card: 1px solid rgba(255, 255, 255, 0.05);
--bg-widget: rgba(11, 14, 26, var(--widget-opacity));
--bg-card: rgba(255, 255, 255, 0.035);
--bg-card-hover: rgba(255, 255, 255, 0.08);
--border-widget: 1px solid rgba(255, 255, 255, 0.11);
--border-card: 1px solid rgba(255, 255, 255, 0.06);
/* Couleurs thématiques HSL */
--accent-indigo: hsl(250, 85%, 65%);
--accent-indigo-glow: rgba(99, 102, 241, 0.3);
--accent-mint: hsl(165, 80%, 42%);
--accent-mint-glow: rgba(16, 185, 129, 0.15);
--accent-amber: hsl(38, 95%, 55%);
--accent-amber-glow: rgba(245, 158, 11, 0.15);
/* Couleurs de marque HSL vibrantes */
--accent-indigo: hsl(250, 95%, 68%);
--accent-indigo-glow: rgba(99, 102, 241, 0.35);
--accent-mint: hsl(165, 90%, 45%);
--accent-mint-glow: rgba(16, 185, 129, 0.2);
--accent-amber: hsl(38, 95%, 58%);
--accent-amber-glow: rgba(245, 158, 11, 0.2);
/* Textes */
--text-primary: hsl(220, 20%, 96%);
--text-secondary: hsl(220, 12%, 68%);
--text-muted: rgba(241, 243, 249, 0.45);
--text-primary: hsl(220, 20%, 97%);
--text-secondary: hsl(220, 14%, 72%);
--text-muted: rgba(241, 243, 249, 0.40);
/* Gradients */
--gradient-primary: linear-gradient(135deg, var(--accent-indigo), var(--accent-mint));
--gradient-card-hover: linear-gradient(135deg, rgba(99, 102, 241, 0.1), rgba(16, 185, 129, 0.05));
}
/* Réinitialisation de base */
/* Réinitialisation moderne */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
user-select: none; /* Évite la sélection de texte accidentelle pour un widget */
user-select: none;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
body {
background: transparent; /* Indispensable pour la transparence de la fenêtre Tauri */
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
color: var(--text-primary);
background: transparent;
display: flex;
justify-content: center;
align-items: center;
@@ -39,27 +45,30 @@ body {
overflow: hidden;
}
/* --- LE CONTAINER DU WIDGET (GLASSMORPHISM PREMIUM) --- */
/* --- LE CONTAINER DU WIDGET (GLASSMORPHISM ABSOLU RESONSIVE) --- */
.todo-widget {
width: 340px;
height: 520px;
width: 100%;
height: 100%;
min-width: 280px;
min-height: 400px;
background: var(--bg-widget);
backdrop-filter: blur(30px) saturate(125%);
-webkit-backdrop-filter: blur(30px) saturate(125%);
border-radius: 16px;
backdrop-filter: blur(40px) saturate(150%);
-webkit-backdrop-filter: blur(40px) saturate(150%);
border-radius: 20px;
border: var(--border-widget);
box-shadow:
0 8px 32px 0 rgba(0, 0, 0, 0.35),
0 1px 0px 0px rgba(255, 255, 255, 0.12) inset;
0 12px 40px 0 rgba(0, 0, 0, 0.5),
0 1px 0px 0px rgba(255, 255, 255, 0.15) inset;
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
transition: background 0.3s ease;
}
/* --- HEADER DU WIDGET --- */
.widget-header {
padding: 16px 16px 12px 16px;
padding: 18px 18px 12px 18px;
display: flex;
justify-content: space-between;
align-items: center;
@@ -68,7 +77,7 @@ body {
.header-drag-zone {
display: flex;
align-items: center;
gap: 8px;
gap: 10px;
flex: 1;
height: 100%;
cursor: grab;
@@ -78,102 +87,108 @@ body {
cursor: grabbing;
}
/* Indicateur d'état connecté à l'API */
/* Indicateur d'état connecté lumineux */
.status-indicator {
width: 8px;
height: 8px;
width: 9px;
height: 9px;
border-radius: 50%;
transition: all 0.5s ease;
transition: all 0.4s ease;
}
.status-indicator.connected {
background-color: var(--accent-mint);
box-shadow: 0 0 8px var(--accent-mint);
box-shadow: 0 0 10px 2px var(--accent-mint);
}
.status-indicator.disconnected {
background-color: hsl(0, 80%, 60%);
box-shadow: 0 0 8px rgba(239, 68, 68, 0.5);
background-color: hsl(0, 85%, 60%);
box-shadow: 0 0 10px 2px rgba(239, 68, 68, 0.6);
}
.widget-title {
font-size: 14px;
font-weight: 700;
letter-spacing: 0.8px;
font-size: 15px;
font-weight: 800;
letter-spacing: 1.5px;
background: var(--gradient-primary);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
text-transform: uppercase;
color: var(--text-primary);
opacity: 0.95;
}
.header-right {
display: flex;
align-items: center;
gap: 4px;
z-index: 10; /* Assure le clic par-dessus la zone de drag */
gap: 6px;
z-index: 10;
}
/* Boutons de contrôle style Windows */
/* Boutons de contrôle Premium */
.win-btn {
background: transparent;
border: none;
width: 26px;
height: 26px;
border-radius: 6px;
width: 28px;
height: 28px;
border-radius: 8px;
color: var(--text-secondary);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
}
.win-btn:hover {
background: rgba(255, 255, 255, 0.08);
color: var(--text-primary);
transform: scale(1.05);
}
.win-btn.btn-close:hover {
background: hsl(0, 75%, 50%);
color: #fff;
box-shadow: 0 0 12px rgba(239, 68, 68, 0.4);
}
/* --- INDICATEUR DE PROGRESSION --- */
/* --- INDICATEUR DE PROGRESSION HAUT DE GAMME --- */
.progress-container {
width: 100%;
height: 2px;
background: rgba(255, 255, 255, 0.05);
height: 3px;
background: rgba(255, 255, 255, 0.04);
overflow: hidden;
}
.progress-bar {
height: 100%;
background: linear-gradient(90deg, var(--accent-indigo), var(--accent-mint));
transition: width 0.4s cubic-bezier(0.16, 1, 0.3, 1);
background: var(--gradient-primary);
box-shadow: 0 0 8px var(--accent-indigo-glow);
transition: width 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}
/* --- ZONE DE CORPS --- */
.widget-body {
flex: 1;
padding: 16px;
padding: 18px;
display: flex;
flex-direction: column;
gap: 16px;
overflow: hidden;
}
/* Formulaire d'ajout de tâche */
/* Formulaire d'ajout de tâche flottant */
.add-task-form {
display: flex;
gap: 8px;
background: rgba(0, 0, 0, 0.15);
border: 1px solid rgba(255, 255, 255, 0.03);
border-radius: 10px;
padding: 4px 6px;
transition: border-color 0.3s;
gap: 10px;
background: rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.04);
border-radius: 12px;
padding: 5px 6px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.add-task-form:focus-within {
border-color: rgba(99, 102, 241, 0.4);
border-color: rgba(99, 102, 241, 0.5);
box-shadow: 0 4px 20px rgba(99, 102, 241, 0.15);
}
.add-task-form input {
@@ -182,36 +197,37 @@ body {
border: none;
outline: none;
color: var(--text-primary);
font-size: 13px;
padding: 6px;
font-size: 13.5px;
padding: 8px;
font-family: inherit;
}
.add-task-form input::placeholder {
color: rgba(241, 243, 249, 0.35);
color: rgba(241, 243, 249, 0.3);
}
.btn-add {
background: var(--accent-indigo);
background: var(--gradient-primary);
border: none;
width: 28px;
height: 28px;
border-radius: 8px;
width: 32px;
height: 32px;
border-radius: 10px;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.25s cubic-bezier(0.16, 1, 0.3, 1);
box-shadow: 0 4px 12px var(--accent-indigo-glow);
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.btn-add:hover {
transform: scale(1.05);
box-shadow: 0 0 10px var(--accent-indigo-glow);
transform: scale(1.08);
box-shadow: 0 4px 16px rgba(99, 102, 241, 0.4);
}
.btn-add svg {
transition: transform 0.3s ease;
transition: transform 0.4s ease;
}
.btn-add:hover svg {
@@ -222,13 +238,13 @@ body {
.tasks-list {
flex: 1;
overflow-y: auto;
padding-right: 2px;
padding-right: 4px;
display: flex;
flex-direction: column;
gap: 8px;
gap: 10px;
}
/* Personnalisation Scrollbar */
/* Scrollbar futuriste */
.tasks-list::-webkit-scrollbar {
width: 4px;
}
@@ -238,37 +254,45 @@ body {
}
.tasks-list::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
background: rgba(255, 255, 255, 0.08);
border-radius: 10px;
transition: background 0.3s;
}
/* --- CARTE DE TÂCHE INDIVIDUELLE --- */
.tasks-list::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.15);
}
/* --- CARTE DE TÂCHE SQUIRCLE GLASS --- */
.task-card {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.07);
border-radius: 12px;
padding: 12px 14px;
background: rgba(255, 255, 255, 0.045);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 14px;
padding: 14px 16px;
display: flex;
align-items: center;
gap: 12px;
gap: 14px;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
position: relative;
opacity: 1;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
}
.task-card:hover {
transform: translateY(-2px);
background: rgba(255, 255, 255, 0.08);
border-color: rgba(99, 102, 241, 0.35);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
box-shadow:
0 8px 24px rgba(99, 102, 241, 0.18),
0 4px 12px rgba(0, 0, 0, 0.2);
}
/* Checkbox personnalisée */
/* Checkbox Squircle interactive */
.custom-checkbox {
width: 18px;
height: 18px;
border-radius: 50%;
border: 1.5px solid rgba(255, 255, 255, 0.35);
width: 20px;
height: 20px;
border-radius: 7px;
border: 1.8px solid rgba(255, 255, 255, 0.3);
cursor: pointer;
display: flex;
align-items: center;
@@ -279,13 +303,15 @@ body {
.custom-checkbox:hover {
border-color: var(--accent-indigo);
background: rgba(99, 102, 241, 0.08);
background: rgba(99, 102, 241, 0.1);
transform: scale(1.05);
}
/* Checkbox cochée (Terminé) */
/* Checkbox Pop */
.custom-checkbox.checked {
background: linear-gradient(135deg, hsl(165, 80%, 42%), hsl(165, 80%, 32%));
background: var(--gradient-primary);
border-color: transparent;
box-shadow: 0 0 10px var(--accent-indigo-glow);
animation: checkboxPop 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
@@ -299,7 +325,7 @@ body {
}
@keyframes checkboxPop {
50% { transform: scale(1.2); }
50% { transform: scale(1.15); }
100% { transform: scale(1); }
}
@@ -308,12 +334,12 @@ body {
display: flex;
flex-direction: column;
flex-grow: 1;
min-width: 0; /* Permet le troncage correct du texte */
min-width: 0;
}
.task-title {
font-size: 13.5px;
font-weight: 500;
font-weight: 550;
color: var(--text-primary);
transition: color 0.3s;
position: relative;
@@ -327,16 +353,73 @@ body {
.task-date {
font-size: 9.5px;
color: var(--text-secondary);
margin-top: 3px;
margin-top: 4px;
}
/* --- BOUTONS ACTIONS SURVOL (MODIFIER / SUPPRIMER) --- */
.task-actions {
display: flex;
gap: 8px;
margin-right: 6px;
opacity: 0;
transition: opacity 0.25s ease;
}
.task-card:hover .task-actions {
opacity: 1;
}
.btn-task-action {
background: transparent;
border: none;
color: var(--text-secondary);
cursor: pointer;
width: 24px;
height: 24px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.btn-task-action:hover {
background: rgba(255, 255, 255, 0.08);
color: var(--text-primary);
transform: scale(1.08);
}
.btn-task-action.btn-delete:hover {
background: rgba(239, 68, 68, 0.15);
color: hsl(0, 80%, 65%);
box-shadow: 0 0 8px rgba(239, 68, 68, 0.2);
}
.btn-task-action svg {
pointer-events: none;
}
/* Input d'édition premium */
.task-edit-input {
background: rgba(255, 255, 255, 0.09);
border: 1px solid var(--accent-indigo);
border-radius: 6px;
color: var(--text-primary);
font-size: 13px;
font-family: inherit;
padding: 3px 8px;
width: 100%;
outline: none;
box-shadow: 0 0 10px var(--accent-indigo-glow);
}
/* --- BADGES DE STATUT --- */
.status-badge {
font-size: 8.5px;
font-weight: 600;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
padding: 3px 7px;
padding: 4px 8px;
border-radius: 20px;
flex-shrink: 0;
}
@@ -344,26 +427,39 @@ body {
.status-badge.in-progress {
background: var(--accent-amber-glow);
color: var(--accent-amber);
border: 1px solid rgba(245, 158, 11, 0.1);
border: 1px solid rgba(245, 158, 11, 0.2);
cursor: pointer;
transition: all 0.2s;
}
.status-badge.in-progress:hover {
transform: scale(1.05);
background: rgba(245, 158, 11, 0.3);
}
.status-badge.todo {
background: rgba(255, 255, 255, 0.05);
color: var(--text-secondary);
border: 1px solid rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.08);
cursor: pointer;
transition: all 0.2s;
}
.status-badge.todo:hover {
transform: scale(1.05);
background: rgba(255, 255, 255, 0.12);
}
.status-badge.done {
background: var(--accent-mint-glow);
color: var(--accent-mint);
border: 1px solid rgba(16, 185, 129, 0.1);
border: 1px solid rgba(16, 185, 129, 0.2);
}
/* --- DESIGN DES TÂCHES TERMINÉES (ANIMÉ) --- */
.task-card.completed {
opacity: 0.5;
opacity: 0.45;
background: rgba(0, 0, 0, 0.1);
}
.task-card.completed .task-title {
@@ -380,14 +476,28 @@ body {
background: var(--text-muted);
transform: scaleX(0);
transform-origin: left;
transition: transform 0.35s cubic-bezier(0.16, 1, 0.3, 1);
transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}
.task-card.completed .task-title::after {
transform: scaleX(1);
}
/* État vide */
/* --- ACCORDION EXPANSION STATE --- */
.task-card.expanded {
background: rgba(255, 255, 255, 0.09);
border-color: rgba(99, 102, 241, 0.4);
}
.task-card.expanded .task-title {
white-space: normal;
overflow: visible;
text-overflow: unset;
display: block;
word-break: break-word;
}
/* État vide premium */
.empty-state {
display: flex;
flex-direction: column;
@@ -400,14 +510,15 @@ body {
}
.empty-state p {
font-size: 13px;
font-weight: 500;
margin-bottom: 4px;
font-size: 13.5px;
font-weight: 600;
margin-bottom: 6px;
color: var(--text-primary);
}
.empty-subtext {
font-size: 10.5px;
opacity: 0.7;
opacity: 0.65;
}
/* --- PANNEAU DE PARAMÈTRES (BOTTOM SLIDE) --- */
@@ -416,18 +527,20 @@ body {
bottom: 0;
left: 0;
width: 100%;
height: 380px;
background: rgba(15, 18, 28, 0.95);
height: 390px;
background: rgba(13, 16, 28, 0.96);
backdrop-filter: blur(40px);
-webkit-backdrop-filter: blur(40px);
border-top: var(--border-widget);
border-radius: 16px 16px 0 0;
padding: 20px;
border-radius: 20px 20px 0 0;
padding: 22px;
display: flex;
flex-direction: column;
gap: 16px;
transform: translateY(100%);
transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
transition: transform 0.45s cubic-bezier(0.16, 1, 0.3, 1);
z-index: 100;
box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.4);
box-shadow: 0 -10px 30px rgba(0, 0, 0, 0.5);
}
.settings-panel.open {
@@ -442,9 +555,12 @@ body {
.settings-header h2 {
font-size: 14px;
font-weight: 700;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.5px;
letter-spacing: 0.8px;
background: var(--gradient-primary);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.btn-icon-close {
@@ -452,8 +568,8 @@ body {
border: none;
color: var(--text-secondary);
cursor: pointer;
width: 24px;
height: 24px;
width: 26px;
height: 26px;
border-radius: 50%;
display: flex;
align-items: center;
@@ -480,177 +596,32 @@ body {
}
.setting-group label {
font-size: 11px;
font-weight: 500;
font-size: 11.5px;
font-weight: 600;
color: var(--text-secondary);
}
.setting-group input {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 8px;
padding: 8px 10px;
border-radius: 10px;
padding: 10px 12px;
color: var(--text-primary);
font-size: 12px;
font-size: 12.5px;
outline: none;
font-family: inherit;
transition: border-color 0.3s;
transition: all 0.3s;
}
.setting-group input:focus {
border-color: var(--accent-indigo);
}
/* Switch Toggles */
.setting-toggle {
display: flex;
align-items: center;
justify-content: space-between;
padding: 4px 0;
}
.switch-container {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
cursor: pointer;
}
.switch-label {
font-size: 12px;
font-weight: 500;
color: var(--text-primary);
}
.switch-container input {
display: none;
}
.switch-slider {
width: 34px;
height: 18px;
background-color: rgba(255, 255, 255, 0.15);
border-radius: 20px;
position: relative;
transition: background-color 0.3s;
}
.switch-slider::before {
content: "";
position: absolute;
width: 12px;
height: 12px;
left: 3px;
bottom: 3px;
background-color: white;
border-radius: 50%;
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.switch-container input:checked + .switch-slider {
background-color: var(--accent-indigo);
}
.switch-container input:checked + .switch-slider::before {
transform: translateX(16px);
}
/* Bouton enregistrer */
.btn-save {
background: var(--accent-indigo);
color: white;
border: none;
border-radius: 8px;
padding: 10px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
margin-top: 8px;
transition: all 0.25s cubic-bezier(0.16, 1, 0.3, 1);
}
.btn-save:hover {
background: hsl(250, 85%, 58%);
box-shadow: 0 0 10px var(--accent-indigo-glow);
}
/* --- BOUTONS ACTIONS DES TÂCHES (MODIFIER / SUPPRIMER) --- */
.task-actions {
display: flex;
gap: 6px;
margin-right: 8px;
opacity: 0;
transition: opacity 0.2s ease;
}
.task-card:hover .task-actions {
opacity: 1;
}
.btn-task-action {
background: transparent;
border: none;
color: var(--text-secondary);
cursor: pointer;
width: 22px;
height: 22px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.btn-task-action:hover {
background: rgba(255, 255, 255, 0.08);
color: var(--text-primary);
}
.btn-task-action.btn-delete:hover {
background: rgba(239, 68, 68, 0.15);
color: hsl(0, 80%, 65%);
}
.btn-task-action svg {
pointer-events: none; /* Évite d'interférer avec le clic */
}
/* Style de l'input d'édition en ligne */
.task-edit-input {
background: rgba(255, 255, 255, 0.1);
border: 1px solid var(--accent-indigo);
border-radius: 6px;
color: var(--text-primary);
font-size: 13px;
font-family: inherit;
padding: 2px 6px;
width: 100%;
outline: none;
box-shadow: 0 0 8px var(--accent-indigo-glow);
}
/* --- ACCORDION / EXPANDED STATE --- */
.task-card {
cursor: pointer;
}
.task-card.expanded {
background: var(--bg-card-hover);
border-color: rgba(99, 102, 241, 0.3);
}
.task-card.expanded .task-title {
white-space: normal;
overflow: visible;
text-overflow: unset;
display: block;
word-break: break-word;
}
/* --- RANGE SLIDER STYLING --- */
/* Transparence Slider */
.slider-opacity {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 6px;
border-radius: 5px;
@@ -676,23 +647,99 @@ body {
transform: scale(1.2);
}
/* --- CUSTOM CONFIRMATION MODAL OVERLAY --- */
/* Switch Toggles */
.setting-toggle {
display: flex;
align-items: center;
justify-content: space-between;
padding: 4px 0;
}
.switch-container {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
cursor: pointer;
}
.switch-label {
font-size: 12.5px;
font-weight: 550;
color: var(--text-primary);
}
.switch-container input {
display: none;
}
.switch-slider {
width: 36px;
height: 20px;
background-color: rgba(255, 255, 255, 0.15);
border-radius: 20px;
position: relative;
transition: background-color 0.3s;
}
.switch-slider::before {
content: "";
position: absolute;
width: 14px;
height: 14px;
left: 3px;
bottom: 3px;
background-color: white;
border-radius: 50%;
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.switch-container input:checked + .switch-slider {
background-color: var(--accent-indigo);
box-shadow: 0 0 8px var(--accent-indigo-glow);
}
.switch-container input:checked + .switch-slider::before {
transform: translateX(16px);
}
/* Bouton enregistrer premium */
.btn-save {
background: var(--gradient-primary);
color: white;
border: none;
border-radius: 10px;
padding: 12px;
font-size: 13.5px;
font-weight: 700;
cursor: pointer;
margin-top: 8px;
box-shadow: 0 4px 12px var(--accent-indigo-glow);
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.btn-save:hover {
transform: scale(1.02);
box-shadow: 0 4px 20px rgba(99, 102, 241, 0.4);
}
/* --- GLASSMORPHIC CONFIRMATION MODAL --- */
.confirm-modal-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.45);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
background: rgba(0, 0, 0, 0.55);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s cubic-bezier(0.16, 1, 0.3, 1);
transition: opacity 0.35s cubic-bezier(0.16, 1, 0.3, 1);
}
.confirm-modal-overlay.open {
@@ -702,13 +749,13 @@ body {
.confirm-modal-box {
width: 290px;
background: rgba(18, 22, 33, 0.95);
background: rgba(14, 18, 30, 0.96);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 14px;
padding: 20px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
border-radius: 16px;
padding: 22px;
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.6);
transform: scale(0.9);
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
transition: transform 0.35s cubic-bezier(0.16, 1, 0.3, 1);
text-align: center;
}
@@ -717,30 +764,30 @@ body {
}
.confirm-modal-box h3 {
font-size: 15px;
font-weight: 700;
font-size: 16px;
font-weight: 800;
color: var(--text-primary);
margin-bottom: 8px;
margin-bottom: 10px;
}
.confirm-modal-box p {
font-size: 11.5px;
font-size: 12px;
color: var(--text-secondary);
line-height: 1.5;
margin-bottom: 20px;
line-height: 1.6;
margin-bottom: 22px;
}
.confirm-modal-actions {
display: flex;
gap: 10px;
gap: 12px;
justify-content: center;
}
.confirm-modal-actions button {
padding: 8px 16px;
font-size: 12px;
font-weight: 600;
border-radius: 8px;
padding: 10px 18px;
font-size: 12.5px;
font-weight: 700;
border-radius: 10px;
border: none;
cursor: pointer;
transition: all 0.2s;
@@ -750,6 +797,7 @@ body {
.btn-confirm-cancel {
background: rgba(255, 255, 255, 0.08);
color: var(--text-primary);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.btn-confirm-cancel:hover {
@@ -759,10 +807,11 @@ body {
.btn-confirm-delete {
background: hsl(0, 75%, 50%);
color: white;
box-shadow: 0 0 12px rgba(239, 68, 68, 0.3);
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
}
.btn-confirm-delete:hover {
background: hsl(0, 75%, 45%);
box-shadow: 0 0 16px rgba(239, 68, 68, 0.5);
box-shadow: 0 4px 18px rgba(239, 68, 68, 0.5);
transform: scale(1.02);
}