mirror of
https://github.com/R0m1k3/TodoFlow.git
synced 2026-10-11 17:28:17 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e759a1a7d5 | ||
|
|
3b5ede5872 | ||
|
|
a635aa29a4 | ||
|
|
fcac504693 | ||
|
|
27d3fb7d50 | ||
|
|
d9c1720523 | ||
|
|
e4a56f2fc6 | ||
|
|
3e6dbb5c76 | ||
|
|
1ca25917d4 | ||
|
|
278f74326c | ||
|
|
8cebc3c2fa | ||
|
|
9067b93c88 | ||
|
|
891782515d | ||
|
|
8214b2e9ac |
No files matched your search
@@ -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
@@ -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
@@ -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
@@ -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(
|
||||
|
||||
@@ -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 "$@"
|
||||
@@ -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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "todoflow-widget",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"tauri": "tauri"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
+182
-1
@@ -11,6 +11,7 @@ let state = {
|
||||
apiKey: "widget-token-secure-789",
|
||||
alwaysOnTop: false,
|
||||
autostart: false,
|
||||
opacity: 70,
|
||||
tasks: [],
|
||||
pollingInterval: null
|
||||
};
|
||||
@@ -38,6 +39,10 @@ const inputAutostart = document.getElementById("setting-autostart");
|
||||
const btnMinimize = document.getElementById("btn-minimize");
|
||||
const btnClose = document.getElementById("btn-close");
|
||||
|
||||
const inputOpacity = document.getElementById("setting-opacity");
|
||||
const opacityValue = document.getElementById("opacity-value");
|
||||
let taskToDeleteId = null;
|
||||
|
||||
// --- INITIALISATION ---
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
@@ -48,6 +53,33 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
inputApiUrl.value = state.apiUrl;
|
||||
inputApiKey.value = state.apiKey;
|
||||
inputAlwaysOnTop.checked = state.alwaysOnTop;
|
||||
inputOpacity.value = state.opacity;
|
||||
opacityValue.textContent = state.opacity;
|
||||
|
||||
// Real-time opacity slider updates
|
||||
inputOpacity.addEventListener("input", () => {
|
||||
const val = inputOpacity.value;
|
||||
opacityValue.textContent = val;
|
||||
document.documentElement.style.setProperty("--widget-opacity", val / 100);
|
||||
});
|
||||
|
||||
// Modal de confirmation de suppression personnalisé
|
||||
const confirmModal = document.getElementById("confirm-modal");
|
||||
const btnConfirmCancel = document.getElementById("btn-confirm-cancel");
|
||||
const btnConfirmDelete = document.getElementById("btn-confirm-delete");
|
||||
|
||||
btnConfirmCancel.addEventListener("click", () => {
|
||||
confirmModal.classList.remove("open");
|
||||
taskToDeleteId = null;
|
||||
});
|
||||
|
||||
btnConfirmDelete.addEventListener("click", async () => {
|
||||
if (taskToDeleteId) {
|
||||
confirmModal.classList.remove("open");
|
||||
await executeDeleteTask(taskToDeleteId);
|
||||
taskToDeleteId = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Événements d'ouverture/fermeture des paramètres
|
||||
btnSettings.addEventListener("click", () => settingsPanel.classList.add("open"));
|
||||
@@ -56,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
|
||||
@@ -88,6 +123,7 @@ function loadSettingsFromStorage() {
|
||||
const storedApiUrl = localStorage.getItem("todoflow_api_url");
|
||||
const storedApiKey = localStorage.getItem("todoflow_api_key");
|
||||
const storedAlwaysOnTop = localStorage.getItem("todoflow_always_on_top");
|
||||
const storedOpacity = localStorage.getItem("todoflow_opacity");
|
||||
|
||||
if (storedApiUrl) state.apiUrl = storedApiUrl;
|
||||
if (storedApiKey) state.apiKey = storedApiKey;
|
||||
@@ -98,6 +134,13 @@ function loadSettingsFromStorage() {
|
||||
invoke("toggle_always_on_top", { enabled: state.alwaysOnTop }).catch(console.error);
|
||||
}
|
||||
}
|
||||
if (storedOpacity) {
|
||||
state.opacity = parseInt(storedOpacity);
|
||||
document.documentElement.style.setProperty("--widget-opacity", state.opacity / 100);
|
||||
} else {
|
||||
state.opacity = 70;
|
||||
document.documentElement.style.setProperty("--widget-opacity", 0.70);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSettings() {
|
||||
@@ -105,14 +148,17 @@ async function saveSettings() {
|
||||
const newKey = inputApiKey.value.trim();
|
||||
const newAlwaysOnTop = inputAlwaysOnTop.checked;
|
||||
const newAutostart = inputAutostart.checked;
|
||||
const newOpacity = inputOpacity.value;
|
||||
|
||||
state.apiUrl = newUrl;
|
||||
state.apiKey = newKey;
|
||||
state.alwaysOnTop = newAlwaysOnTop;
|
||||
state.opacity = parseInt(newOpacity);
|
||||
|
||||
localStorage.setItem("todoflow_api_url", newUrl);
|
||||
localStorage.setItem("todoflow_api_key", newKey);
|
||||
localStorage.setItem("todoflow_always_on_top", newAlwaysOnTop.toString());
|
||||
localStorage.setItem("todoflow_opacity", newOpacity.toString());
|
||||
|
||||
// Appel des fonctions système Windows natives via Tauri
|
||||
if (appWindow && invoke) {
|
||||
@@ -133,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`, {
|
||||
@@ -265,6 +316,14 @@ function renderTasksList() {
|
||||
const card = document.createElement("div");
|
||||
card.className = `task-card ${isCompleted ? 'completed' : ''}`;
|
||||
|
||||
// Clic pour étendre/réduire le titre si long
|
||||
card.addEventListener("click", (e) => {
|
||||
if (e.target.closest(".custom-checkbox") || e.target.closest(".btn-task-action") || e.target.closest(".task-edit-input")) {
|
||||
return;
|
||||
}
|
||||
card.classList.toggle("expanded");
|
||||
});
|
||||
|
||||
// Checkbox personnalisée cochable
|
||||
const checkbox = document.createElement("div");
|
||||
checkbox.className = `custom-checkbox ${isCompleted ? 'checked' : ''}`;
|
||||
@@ -294,6 +353,39 @@ function renderTasksList() {
|
||||
content.appendChild(title);
|
||||
content.appendChild(date);
|
||||
|
||||
// Container d'actions (Edit / Delete)
|
||||
const actions = document.createElement("div");
|
||||
actions.className = "task-actions";
|
||||
|
||||
// Bouton Modifier
|
||||
const btnEdit = document.createElement("button");
|
||||
btnEdit.className = "btn-task-action btn-edit";
|
||||
btnEdit.title = "Modifier la tâche";
|
||||
btnEdit.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 1 1 3 3L12 15l-4 1 1-4z"/></svg>`;
|
||||
btnEdit.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
startEditingTask(task.id, card, title, task.title);
|
||||
});
|
||||
|
||||
// Double-clic sur le titre pour éditer également
|
||||
title.addEventListener("dblclick", (e) => {
|
||||
e.stopPropagation();
|
||||
startEditingTask(task.id, card, title, task.title);
|
||||
});
|
||||
|
||||
// Bouton Supprimer
|
||||
const btnDelete = document.createElement("button");
|
||||
btnDelete.className = "btn-task-action btn-delete";
|
||||
btnDelete.title = "Supprimer la tâche";
|
||||
btnDelete.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg>`;
|
||||
btnDelete.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
deleteTask(task.id);
|
||||
});
|
||||
|
||||
actions.appendChild(btnEdit);
|
||||
actions.appendChild(btnDelete);
|
||||
|
||||
// Badge de statut (En cours / À faire)
|
||||
const badge = document.createElement("span");
|
||||
|
||||
@@ -314,6 +406,7 @@ function renderTasksList() {
|
||||
|
||||
card.appendChild(checkbox);
|
||||
card.appendChild(content);
|
||||
card.appendChild(actions);
|
||||
card.appendChild(badge);
|
||||
|
||||
tasksList.appendChild(card);
|
||||
@@ -337,3 +430,91 @@ function formatDate(dateString) {
|
||||
return dateString;
|
||||
}
|
||||
}
|
||||
|
||||
// --- NOUVELLES FONCTIONS DE SUPPRESSION ET MODIFICATION ---
|
||||
|
||||
function startEditingTask(taskId, card, titleElement, currentTitle) {
|
||||
if (card.classList.contains("editing")) return;
|
||||
card.classList.add("editing");
|
||||
|
||||
const input = document.createElement("input");
|
||||
input.type = "text";
|
||||
input.className = "task-edit-input";
|
||||
input.value = currentTitle;
|
||||
input.maxLength = 100;
|
||||
|
||||
titleElement.innerHTML = "";
|
||||
titleElement.appendChild(input);
|
||||
input.focus();
|
||||
input.select();
|
||||
|
||||
const finishEditing = async (save) => {
|
||||
const newTitle = input.value.trim();
|
||||
card.classList.remove("editing");
|
||||
|
||||
if (save && newTitle && newTitle !== currentTitle) {
|
||||
titleElement.textContent = newTitle;
|
||||
await updateTaskTitle(taskId, newTitle);
|
||||
} else {
|
||||
titleElement.textContent = currentTitle;
|
||||
}
|
||||
};
|
||||
|
||||
input.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter") {
|
||||
finishEditing(true);
|
||||
} else if (e.key === "Escape") {
|
||||
finishEditing(false);
|
||||
}
|
||||
});
|
||||
|
||||
input.addEventListener("blur", () => {
|
||||
finishEditing(true);
|
||||
});
|
||||
|
||||
input.addEventListener("click", (e) => e.stopPropagation());
|
||||
}
|
||||
|
||||
async function updateTaskTitle(taskId, newTitle) {
|
||||
try {
|
||||
const response = await fetch(`${state.apiUrl}/api/v1/tasks/${taskId}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-API-Key": state.apiKey
|
||||
},
|
||||
body: JSON.stringify({
|
||||
title: newTitle
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error("Échec de la mise à jour");
|
||||
fetchTasks();
|
||||
} catch (error) {
|
||||
console.error("Erreur de mise à jour du titre :", error);
|
||||
alert("Impossible de modifier la tâche.");
|
||||
}
|
||||
}
|
||||
|
||||
function deleteTask(taskId) {
|
||||
taskToDeleteId = taskId;
|
||||
const modal = document.getElementById("confirm-modal");
|
||||
modal.classList.add("open");
|
||||
}
|
||||
|
||||
async function executeDeleteTask(taskId) {
|
||||
try {
|
||||
const response = await fetch(`${state.apiUrl}/api/v1/tasks/${taskId}`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"X-API-Key": state.apiKey
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error("Échec de la suppression");
|
||||
fetchTasks();
|
||||
} catch (error) {
|
||||
console.error("Erreur de suppression de la tâche :", error);
|
||||
alert("Impossible de supprimer la tâche.");
|
||||
}
|
||||
}
|
||||
@@ -84,6 +84,11 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-group">
|
||||
<label for="setting-opacity">Transparence du fond (<span id="opacity-value">70</span>%)</label>
|
||||
<input type="range" id="setting-opacity" min="20" max="100" value="70" class="slider-opacity">
|
||||
</div>
|
||||
|
||||
<div class="setting-toggle">
|
||||
<label class="switch-container">
|
||||
<span class="switch-label">Lancer au démarrage</span>
|
||||
@@ -102,6 +107,18 @@
|
||||
// Import dynamique pour supporter l'exécution dans le navigateur pendant le dév
|
||||
window.__TAURI__ = window.__TAURI__ || null;
|
||||
</script>
|
||||
<!-- Modal de confirmation de suppression personnalisé (Thème Glassmorphism) -->
|
||||
<div class="confirm-modal-overlay" id="confirm-modal">
|
||||
<div class="confirm-modal-box">
|
||||
<h3>Supprimer la tâche ?</h3>
|
||||
<p>Cette action est irréversible et supprimera définitivement la tâche de la base SaaS.</p>
|
||||
<div class="confirm-modal-actions">
|
||||
<button class="btn-confirm-cancel" id="btn-confirm-cancel">Annuler</button>
|
||||
<button class="btn-confirm-delete" id="btn-confirm-delete">Supprimer</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+388
-146
@@ -1,37 +1,43 @@
|
||||
/* --- SYSTÈME DE DESIGN & TOKENS --- */
|
||||
/* --- SYSTÈME DE DESIGN & TOKENS FUTURISTES --- */
|
||||
:root {
|
||||
--bg-widget: rgba(18, 22, 33, 0.70);
|
||||
--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);
|
||||
--widget-opacity: 0.70;
|
||||
--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: var(--bg-card);
|
||||
border: var(--border-card);
|
||||
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: var(--bg-card-hover);
|
||||
border-color: rgba(99, 102, 241, 0.2);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-color: rgba(99, 102, 241, 0.35);
|
||||
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,34 +334,92 @@ 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: 13px;
|
||||
font-weight: 500;
|
||||
font-size: 13.5px;
|
||||
font-weight: 550;
|
||||
color: var(--text-primary);
|
||||
transition: color 0.3s;
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: inline-block;
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
@@ -343,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 {
|
||||
@@ -379,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;
|
||||
@@ -399,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) --- */
|
||||
@@ -415,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 {
|
||||
@@ -441,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 {
|
||||
@@ -451,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;
|
||||
@@ -479,25 +596,55 @@ 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);
|
||||
box-shadow: 0 0 8px var(--accent-indigo-glow);
|
||||
}
|
||||
|
||||
/* Transparence Slider */
|
||||
.slider-opacity {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
border-radius: 5px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
outline: none;
|
||||
transition: background 0.3s;
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
.slider-opacity::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent-indigo);
|
||||
cursor: pointer;
|
||||
box-shadow: 0 0 10px var(--accent-indigo-glow);
|
||||
transition: transform 0.1s;
|
||||
}
|
||||
|
||||
.slider-opacity::-webkit-slider-thumb:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
/* Switch Toggles */
|
||||
@@ -517,8 +664,8 @@ body {
|
||||
}
|
||||
|
||||
.switch-label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
font-size: 12.5px;
|
||||
font-weight: 550;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
@@ -527,8 +674,8 @@ body {
|
||||
}
|
||||
|
||||
.switch-slider {
|
||||
width: 34px;
|
||||
height: 18px;
|
||||
width: 36px;
|
||||
height: 20px;
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 20px;
|
||||
position: relative;
|
||||
@@ -538,8 +685,8 @@ body {
|
||||
.switch-slider::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
left: 3px;
|
||||
bottom: 3px;
|
||||
background-color: white;
|
||||
@@ -549,27 +696,122 @@ body {
|
||||
|
||||
.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 */
|
||||
/* Bouton enregistrer premium */
|
||||
.btn-save {
|
||||
background: var(--accent-indigo);
|
||||
background: var(--gradient-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
font-size: 13.5px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
margin-top: 8px;
|
||||
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-save:hover {
|
||||
background: hsl(250, 85%, 58%);
|
||||
box-shadow: 0 0 10px var(--accent-indigo-glow);
|
||||
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.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.35s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.confirm-modal-overlay.open {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.confirm-modal-box {
|
||||
width: 290px;
|
||||
background: rgba(14, 18, 30, 0.96);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 16px;
|
||||
padding: 22px;
|
||||
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.6);
|
||||
transform: scale(0.9);
|
||||
transition: transform 0.35s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.confirm-modal-overlay.open .confirm-modal-box {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.confirm-modal-box h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.confirm-modal-box p {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.confirm-modal-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.confirm-modal-actions button {
|
||||
padding: 10px 18px;
|
||||
font-size: 12.5px;
|
||||
font-weight: 700;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.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 {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.btn-confirm-delete {
|
||||
background: hsl(0, 75%, 50%);
|
||||
color: white;
|
||||
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.btn-confirm-delete:hover {
|
||||
background: hsl(0, 75%, 45%);
|
||||
box-shadow: 0 4px 18px rgba(239, 68, 68, 0.5);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
Reference in new issue
Block a user