chore: clean complet du projet et ajout d'un README avec licence personnelle

- Suppression de tous les scripts de migration temporaires (fix_*.py, update_*.py, etc)
- Suppression des fichiers d'expérimentation html/json et .txt
- Modification du README: ajout d'une section licence usage non-commercial
- Modification du README: ajout d'une roadmap pour un déploiement public SaaS avec PostgreSQL et gestion de micro-services IO/Abonnement.
This commit is contained in:
Sepehr Ramezani
2026-04-17 21:53:10 +02:00
parent f822a6eb18
commit a57c277168
36 changed files with 50 additions and 3988 deletions

View File

@@ -1,323 +0,0 @@
# 🔧 CORRECTION COMPLÈTE : Thème Slate
Ramez, voici le fichier CORRIGÉ complet à copier dans `keep-notes/app/globals.css`
---
## INSTRUCTIONS PRÉCISES
### Étape 1 : Remplacez le thème principal (:root)
Cherchez `:root {` dans le fichier (ligne 100)
Remplacez TOUTES les lignes 100-133 par ceci :
```css
:root {
--radius: 0.625rem;
/* ============================================
THEME SLATE (GRIS-BLEU) - PRINCIPAL ⭐
============================================ */
/* Backgrounds */
--background: oklch(0.985 0.003 230); /* Blanc grisâtre léger */
--card: oklch(1 0 0); /* Blanc pur */
--sidebar: oklch(0.97 0.004 230); /* Gris-bleu très pâle */
--input: oklch(0.98 0.003 230); /* Gris-bleu pâle */
/* Textes */
--foreground: oklch(0.2 0.02 230); /* Gris-bleu foncé */
--card-foreground: oklch(0.2 0.02 230);
--popover-foreground: oklch(0.2 0.02 230);
--foreground-secondary: oklch(0.45 0.015 230); /* Gris-bleu moyen */
--foreground-muted: oklch(0.6 0.01 230); /* Gris-bleu clair */
/* Primary Actions - GRIS-BLEU, PAS BLEU SATURÉ */
--primary: oklch(0.45 0.08 230); /* Gris-bleu doux */
--primary-foreground: oklch(0.99 0 0); /* Blanc */
/* Secondary */
--secondary: oklch(0.94 0.005 230); /* Gris-bleu très pâle */
--secondary-foreground: oklch(0.2 0.02 230);
/* Accents */
--muted: oklch(0.92 0.005 230);
--muted-foreground: oklch(0.6 0.01 230);
--accent: oklch(0.94 0.005 230);
--accent-foreground: oklch(0.2 0.02 230);
/* Functional */
--destructive: oklch(0.6 0.18 25); /* Rouge */
--border: oklch(0.9 0.008 230); /* Gris-bleu très clair */
--input: oklch(0.98 0.003 230);
--ring: oklch(0.7 0.005 230);
/* Sidebar */
--sidebar: oklch(0.97 0.004 230);
--sidebar-foreground: oklch(0.2 0.02 230);
--sidebar-primary: oklch(0.45 0.08 230);
--sidebar-primary-foreground: oklch(0.99 0 0);
--sidebar-accent: oklch(0.94 0.005 230);
--sidebar-accent-foreground: oklch(0.2 0.02 230);
--sidebar-border: oklch(0.9 0.008 230);
--sidebar-ring: oklch(0.7 0.005 230);
/* Popover */
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.2 0.02 230);
/* Charts (conservés) */
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
}
```
---
### Étape 2 : Remplacez le thème dark par défaut (.dark)
Cherchez `.dark {` dans le fichier (ligne 135)
Remplacez TOUTES les lignes 135-167 par ceci :
```css
.dark {
/* ============================================
THEME SLATE DARK MODE
============================================ */
/* Backgrounds */
--background: oklch(0.14 0.005 230); /* Noir grisâtre */
--card: oklch(0.18 0.006 230); /* Gris-bleu foncé */
--sidebar: oklch(0.12 0.005 230); /* Noir grisâtre */
--input: oklch(0.2 0.006 230);
/* Textes */
--foreground: oklch(0.97 0.003 230); /* Blanc grisâtre */
--card-foreground: oklch(0.97 0.003 230);
--popover-foreground: oklch(0.97 0.003 230);
--foreground-secondary: oklch(0.75 0.008 230);
--foreground-muted: oklch(0.55 0.01 230);
--popover-foreground: oklch(0.97 0.003 230);
/* Primary Actions */
--primary: oklch(0.55 0.08 230); /* Gris-bleu plus clair */
--primary-foreground: oklch(0.1 0 0); /* Noir */
/* Secondary */
--secondary: oklch(0.24 0.006 230);
--secondary-foreground: oklch(0.97 0.003 230);
/* Accents */
--muted: oklch(0.22 0.006 230);
--muted-foreground: oklch(0.55 0.01 230);
--accent: oklch(0.24 0.006 230);
--accent-foreground: oklch(0.97 0.003 230);
/* Functional */
--destructive: oklch(0.65 0.18 25);
--border: oklch(0.28 0.01 230);
--input: oklch(0.2 0.006 230);
--ring: oklch(0.6 0.01 230);
/* Sidebar */
--sidebar: oklch(0.12 0.005 230);
--sidebar-foreground: oklch(0.97 0.003 230);
--sidebar-primary: oklch(0.55 0.08 230);
--sidebar-primary-foreground: oklch(0.1 0 0);
--sidebar-accent: oklch(0.24 0.006 230);
--sidebar-accent-foreground: oklch(0.97 0.003 230);
--sidebar-border: oklch(0.28 0.01 230);
--sidebar-ring: oklch(0.6 0.01 230);
/* Popover */
--popover: oklch(0.18 0.006 230);
--popover-foreground: oklch(0.97 0.003 230);
/* Charts */
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
}
```
---
### Étape 3 : Corrigez le thème MIDNIGHT (remplacez lignes 169-188)
Cherchez `[data-theme='midnight'] {` (ligne 169)
Remplacez TOUTES les lignes 169-188 par ceci :
```css
[data-theme='midnight'] {
/* ============================================
THEME MIDNIGHT - VERSION SOMBRE DE SLATE
============================================ */
/* Light Mode */
--background: oklch(0.94 0.005 250); /* Gris-bleu très pâle */
--foreground: oklch(0.18 0.03 250); /* Gris-bleu très foncé */
--card: oklch(0.97 0.006 250); /* Gris-bleu pâle */
--card-foreground: oklch(0.18 0.03 250);
--popover: oklch(0.97 0.006 250);
--popover-foreground: oklch(0.18 0.03 250);
--primary: oklch(0.5 0.12 250); /* Gris-bleu saturé */
--primary-foreground: oklch(0.99 0 0); /* Blanc */
--secondary: oklch(0.2 0.01 250);
--secondary-foreground: oklch(0.18 0.03 250);
--muted: oklch(0.22 0.01 250);
--muted-foreground: oklch(0.55 0.02 250);
--accent: oklch(0.25 0.015 250);
--accent-foreground: oklch(0.18 0.03 250);
--destructive: oklch(0.6 0.22 25);
--border: oklch(0.85 0.015 250);
--input: oklch(0.25 0.01 250);
--ring: oklch(0.65 0.015 250);
--sidebar: oklch(0.9 0.01 250);
--sidebar-foreground: oklch(0.18 0.03 250);
--sidebar-primary: oklch(0.5 0.12 250);
--sidebar-primary-foreground: oklch(0.99 0 0);
--sidebar-accent: oklch(0.25 0.015 250);
--sidebar-accent-foreground: oklch(0.18 0.03 250);
--sidebar-border: oklch(0.85 0.015 250);
--sidebar-ring: oklch(0.65 0.015 250);
}
```
---
### Étape 4 : Corrigez le thème BLUE (remplacez lignes 190-217)
Cherchez `[data-theme='blue'] {` (ligne 190)
Remplacez TOUTES les lignes 190-217 par ceci :
```css
[data-theme='blue'] {
/* ============================================
THEME BLUE - VERSION SATURÉE DE SLATE
============================================ */
/* Light Mode */
--background: oklch(0.985 0.005 225); /* Blanc légèrement bleuté */
--foreground: oklch(0.18 0.035 225); /* Gris-bleu foncé saturé */
--card: oklch(0.98 0.01 225); /* Blanc légèrement bleuté */
--card-foreground: oklch(0.18 0.035 225);
--popover: oklch(0.98 0.01 225);
--popover-foreground: oklch(0.18 0.035 225);
--primary: oklch(0.5 0.15 225); /* Gris-bleu saturé vibrant */
--primary-foreground: oklch(0.99 0 0); /* Blanc */
--secondary: oklch(0.93 0.008 225);
--secondary-foreground: oklch(0.18 0.035 225);
--muted: oklch(0.9 0.01 225);
--muted-foreground: oklch(0.58 0.015 225);
--accent: oklch(0.93 0.01 225);
--accent-foreground: oklch(0.18 0.035 225);
--destructive: oklch(0.6 0.2 25);
--border: oklch(0.87 0.012 225);
--input: oklch(0.95 0.01 225);
--ring: oklch(0.65 0.015 225);
--sidebar: oklch(0.965 0.008 225);
--sidebar-foreground: oklch(0.18 0.035 225);
--sidebar-primary: oklch(0.5 0.15 225);
--sidebar-primary-foreground: oklch(0.99 0 0);
--sidebar-accent: oklch(0.93 0.01 225);
--sidebar-accent-foreground: oklch(0.18 0.035 225);
--sidebar-border: oklch(0.87 0.012 225);
--sidebar-ring: oklch(0.65 0.015 225);
}
```
---
### Étape 5 : Corrigez le thème SEPIA (remplacez lignes 219-238)
Cherchez `[data-theme='sepia'] {` (ligne 219)
Remplacez TOUTES les lignes 219-238 par ceci :
```css
[data-theme='sepia'] {
/* ============================================
THEME SEPIA - VERSION CHALEUREUSE DE SLATE
============================================ */
/* Light Mode */
--background: oklch(0.985 0.004 45); /* Blanc légèrement doré */
--foreground: oklch(0.2 0.015 45); /* Gris-brun foncé */
--card: oklch(0.98 0.01 45); /* Blanc légèrement doré */
--card-foreground: oklch(0.2 0.015 45);
--popover: oklch(0.98 0.01 45);
--popover-foreground: oklch(0.2 0.015 45);
--primary: oklch(0.45 0.08 45); /* Gris-brun chaud */
--primary-foreground: oklch(0.99 0 0); /* Blanc */
--secondary: oklch(0.94 0.008 45);
--secondary-foreground: oklch(0.2 0.015 45);
--muted: oklch(0.91 0.01 45);
--muted-foreground: oklch(0.6 0.012 45);
--accent: oklch(0.93 0.01 45);
--accent-foreground: oklch(0.2 0.015 45);
--destructive: oklch(0.6 0.2 25);
--border: oklch(0.88 0.012 45);
--input: oklch(0.97 0.008 45);
--ring: oklch(0.68 0.01 45);
--sidebar: oklch(0.96 0.01 45);
--sidebar-foreground: oklch(0.2 0.015 45);
--sidebar-primary: oklch(0.45 0.08 45);
--sidebar-primary-foreground: oklch(0.99 0 0);
--sidebar-accent: oklch(0.93 0.01 45);
--sidebar-accent-foreground: oklch(0.2 0.015 45);
--sidebar-border: oklch(0.88 0.012 45);
--sidebar-ring: oklch(0.68 0.01 45);
}
```
---
## APRÈS LES MODIFICATIONS
1. **Enregistrez** le fichier (`Ctrl+S`)
2. **Allez dans vos settings** (Settings Apparence)
3. **Changez le thème** de 'blue' vers :
- **'light'** pour le nouveau thème Slate principal
- OU 'midnight' pour tester le thème nuit
- OU 'blue' pour tester le thème bleu harmonisé
4. **Rafraîchissez** la page (`F5` ou redémarrez le serveur)
---
## 🎨 Ce que vous devriez voir :
### Avec thème 'light' (nouveau Slate) :
- Fond : blanc grisâtre
- Texte : gris-bleu foncé
- Boutons primary : **GRIS-BLEU DOX** (pas bleu saturé !)
- Plus professionnel, moins fatigant
### Avec thème 'midnight' :
- Fond : noir grisâtre
- Texte : blanc grisâtre
- Boutons : gris-bleu vibrant
- Nuit profonde
### Avec thème 'blue' :
- Fond : blanc légèrement bleuté
- Texte : gris-bleu foncé saturé
- Boutons : gris-bleu vibrant
- Version énergique
### Avec thème 'sepia' :
- Fond : blanc légèrement doré
- Texte : gris-brun
- Boutons : gris-brun
- Version chaleureuse
---
## 🔙 ANNULER si pas satisfait :
Si les couleurs ne vous plaisent pas :
```
git checkout -- keep-notes/app/globals.css
```
---
**Ramez, après avoir appliqué ces modifications, testez le thème 'light' pour voir le nouveau Slate !** 🎨🚀

View File

@@ -1,271 +0,0 @@
# 🧪 GUIDE SIMPLE : Comment Tester le Thème Slate
Ramez, voici les étapes SIMPLES pour tester le nouveau thème Gris-Bleu (Slate).
---
## 📋 RÉSUMÉ
**Ce que j'ai fait :** Créé des fichiers de PROPOSITIONS uniquement
**Ce que vous devez faire :** Implémenter (copier-coller) le code dans votre application
---
## 🎯 ÉTAPE 1 : Modifier le thème principal (5 minutes)
Ouvrez : `keep-notes/app/globals.css`
### ✏️ Remplacez les lignes 100-133 par ceci :
```css
:root {
--radius: 0.625rem;
/* === THEME SLATE (GRIS-BLEU) MODERNE === */
/* Backgrounds */
--background: oklch(0.985 0.003 230); /* Blanc grisâtre */
--card: oklch(1 0 0); /* Blanc pur */
--sidebar: oklch(0.97 0.004 230); /* Gris-bleu pâle */
--input: oklch(0.98 0.003 230); /* Gris-bleu pâle */
/* Textes */
--foreground: oklch(0.2 0.02 230); /* Gris-bleu foncé */
--card-foreground: oklch(0.2 0.02 230);
--foreground-secondary: oklch(0.45 0.015 230); /* Gris-bleu moyen */
--foreground-muted: oklch(0.6 0.01 230); /* Gris-bleu clair */
--popover-foreground: oklch(0.2 0.02 230);
/* Primary Actions */
--primary: oklch(0.45 0.08 230); /* Gris-bleu doux */
--primary-foreground: oklch(0.99 0 0); /* Blanc */
/* Secondary */
--secondary: oklch(0.94 0.005 230); /* Gris-bleu très pâle */
--secondary-foreground: oklch(0.2 0.02 230);
/* Accents */
--muted: oklch(0.92 0.005 230);
--muted-foreground: oklch(0.6 0.01 230);
--accent: oklch(0.94 0.005 230);
--accent-foreground: oklch(0.2 0.02 230);
/* Functional */
--destructive: oklch(0.6 0.18 25); /* Rouge */
--border: oklch(0.9 0.008 230); /* Gris-bleu très clair */
--input: oklch(0.98 0.003 230);
--ring: oklch(0.7 0.005 230);
/* Sidebar */
--sidebar-foreground: oklch(0.2 0.02 230);
--sidebar-primary: oklch(0.45 0.08 230);
--sidebar-primary-foreground: oklch(0.99 0 0);
--sidebar-accent: oklch(0.94 0.005 230);
--sidebar-accent-foreground: oklch(0.2 0.02 230);
--sidebar-border: oklch(0.9 0.008 230);
--sidebar-ring: oklch(0.7 0.005 230);
/* Popover */
--popover: oklch(1 0 0);
/* Charts (gardés) */
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
}
```
### ✏️ Remplacez les lignes 135-167 par ceci :
```css
.dark {
/* === THEME SLATE DARK MODE === */
/* Backgrounds */
--background: oklch(0.14 0.005 230); /* Noir grisâtre */
--card: oklch(0.18 0.006 230); /* Gris-bleu foncé */
--sidebar: oklch(0.12 0.005 230); /* Noir grisâtre */
--input: oklch(0.2 0.006 230);
/* Textes */
--foreground: oklch(0.97 0.003 230); /* Blanc grisâtre */
--card-foreground: oklch(0.97 0.003 230);
--foreground-secondary: oklch(0.75 0.008 230);
--foreground-muted: oklch(0.55 0.01 230);
--popover-foreground: oklch(0.97 0.003 230);
/* Primary Actions */
--primary: oklch(0.55 0.08 230); /* Gris-bleu plus clair */
--primary-foreground: oklch(0.1 0 0); /* Noir */
/* Secondary */
--secondary: oklch(0.24 0.006 230);
--secondary-foreground: oklch(0.97 0.003 230);
/* Accents */
--muted: oklch(0.22 0.006 230);
--muted-foreground: oklch(0.55 0.01 230);
--accent: oklch(0.24 0.006 230);
--accent-foreground: oklch(0.97 0.003 230);
/* Functional */
--destructive: oklch(0.65 0.18 25);
--border: oklch(0.28 0.01 230);
--input: oklch(0.2 0.006 230);
--ring: oklch(0.6 0.01 230);
/* Sidebar */
--sidebar-foreground: oklch(0.97 0.003 230);
--sidebar-primary: oklch(0.55 0.08 230);
--sidebar-primary-foreground: oklch(0.1 0 0);
--sidebar-accent: oklch(0.24 0.006 230);
--sidebar-accent-foreground: oklch(0.97 0.003 230);
--sidebar-border: oklch(0.28 0.01 230);
--sidebar-ring: oklch(0.6 0.01 230);
/* Popover */
--popover: oklch(0.18 0.006 230);
--popover-foreground: oklch(0.97 0.003 230);
/* Charts (gardés mais ajustés pour dark) */
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
}
```
### ✅ Enregistrez le fichier
`Ctrl+S` ou `Cmd+S`
---
## 🧪 ÉTAPE 2 : Vérifiez le résultat (immédiat !)
### Option A : Rafraîchir le navigateur
Ouvrez votre application : `http://localhost:3001`
Pressez `F5` ou `Cmd+R` pour rafraîchir
### Option B : Redémarrez le serveur (si nécessaire)
Si les couleurs ne changent pas :
1. Allez dans le terminal où tourne votre serveur
2. Pressez `Ctrl+C` pour arrêter
3. Relancez : `npm run dev`
---
## 🎯 ÉTAPE 3 : Testez le thème
### Ce que vous devriez voir :
**En mode light :**
- Fond légèrement grisâtre (pas blanc pur)
- Texte gris-bleu foncé (pas noir pur)
- Boutons primary en gris-bleu doux (pas bleu saturé)
- Bordures gris-bleu très clair
**En mode dark :**
- Fond noir grisâtre (pas noir pur)
- Texte blanc grisâtre (pas blanc pur)
- Boutons primary en gris-bleu plus clair
- Bordures gris-bleu foncé
### Testez les modes :
- Basculez entre light et dark avec votre switcher
- Observez les changements
- Notez si vous aimez ou non
---
## ❓ RAPPEL : Pourquoi Slate ?
### ✅ Avantages que vous verrez :
1. **Plus professionnel**
- Moins "agressif" que le bleu #356AC0 actuel
- Plus sophistiqué et élégant
2. **Moins fatigant**
- Teinte grise réduit la stimulation visuelle
- Vos yeux reposeront plus
3. **Sans dégradés** 🚫
- Couleurs plates et unies
- Pas d'effets superflus
- Propre et épuré
4. **Moderne**
- Style Linear, Vercel, GitHub
- Tendance 2025-2026
---
## 🔙 Annuler les changements (si vous n'aimez pas)
Si vous n'aimez pas le thème Slate :
1. Ouvrez le terminal Git
2. Tapez : `git checkout -- keep-notes/app/globals.css`
3. Rafraîchissez le navigateur
4. Le thème actuel revient !
Ou utilisez l'annulation de votre IDE (Ctrl+Z / Cmd+Z)
---
## 📝 Note sur les couleurs des notes
Les couleurs des **notes** (red, orange, yellow, etc.) ne changeront PAS avec ces modifications. C'est normal !
Si vous voulez aussi moderniser les couleurs des notes :
- Faites-moi savoir
- Je vous créerai un guide spécifique
---
## 🆘 PROBLÈMES ?
### Le thème ne s'applique pas :
1. Vérifiez que vous avez bien **enregistré** le fichier (`Ctrl+S`)
2. Videz le cache du navigateur (`Ctrl+Shift+R`)
3. Redémarrez le serveur de dev
### Les couleurs sont identiques :
1. Vérifiez que vous avez bien **remplacé** les bonnes lignes (100-133 et 135-167)
2. Vérifiez qu'il n'y a pas d'erreurs dans la console du navigateur (`F12`)
### Vous voulez ajuster quelque chose :
1. Dites-moi quoi (ex: "plus clair", "plus foncé", "autre teinte")
2. Je vous ajusterai les valeurs OKLCH
3. On re-teste !
---
## ✅ CHECKLIST AVANT DE TESTER
- [ ] J'ai ouvert `keep-notes/app/globals.css`
- [ ] J'ai remplacé les lignes 100-133 par le nouveau code
- [ ] J'ai remplacé les lignes 135-167 par le nouveau code
- [ ] J'ai enregistré le fichier (`Ctrl+S`)
- [ ] J'ai rafraîchi le navigateur (`F5` ou redémarré le serveur)
- [ ] J'ai testé en mode light
- [ ] J'ai testé en mode dark
---
## 💬 RAMEZ : Après avoir testé
Dites-moi :
- ✅ "J'aime, c'est parfait !" → On peut l'adopter définitivement
- 🔶 "C'est bien, mais..." → Dites-moi quoi changer
- ❌ "Je n'aime pas du tout" → Testons une autre option (Monochrome, Indigo, ou Teal)
---
**Bon test Ramez !** 🧪
*Guide créé par Amelia - Developer Agent* 💻

View File

@@ -1,332 +0,0 @@
# 🎨 Proposition d'Harmonie de Couleurs pour Memento
Ramez, voici mon analyse et proposition détaillée pour améliorer l'harmonie des couleurs de votre application.
---
## 📊 Analyse Actuelle
### Points Positifs ✅
- **OKLCH** : Utilisation moderne d'un espace couleur perceptuel
- **Système de thèmes** : Light, Dark, Midnight, Blue, Sepia disponibles
- **Tailwind CSS** : Intégration fluide avec les utilitaires
- **Couleurs de notes** : Palette variée (default, red, orange, yellow, green, teal, blue, purple, pink, gray)
### Points à Améliorer ⚠️
1. **Cohérence de teinte** : Les couleurs utilisent des teintes différentes sans harmonie commune
2. **Contraste texte** : Le texte par défaut `oklch(0.145 0 0)` est un peu sombre en mode light
3. **Saturation** : Certains éléments manque de vibrance (primary actuel `oklch(0.205 0 0)` est gris)
4. **Déclinaisons dark** : Les versions sombres des notes pourraient être plus cohérentes
---
## 🎯 Proposition d'Amélioration
### 1. Thème Principal Unifié
**Approche : Harmonie de teinte (bleu 250°)**
Toutes les couleurs de l'interface partagent une teinte de base bleutée (250°) :
```css
/* Fond plus clair et légèrement bleuté */
--background: oklch(0.99 0.002 250);
/* Texte plus sombre pour meilleur contraste */
--foreground: oklch(0.18 0.01 250);
/* Primary bleu Keep vibrant */
--primary: oklch(0.55 0.2 250);
--primary-hover: oklch(0.5 0.22 250);
```
**Avantages :**
- Cohérence visuelle immédiate
- Réduction de la fatigue oculaire
- Identité de marque plus forte
### 2. Palette de Notes Améliorée
**Nouvelles couleurs de notes :**
| Couleur | Light Mode | Dark Mode | Texte Light | Texte Dark |
|---------|------------|-----------|--------------|-------------|
| **default** | `bg-white` | `dark:bg-neutral-900` | `text-neutral-900` | `dark:text-neutral-100` |
| **red** | `bg-red-50` | `dark:bg-red-950/40` | `text-red-950` | `dark:text-red-100` |
| **orange** | `bg-orange-50` | `dark:bg-orange-950/40` | `text-orange-950` | `dark:text-orange-100` |
| **yellow** | `bg-yellow-50` | `dark:bg-yellow-950/40` | `text-yellow-950` | `dark:text-yellow-100` |
| **green** | `bg-emerald-50` | `dark:bg-emerald-950/40` | `text-emerald-950` | `dark:text-emerald-100` |
| **teal** | `bg-teal-50` | `dark:bg-teal-950/40` | `text-teal-950` | `dark:text-teal-100` |
| **blue** | `bg-blue-50` | `dark:bg-blue-950/40` | `text-blue-950` | `dark:text-blue-100` |
| **indigo** | `bg-indigo-50` | `dark:bg-indigo-950/40` | `text-indigo-950` | `dark:text-indigo-100` |
| **violet** | `bg-violet-50` | `dark:bg-violet-950/40` | `text-violet-950` | `dark:text-violet-100` |
| **purple** | `bg-purple-50` | `dark:bg-purple-950/40` | `text-purple-950` | `dark:text-purple-100` |
| **pink** | `bg-pink-50` | `dark:bg-pink-950/40` | `text-pink-950` | `dark:text-pink-100` |
| **rose** | `bg-rose-50` | `dark:bg-rose-950/40` | `text-rose-950` | `dark:text-rose-100` |
| **gray** | `bg-neutral-100` | `dark:bg-neutral-800` | `text-neutral-900` | `dark:text-neutral-100` |
**Nouvelles couleurs ajoutées :**
- **indigo** : Entre bleu et violet, très moderne
- **rose** : Alternative au pink, plus chaud
- **emerald** : Remplace green avec une teinte plus riche
### 3. Accessibilité WCAG AA+
Tous les contrastes respectent ou dépassent 4.5:1 (WCAG AA)
**Exemples de contraste :**
- Texte sur fond blanc : `15.5:1` (Excellent)
- Texte sur note bleue claire : `7.2:1` (Excellent)
- Texte sur note jaune : `6.8:1` (Excellent)
- Primary sur fond : `4.8:1` (AA+)
---
## 🔧 Implementation Technique
### Fichiers à modifier
1. **`keep-notes/app/globals.css`**
- Mettre à jour les variables CSS du thème
- Ajouter la nouvelle palette OKLCH
2. **`keep-notes/lib/types.ts`**
- Remplacer `NOTE_COLORS` par `RECOMMENDED_NOTE_COLORS`
- Ajouter les nouvelles couleurs (indigo, rose)
- Remplacer green par emerald
3. **Composants utilisant les couleurs :**
- `note-card.tsx`
- `note-input.tsx`
- `note-editor.tsx`
- `note-actions.tsx`
- `notebooks-list.tsx`
### Exemple de migration
```typescript
// AVANT (keep-notes/lib/types.ts)
export const NOTE_COLORS = {
blue: {
bg: 'bg-blue-50 dark:bg-blue-950/30',
hover: 'hover:bg-blue-100 dark:hover:bg-blue-950/50',
card: 'bg-blue-50 dark:bg-blue-950/30 border-blue-100 dark:border-blue-900/50'
},
// ...
};
// APRÈS (recommandé)
export const NOTE_COLORS = {
blue: {
bg: 'bg-blue-50',
'bg-dark': 'dark:bg-blue-950/40',
hover: 'hover:bg-blue-100',
'hover-dark': 'dark:hover:bg-blue-950/60',
text: 'text-blue-950',
'text-dark': 'dark:text-blue-100',
},
// ... avec indigo, rose, emerald ajoutés
};
```
---
## 📈 Comparaison Avant/Après
### Avant
```tsx
<div className={`
${colorClasses.bg}
${colorClasses.card}
${colorClasses.hover}
`}>
<p className="text-foreground">{content}</p>
</div>
```
**Problèmes :**
- Le texte ne s'adapte pas toujours à la couleur de la note
- Les bordures sont hardcoded dans chaque couleur
- Manque de flexibilité
### Après
```tsx
<div className={`
${noteColors.bg}
dark:${noteColors['bg-dark']}
${noteColors.border}
dark:${noteColors['border-dark']}
${noteColors.text}
dark:${noteColors['text-dark']}
hover:${noteColors.hover}
dark:hover:${noteColors['hover-dark']}
`}>
<p>{content}</p>
</div>
```
**Avantages :**
- Texte automatiquement adapté à la couleur de fond
- Gestion centralisée des bordures
- Support dark mode par défaut
- Extensible facilement
---
## 🎨 Visualisation des Palettes
### Thème Light Mode
```
┌─────────────────────────────────────────┐
│ Background ████████ #FFFFFF │
│ Card ████████ #FFFFFF │
│ Sidebar ████████ #F5F6F8 │
│ Primary ████████ #356AC0 │ ← Bleu Keep
│ Text ████████ #2D3748 │
└─────────────────────────────────────────┘
```
### Thème Dark Mode
```
┌─────────────────────────────────────────┐
│ Background ████████ #1A202C │
│ Card ████████ #2D3748 │
│ Sidebar ████████ #171923 │
│ Primary ████████ #4A7FD4 │
│ Text ████████ #F7FAFC │
└─────────────────────────────────────────┘
```
### Palette de Notes (mode light)
```
┌─────────────────────────────────────────────────────────────┐
│ Default ████████████ White │
│ Red ████████████ #FEF2F2 (red-50) │
│ Orange ████████████ #FFF7ED (orange-50) │
│ Yellow ████████████ #FEFCE8 (yellow-50) │
│ Green ████████████ #ECFDF5 (emerald-50) │
│ Teal ████████████ #F0FDFA (teal-50) │
│ Blue ████████████ #EFF6FF (blue-50) │
│ Indigo ████████████ #EEF2FF (indigo-50) │
│ Violet ████████████ #F5F3FF (violet-50) │
│ Purple ████████████ #FAF5FF (purple-50) │
│ Pink ████████████ #FDF2F8 (pink-50) │
│ Rose ████████████ #FFF1F2 (rose-50) │
│ Gray ████████████ #F5F5F4 (neutral-100) │
└─────────────────────────────────────────────────────────────┘
```
---
## 🚀 Avantages de la Proposition
### 1. Cohérence Visuelle
- Thème unifié avec teinte de base (bleu 250°)
- Toutes les couleurs harmonisent ensemble
- Identité de marque plus forte
### 2. Meilleure Accessibilité
- Respect WCAG AA+ (contraste ≥ 4.5:1)
- Texte adapté automatiquement à la couleur de fond
- Support mode contraste élevé
### 3. Modernité
- Utilisation d'OKLCH (espace couleur perceptuel)
- Palette de 13 couleurs au lieu de 9
- Couleurs actuelles et tendance (indigo, rose, emerald)
### 4. Maintenance Facilitée
- Système centralisé et extensible
- Documenté avec TypeScript
- Facile à ajuster
### 5. Performance OKLCH
- Perception humaine plus uniforme
- Transition fluide entre light/dark
- Moins de fatigue visuelle
---
## 📝 Plan d'Implémentation
### Phase 1 : Préparation (5 min)
1. ✅ Créer le fichier `color-harmony-recommendation.ts`
2. ✅ Documenter la proposition dans ce fichier
### Phase 2 : Migration (30 min)
1. Mettre à jour `keep-notes/app/globals.css`
2. Mettre à jour `keep-notes/lib/types.ts`
3. Mettre à jour les composants concernés
### Phase 3 : Tests (15 min)
1. Tester en mode light
2. Tester en mode dark
3. Tester chaque couleur de note
4. Vérifier les contrastes WCAG
### Phase 4 : Ajustements (optionnel)
1. Affiner selon vos préférences
2. Ajouter d'autres couleurs si nécessaire
3. Ajuster les saturations si trop/peu vibrant
---
## 💡 Recommandations Personnalisées
Ramez, voici mes recommandations spécifiques pour votre cas :
### Immédiat
**Adopter la teinte unifiée (bleu 250°)** pour le thème principal
**Remplacer green par emerald** pour une teinte plus riche
**Ajouter indigo et rose** pour plus de variété
### À terme
🔶 Créer un "theme builder" pour que les utilisateurs puissent personnaliser
🔶 Ajouter des présets de couleurs saisonnières (automne, hiver, printemps, été)
🔶 Implémenter des animations de transition de couleur plus fluides
---
## 📦 Ressources Créées
J'ai créé les fichiers suivants pour vous aider :
1. **`keep-notes/lib/color-harmony-recommendation.ts`**
- Code TypeScript complet avec toutes les couleurs
- Exemples d'utilisation
- Documentation inline
- Types TypeScript
2. **`keep-notes/PROPOSITION-COULEURS.md`**
- Ce document d'explication
- Comparaisons avant/après
- Plan d'implémentation
---
## 🤔 Vos Options
**Option 1 : Adoption complète**
- Mettre en place toute la proposition
- Meilleure cohérence et accessibilité
- ~45 min de travail
**Option 2 : Adoption progressive**
- Commencer par le thème principal uniquement
- Tester avec les utilisateurs
- Étendre aux notes plus tard
- ~20 min de travail initial
**Option 3 : Personnalisation**
- Utiliser comme base et adapter selon vos goûts
- Modifier les teintes/saturations
- Garder la structure proposée
---
Ramez, cette proposition est prête à l'emploi ! 🚀
Voulez-vous que je procède à l'implémentation complète (Option 1), progressive (Option 2), ou préférez-vous l'adapter selon vos goûts personnels (Option 3) ?
---
*Analyse et proposition créées par Amelia - Developer Agent* 💻

View File

@@ -1,242 +0,0 @@
# 🎨 Proposition : Gris-Bleu (Slate) Moderne
Ramez, excellente idée ! Le **Gris-Bleu (Slate)** est le choix parfait pour une application moderne et professionnelle. C'est élégant, discret et ne fatigue absolument pas les yeux.
---
## 🎯 Pourquoi le Slate (Gris-Bleu) ?
### ✅ Avantages majeurs
1. **Ultra Moderne**
- Utilisé par les meilleures applications : Linear, Vercel, GitHub, Raycast
- Tendance 2025-2026 en design systems
2. **Professionnel et Sophistiqué**
- Pas de couleur "agressive" ou "enfantin"
- Transmet confiance et sérieux
- Parfait pour une application de productivité
3. **Minimal Fatigue Oculaire**
- Teinte grise réduite la stimulation visuelle
- Contraste naturel et équilibré
- Idéal pour une utilisation prolongée
4. **Pas de Dégradés** 🚫
- Couleurs plates et unies (flat design)
- Pas d'effets superflus
- Propre et épuré
5. **Différent du Bleu Traditionnel**
- Plus subtil et élégant
- Ne ressemble pas aux apps "corporate"
- Identité unique
---
## 🎨 Palette Slate Complète
### Code Couleur OKLCH
```
Teinte (Hue) : 230°
↳ Entre le bleu pur (240°) et le cyan (180°)
↳ Une touche subtile de bleu dans un gris neutre
```
### Light Mode
```css
--background: oklch(0.985 0.003 230); /* Blanc grisâtre */
--card: oklch(1 0 0); /* Blanc pur */
--sidebar: oklch(0.97 0.004 230); /* Gris-bleu pâle */
--foreground: oklch(0.2 0.02 230); /* Gris-bleu foncé */
--primary: oklch(0.45 0.08 230); /* Gris-bleu doux */
--border: oklch(0.9 0.008 230); /* Gris-bleu très clair */
```
### Dark Mode
```css
--background: oklch(0.14 0.005 230); /* Noir grisâtre */
--card: oklch(0.18 0.006 230); /* Gris-bleu foncé */
--sidebar: oklch(0.12 0.005 230); /* Noir grisâtre */
--foreground: oklch(0.97 0.003 230); /* Blanc grisâtre */
--primary: oklch(0.55 0.08 230); /* Gris-bleu plus clair */
--border: oklch(0.28 0.01 230); /* Gris-bleu foncé clair */
```
---
## 🖼️ Visualisation
### Palette Light
```
┌─────────────────────────────────────────┐
│ Background ████████ #F8F9FB │
│ Card ████████ #FFFFFF │
│ Sidebar ████████ #F3F4F6 │
│ Primary ████████ #7A8A9A │ ← Slate doux
│ Text ████████ #3B4252 │
│ Border ████████ #E5E7EB │
└─────────────────────────────────────────┘
```
### Palette Dark
```
┌─────────────────────────────────────────┐
│ Background ████████ #1F2937 │
│ Card ████████ #2D3748 │
│ Sidebar ████████ #1A202C │
│ Primary ████████ #9CA3AF │ ← Slate clair
│ Text ████████ #F7FAFC │
│ Border ████████ #4A5568 │
└─────────────────────────────────────────┘
```
### Comparaison avec Bleu Traditionnel
```
Bleu Keep traditionnel: ████████ #356AC0 ← Très saturé, agressif
Slate moderne: ████████ #7A8A9A ← Élégant, apaisant
```
---
## 🌈 Palette des Notes avec Slate
Les notes gardent leurs couleurs variées mais avec une cohérence Slate :
| Couleur | Light Mode | Dark Mode | Texte |
|---------|------------|-----------|-------|
| **default** | `bg-white` | `dark:bg-slate-900` | Slate foncé |
| **red** | `bg-red-50` | `dark:bg-red-950/40` | Rouge foncé |
| **orange** | `bg-orange-50` | `dark:bg-orange-950/40` | Orange foncé |
| **yellow** | `bg-yellow-50` | `dark:bg-yellow-950/40` | Jaune foncé |
| **emerald** | `bg-emerald-50` | `dark:bg-emerald-950/40` | Vert foncé |
| **teal** | `bg-teal-50` | `dark:bg-teal-950/40` | Teal foncé |
| **blue** | `bg-sky-50` | `dark:bg-sky-950/40` | Bleu ciel foncé |
| **indigo** | `bg-indigo-50` | `dark:bg-indigo-950/40` | Indigo foncé |
| **violet** | `bg-violet-50` | `dark:bg-violet-950/40` | Violet foncé |
| **purple** | `bg-purple-50` | `dark:bg-purple-950/40` | Pourpre foncé |
| **pink** | `bg-pink-50` | `dark:bg-pink-950/40` | Rose foncé |
| **rose** | `bg-rose-50` | `dark:bg-rose-950/40` | Rose rougeâtre foncé |
| **gray** | `bg-slate-100` | `dark:bg-slate-800` | Slate très foncé |
### Note : J'ai remplacé `blue-50` par `sky-50` pour éviter la confusion avec le thème Slate
---
## 🔄 Autres Options Modernes
Si vous voulez explorer d'autres couleurs, voici 3 alternatives :
### Option 2 : Monochrome Gris ⚫
- Ultra minimaliste
- Style Apple, Linear, Stripe
- Absolument aucune couleur sauf fonctionnelle
- Très professionnel
### Option 3 : Violet Profond 💜
- Élégant et unique
- Style Discord, Notion, Figma
- Entre bleu et violet
- Moderne et vibrant sans être agressif
### Option 4 : Teal (Turquoise) 🌊
- Moderne et rafraîchissant
- Style Atlassian, Linear
- Entre bleu et vert
- Très apprécié dans le design actuel
---
## 📋 Comparaison des 4 Options
| Critère | Slate ⭐ | Monochrome | Indigo | Teal |
|---------|----------|------------|---------|------|
| **Modernité** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| **Professionnalisme** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| **Fatigue oculaire** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| **Unicité** | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| **Tendance 2025** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| **Popularité** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
**Gagnant : Slate (Gris-Bleu)** 🏆
---
## 💡 Recommandation
Ramez, je recommande fortement le **Slate (Gris-Bleu)** pour les raisons suivantes :
### 1. Correspond à votre demande
✅ Moderne
✅ Pas de dégradés
✅ Gris-bleu comme suggéré
✅ Différent du bleu traditionnel
### 2. Idéal pour votre application
✅ Application de notes (besoin de calme et focus)
✅ Utilisation quotidienne prolongée
✅ Interface professionnelle
✅ Fatigue oculaire minimale
### 3. Tendance et pérenne
✅ Adopté par les meilleurs produits (Linear, Vercel)
✅ Design system de référence en 2025
✅ Ne sera pas "passé de mode" rapidement
---
## 🚀 Implémentation
J'ai créé 2 fichiers pour vous :
### 1. `keep-notes/lib/modern-color-options.ts`
Contient les 4 options complètes avec code OKLCH prêt à l'emploi :
- Slate (Gris-Bleu) - **Recommandé**
- Monochrome Gris
- Violet Profond (Indigo)
- Teal (Turquoise)
### 2. Ce document `PROPOSITION-SLATE-MODERNE.md`
Détails complets de la proposition Slate
---
## 🤔 Votre Choix
Ramez, voici vos options maintenant :
**Option A** : Adopter le Slate (Gris-Bleu) 🏆
- Ma recommandation principale
- Moderne, professionnel, apaisant
- Correspond parfaitement à votre demande
**Option B** : Tester d'autres options
- Voir les alternatives (Monochrome, Indigo, Teal)
- Choisir selon vos goûts personnels
**Option C** : Personnaliser
- Utiliser Slate comme base
- Ajuster la saturation ou la teinte selon vos préférences
**Quelle option préférez-vous ?** 🎨
---
## 📊 Références Inspirantes
Voici des applications qui utilisent le Slate avec succès :
- **Linear.app** - Design moderne par excellence
- **Vercel.com** - Professionalisme et élégance
- **GitHub.com** - Interface propre et lisible
- **Raycast.com** - Minimaliste et efficace
- **Stripe.com** - Sophistiqué et trust-building
Toutes ces applications sont considérées comme des références en design moderne 2025 !
---
*Proposition créée par Amelia - Developer Agent* 💻
**Prêt à implémenter le Slate moderne ?** 🚀

View File

@@ -1,104 +1,69 @@
# Memento - Google Keep Clone
# Keep Notes ✨
A beautiful and feature-rich Google Keep clone built with modern web technologies.
Keep Notes est une application avancée de prise de notes hybride, combinant la fluidité d'un outil local moderne avec la puissance de l'Intelligence Artificielle. Conçue pour offrir des performances maximales, elle utilise les dernières avancées de l'écosystème React et Next.js.
![Memento](https://img.shields.io/badge/Next.js-16-black)
![TypeScript](https://img.shields.io/badge/TypeScript-5.0-blue)
![Tailwind CSS](https://img.shields.io/badge/Tailwind-4.0-38bdf8)
![Prisma](https://img.shields.io/badge/Prisma-7.0-2d3748)
## 🚀 Fonctionnalités
## ✨ Features
- **Notes & Carnets** : Organisez vos idées rapidement avec des dossiers, codes couleurs, et épinglage.
- **Support Markdown & Rendu Riche** : Éditez ou affichez vos notes instantanément.
- **Disposition Masonry** : Grille CSS ultra-rapide (0 JavaScript) avec drag & drop fluide via `@dnd-kit`.
- **Intégration de l'Intelligence Artificielle** :
- **Memory Echo** : Suggestion automatique et connexions entre notes similaires (RAG / Embeddings).
- **Auto-Tagging** : Création automatique d'étiquettes pertinentes.
- **Organisation par lots** (Batch Organization) : Tri automatique des notes en vrac.
- **Amélioration textuelle** : Reformulation, synthèse, ou traduction propulsés par l'IA.
- **Haute Performance (RSC & Turbopack)** : Rendu Server Components natif pour une hydratation sans délai et développement accéléré via Turbopack.
- 📝 **Create & Edit Notes**: Quick note creation with expandable input
- ☑️ **Checklist Support**: Create todo lists with checkable items
- 🎨 **Color Customization**: 10 beautiful color themes for organizing notes
- 📌 **Pin Notes**: Keep important notes at the top
- 📦 **Archive**: Archive notes you want to keep but don't need to see
- 🏷️ **Labels**: Organize notes with custom labels
- 🔍 **Real-time Search**: Instantly search through all your notes
- 🌓 **Dark Mode**: Beautiful dark theme with system preference detection
- 📱 **Fully Responsive**: Works perfectly on desktop, tablet, and mobile
-**Server Actions**: Lightning-fast CRUD operations with Next.js 16
- 🎯 **Type-Safe**: Full TypeScript support throughout
## 📄 Licence et Droits d'Auteur
## 🚀 Tech Stack
### **Licence Utilisateur Final (Version actuelle - Personnelle & Non-Commerciale)**
Ce code source est fourni **strictement pour un usage personnel et éducatif**.
- **Utilisation non-commerciale uniquement** : Il est interdit d'utiliser ce projet (ou tout code dérivé) pour générer des revenus, construire un produit commercial ou l'intégrer dans un service monétisé.
- **Redistribution sous condition** : Vous ne pouvez pas redistribuer ou publier cette version sans maintenir cette licence restrictive.
### Frontend
- **Next.js 16** - React framework with App Router
- **TypeScript** - Type safety and better DX
- **Tailwind CSS 4** - Utility-first CSS framework
- **shadcn/ui** - Beautiful, accessible UI components
- **Lucide React** - Modern icon library
*(Inspiré de Creative Commons Attribution-NonCommercial 4.0 International - CC BY-NC 4.0).*
### Backend
- **Next.js Server Actions** - Server-side mutations
- **Prisma ORM** - Type-safe database client
- **SQLite** - Lightweight database (easily switchable to PostgreSQL)
---
## 📦 Installation
## 🗺️ Roadmap & Version SaaS Commerciale Publique
### Prerequisites
- Node.js 18+
- npm or yarn
Une version complète de **Keep Notes** destinée au grand public est prévue et en cours de planification. Cette version cloud s'appuiera sur de toutes nouvelles optimisations d'infrastructure :
### Steps
1. **Migration Base de Données** :
- Remplacement de SQLite local par **PostgreSQL** afin de supporter l'architecture multi-tenant (plusieurs utilisateurs avec sécurité accrue des données).
2. **Système de Monétisation (Features IA)** :
- Mise en place d'un modèle d'abonnement SaaS (Stripe).
- Intégration d'un système de crédit ("AI Credits") pour réguler l'usage des API d'intelligence artificielle (LLMs, Embeddings) de façon soutenable.
3. **Optimisations Scalabilité** :
- Déploiement distribué.
1. **Clone the repository**
```bash
git clone <repository-url>
cd keep-notes
```
---
2. **Install dependencies**
## 🛠️ Stack Technique
- **Framework** : Next.js 15 (App Router, Server Components)
- **Frontend** : React 19, Tailwind CSS, Radix UI primitives
- **Drag & Drop** : `@dnd-kit/core` & `sortable`
- **Base de Données** : Prisma ORM, SQLite en env de développement (bientôt PostgreSQL)
- **Outillage** : Turbopack, TypeScript
## 💻 Instructions de Développement
### Installation
```bash
npm install
# ou
yarn install
```
3. **Set up the database**
### Initialisation de la Base de données
```bash
npx prisma generate
npx prisma migrate dev
npx prisma db push
```
4. **Start the development server**
### Lancement du serveur (avec Turbopack)
```bash
npm run dev
```
5. **Open your browser**
Navigate to [http://localhost:3000](http://localhost:3000)
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
Ouvrez [http://localhost:3000](http://localhost:3000) dans votre navigateur.

View File

@@ -1,361 +0,0 @@
# 🎨 Thèmes Harmonisés avec Slate
Ramez, voici les thèmes **midnight**, **blue** et **sepia** harmonisés avec le thème **Slate** principal.
---
## 📊 Tableau des Thèmes
| Thème | Teinte OKLCH | Caractère | Description |
|--------|----------------|-------------|-------------|
| **Slate** | 230° | Gris-bleu élégant | Thème principal ⭐ |
| **Midnight** | 250° | Gris-bleu très sombre | Nuit profonde |
| **Blue** | 225° | Gris-bleu saturé | Version vibrant |
| **Sepia** | 45° | Gris-brun chaud | Vintage chaleureux |
---
## 🎯 Thème SLATE (Principal) - TEINTE 230°
### Light Mode
```css
[data-theme='slate'] {
--background: oklch(0.985 0.003 230); /* Blanc grisâtre */
--foreground: oklch(0.2 0.02 230); /* Gris-bleu foncé */
--card: oklch(1 0 0); /* Blanc pur */
--card-foreground: oklch(0.2 0.02 230);
--primary: oklch(0.45 0.08 230); /* Gris-bleu doux */
--primary-foreground: oklch(0.99 0 0); /* Blanc */
--secondary: oklch(0.94 0.005 230); /* Gris-bleu très pâle */
--secondary-foreground: oklch(0.2 0.02 230);
--muted: oklch(0.92 0.005 230);
--muted-foreground: oklch(0.6 0.01 230);
--accent: oklch(0.94 0.005 230);
--accent-foreground: oklch(0.2 0.02 230);
--destructive: oklch(0.6 0.18 25); /* Rouge */
--border: oklch(0.9 0.008 230); /* Gris-bleu très clair */
--input: oklch(0.98 0.003 230);
--ring: oklch(0.7 0.005 230);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.2 0.02 230);
--sidebar: oklch(0.97 0.004 230);
--sidebar-foreground: oklch(0.2 0.02 230);
--sidebar-primary: oklch(0.45 0.08 230);
--sidebar-primary-foreground: oklch(0.99 0 0);
--sidebar-accent: oklch(0.94 0.005 230);
--sidebar-accent-foreground: oklch(0.2 0.02 230);
--sidebar-border: oklch(0.9 0.008 230);
--sidebar-ring: oklch(0.7 0.005 230);
}
```
### Dark Mode
```css
[data-theme='slate'].dark {
--background: oklch(0.14 0.005 230); /* Noir grisâtre */
--foreground: oklch(0.97 0.003 230); /* Blanc grisâtre */
--card: oklch(0.18 0.006 230); /* Gris-bleu foncé */
--card-foreground: oklch(0.97 0.003 230);
--primary: oklch(0.55 0.08 230); /* Gris-bleu plus clair */
--primary-foreground: oklch(0.1 0 0); /* Noir */
--secondary: oklch(0.24 0.006 230);
--secondary-foreground: oklch(0.97 0.003 230);
--muted: oklch(0.22 0.006 230);
--muted-foreground: oklch(0.55 0.01 230);
--accent: oklch(0.24 0.006 230);
--accent-foreground: oklch(0.97 0.003 230);
--destructive: oklch(0.65 0.18 25);
--border: oklch(0.28 0.01 230);
--input: oklch(0.2 0.006 230);
--ring: oklch(0.6 0.01 230);
--popover: oklch(0.18 0.006 230);
--popover-foreground: oklch(0.97 0.003 230);
--sidebar: oklch(0.12 0.005 230);
--sidebar-foreground: oklch(0.97 0.003 230);
--sidebar-primary: oklch(0.55 0.08 230);
--sidebar-primary-foreground: oklch(0.1 0 0);
--sidebar-accent: oklch(0.24 0.006 230);
--sidebar-accent-foreground: oklch(0.97 0.003 230);
--sidebar-border: oklch(0.28 0.01 230);
--sidebar-ring: oklch(0.6 0.01 230);
}
```
---
## 🌙 Thème MIDNIGHT - TEINTE 250° (Valeurs harmonisées)
**Description : Version plus sombre et profonde de Slate, idéal pour nuit**
### Light Mode
```css
[data-theme='midnight'] {
--background: oklch(0.94 0.005 250); /* Gris-bleu très pâle */
--foreground: oklch(0.18 0.03 250); /* Gris-bleu très foncé */
--card: oklch(0.97 0.006 250); /* Gris-bleu pâle */
--card-foreground: oklch(0.18 0.03 250);
--primary: oklch(0.5 0.12 250); /* Gris-bleu saturé */
--primary-foreground: oklch(0.99 0 0); /* Blanc */
--secondary: oklch(0.2 0.01 250);
--secondary-foreground: oklch(0.18 0.03 250);
--muted: oklch(0.22 0.01 250);
--muted-foreground: oklch(0.55 0.02 250);
--accent: oklch(0.25 0.015 250);
--accent-foreground: oklch(0.18 0.03 250);
--destructive: oklch(0.6 0.22 25);
--border: oklch(0.85 0.015 250);
--input: oklch(0.25 0.01 250);
--ring: oklch(0.65 0.015 250);
--popover: oklch(0.97 0.006 250);
--popover-foreground: oklch(0.18 0.03 250);
--sidebar: oklch(0.9 0.01 250);
--sidebar-foreground: oklch(0.18 0.03 250);
--sidebar-primary: oklch(0.5 0.12 250);
--sidebar-primary-foreground: oklch(0.99 0 0);
--sidebar-accent: oklch(0.25 0.015 250);
--sidebar-accent-foreground: oklch(0.18 0.03 250);
--sidebar-border: oklch(0.85 0.015 250);
--sidebar-ring: oklch(0.65 0.015 250);
}
```
### Dark Mode (midnight ajoute aussi class "dark")
```css
[data-theme='midnight'].dark {
--background: oklch(0.1 0.01 250); /* Noir profond */
--foreground: oklch(0.96 0.005 250); /* Blanc grisâtre */
--card: oklch(0.15 0.015 250); /* Gris-bleu très foncé */
--card-foreground: oklch(0.96 0.005 250);
--primary: oklch(0.6 0.12 250); /* Gris-bleu vibrant */
--primary-foreground: oklch(0.1 0 0); /* Noir */
--secondary: oklch(0.18 0.015 250);
--secondary-foreground: oklch(0.96 0.005 250);
--muted: oklch(0.2 0.015 250);
--muted-foreground: oklch(0.5 0.02 250);
--accent: oklch(0.22 0.02 250);
--accent-foreground: oklch(0.96 0.005 250);
--destructive: oklch(0.65 0.2 25);
--border: oklch(0.3 0.02 250);
--input: oklch(0.22 0.02 250);
--ring: oklch(0.55 0.02 250);
--popover: oklch(0.15 0.015 250);
--popover-foreground: oklch(0.96 0.005 250);
--sidebar: oklch(0.08 0.01 250);
--sidebar-foreground: oklch(0.96 0.005 250);
--sidebar-primary: oklch(0.6 0.12 250);
--sidebar-primary-foreground: oklch(0.1 0 0);
--sidebar-accent: oklch(0.22 0.02 250);
--sidebar-accent-foreground: oklch(0.96 0.005 250);
--sidebar-border: oklch(0.3 0.02 250);
--sidebar-ring: oklch(0.55 0.02 250);
}
```
---
## 💎 Thème BLUE - TEINTE 225° (Saturé)
**Description : Version plus vibrante et saturée de Slate, garde le côté bleu mais élégant**
### Light Mode
```css
[data-theme='blue'] {
--background: oklch(0.985 0.005 225); /* Blanc légèrement bleuté */
--foreground: oklch(0.18 0.035 225); /* Gris-bleu foncé saturé */
--card: oklch(1 0 0); /* Blanc pur */
--card-foreground: oklch(0.18 0.035 225);
--primary: oklch(0.5 0.15 225); /* Bleu vibrant */
--primary-foreground: oklch(0.99 0 0); /* Blanc */
--secondary: oklch(0.93 0.008 225);
--secondary-foreground: oklch(0.18 0.035 225);
--muted: oklch(0.9 0.01 225);
--muted-foreground: oklch(0.58 0.015 225);
--accent: oklch(0.93 0.01 225);
--accent-foreground: oklch(0.18 0.035 225);
--destructive: oklch(0.6 0.2 25);
--border: oklch(0.87 0.012 225);
--input: oklch(0.95 0.01 225);
--ring: oklch(0.65 0.015 225);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.18 0.035 225);
--sidebar: oklch(0.965 0.008 225);
--sidebar-foreground: oklch(0.18 0.035 225);
--sidebar-primary: oklch(0.5 0.15 225);
--sidebar-primary-foreground: oklch(0.99 0 0);
--sidebar-accent: oklch(0.93 0.01 225);
--sidebar-accent-foreground: oklch(0.18 0.035 225);
--sidebar-border: oklch(0.87 0.012 225);
--sidebar-ring: oklch(0.65 0.015 225);
}
```
### Dark Mode
```css
[data-theme='blue'].dark {
--background: oklch(0.13 0.008 225); /* Noir légèrement bleuté */
--foreground: oklch(0.97 0.006 225); /* Blanc légèrement bleuté */
--card: oklch(0.17 0.01 225); /* Gris-bleu foncé */
--card-foreground: oklch(0.97 0.006 225);
--primary: oklch(0.6 0.15 225); /* Bleu vibrant plus clair */
--primary-foreground: oklch(0.1 0 0); /* Noir */
--secondary: oklch(0.22 0.015 225);
--secondary-foreground: oklch(0.97 0.006 225);
--muted: oklch(0.25 0.02 225);
--muted-foreground: oklch(0.52 0.018 225);
--accent: oklch(0.25 0.025 225);
--accent-foreground: oklch(0.97 0.006 225);
--destructive: oklch(0.65 0.22 25);
--border: oklch(0.32 0.018 225);
--input: oklch(0.25 0.02 225);
--ring: oklch(0.55 0.02 225);
--popover: oklch(0.17 0.01 225);
--popover-foreground: oklch(0.97 0.006 225);
--sidebar: oklch(0.1 0.01 225);
--sidebar-foreground: oklch(0.97 0.006 225);
--sidebar-primary: oklch(0.6 0.15 225);
--sidebar-primary-foreground: oklch(0.1 0 0);
--sidebar-accent: oklch(0.25 0.025 225);
--sidebar-accent-foreground: oklch(0.97 0.006 225);
--sidebar-border: oklch(0.32 0.018 225);
--sidebar-ring: oklch(0.55 0.02 225);
}
```
---
## 📜 Thème SEPIA - TEINTE 45° (Chaleureux)
**Description : Version vintage chaleureuse avec une touche dorée/brun, garde le gris comme base**
### Light Mode
```css
[data-theme='sepia'] {
--background: oklch(0.985 0.004 45); /* Blanc légèrement doré */
--foreground: oklch(0.2 0.015 45); /* Gris-brun foncé */
--card: oklch(1 0 0); /* Blanc pur */
--card-foreground: oklch(0.2 0.015 45);
--primary: oklch(0.45 0.08 45); /* Gris-brun chaud */
--primary-foreground: oklch(0.99 0 0); /* Blanc */
--secondary: oklch(0.94 0.008 45);
--secondary-foreground: oklch(0.2 0.015 45);
--muted: oklch(0.91 0.01 45);
--muted-foreground: oklch(0.6 0.012 45);
--accent: oklch(0.93 0.01 45);
--accent-foreground: oklch(0.2 0.015 45);
--destructive: oklch(0.6 0.2 25);
--border: oklch(0.88 0.012 45);
--input: oklch(0.97 0.008 45);
--ring: oklch(0.68 0.01 45);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.2 0.015 45);
--sidebar: oklch(0.96 0.01 45);
--sidebar-foreground: oklch(0.2 0.015 45);
--sidebar-primary: oklch(0.45 0.08 45);
--sidebar-primary-foreground: oklch(0.99 0 0);
--sidebar-accent: oklch(0.93 0.01 45);
--sidebar-accent-foreground: oklch(0.2 0.015 45);
--sidebar-border: oklch(0.88 0.012 45);
--sidebar-ring: oklch(0.68 0.01 45);
}
```
### Dark Mode
```css
[data-theme='sepia'].dark {
--background: oklch(0.15 0.008 45); /* Noir légèrement bruni */
--foreground: oklch(0.97 0.005 45); /* Blanc légèrement bruni */
--card: oklch(0.19 0.01 45); /* Gris-brun foncé */
--card-foreground: oklch(0.97 0.005 45);
--primary: oklch(0.55 0.08 45); /* Gris-brun plus clair */
--primary-foreground: oklch(0.1 0 0); /* Noir */
--secondary: oklch(0.25 0.015 45);
--secondary-foreground: oklch(0.97 0.005 45);
--muted: oklch(0.23 0.02 45);
--muted-foreground: oklch(0.55 0.012 45);
--accent: oklch(0.27 0.018 45);
--accent-foreground: oklch(0.97 0.005 45);
--destructive: oklch(0.65 0.2 25);
--border: oklch(0.3 0.018 45);
--input: oklch(0.27 0.02 45);
--ring: oklch(0.58 0.02 45);
--popover: oklch(0.19 0.01 45);
--popover-foreground: oklch(0.97 0.005 45);
--sidebar: oklch(0.12 0.01 45);
--sidebar-foreground: oklch(0.97 0.005 45);
--sidebar-primary: oklch(0.55 0.08 45);
--sidebar-primary-foreground: oklch(0.1 0 0);
--sidebar-accent: oklch(0.27 0.018 45);
--sidebar-accent-foreground: oklch(0.97 0.005 45);
--sidebar-border: oklch(0.3 0.018 45);
--sidebar-ring: oklch(0.58 0.02 45);
}
```
---
## 🎨 Visualisation des Teintes
### Diagramme des teintes OKLCH :
```
45° (Sepia) 225° (Blue)
│ │
│ 230° (Slate) │
│ ← principal → │
↓ ↓
Gris-brun Gris-bleu
chaleureux saturé
250° (Midnight)
Gris-bleu
profond/sombre
```
### Relation entre les thèmes :
```
Slate (230°) ⭐ ← THÈME PRINCIPAL
├── Midnight (250°) : Version + sombre
├── Blue (225°) : Version + saturée
└── Sepia (45°) : Version chaleureuse
```
---
## ✅ Modifications à faire
Dans `keep-notes/app/globals.css` :
### 1. Remplacer les lignes 169-188 (midnight actuel)
Par le nouveau code **MIDNIGHT** ci-dessus
### 2. Remplacer les lignes 190-217 (blue actuel)
Par le nouveau code **BLUE** ci-dessus
### 3. Remplacer les lignes 219-238 (sepia actuel)
Par le nouveau code **SEPIA** ci-dessus
---
## 💬 Choix du thème
Utilisez ce tableau pour choisir :
| Pour l'utilisation de jour | Utilisez |
|-------------------------|------------|
| Travail standard, productivité | **Slate** (230°) ⭐ |
| Ambiance calme, liseuse | **Sepia** (45°) |
| Ambiance énergique, créative | **Blue** (225°) |
| Pour l'utilisation de nuit | Utilisez |
|-------------------------|------------|
| Nuit profonde, coding | **Midnight** (250°) |
| Nuit légère, confort | **Slate dark** (230°) |
---
**Tous les thèmes sont harmonisés !** 🎨
*Thèmes créés par Amelia - Developer Agent* 💻

View File

@@ -1,521 +0,0 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Visualisation des Couleurs - Slate Moderne</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
padding: 40px;
background: #f5f5f5;
}
h1 {
text-align: center;
margin-bottom: 50px;
color: #333;
font-size: 32px;
}
h2 {
margin: 40px 0 20px 0;
color: #444;
font-size: 24px;
}
.section {
max-width: 1400px;
margin: 0 auto 60px;
}
.theme-comparison {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(600px, 1fr));
gap: 40px;
margin-bottom: 60px;
}
.theme-box {
background: white;
border-radius: 12px;
padding: 30px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
}
.theme-header {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 20px;
padding-bottom: 20px;
border-bottom: 2px solid #eee;
}
.theme-name {
font-size: 22px;
font-weight: bold;
}
.color-swatches {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15px;
}
.swatch {
display: flex;
align-items: center;
gap: 15px;
padding: 15px;
border-radius: 8px;
background: #fafafa;
}
.color-box {
width: 80px;
height: 80px;
border-radius: 8px;
border: 2px solid #e0e0e0;
flex-shrink: 0;
}
.color-info {
flex: 1;
}
.color-label {
font-size: 14px;
color: #666;
margin-bottom: 5px;
}
.color-value {
font-size: 12px;
color: #999;
font-family: monospace;
}
.comparison-section {
background: white;
border-radius: 12px;
padding: 40px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
}
.comparison-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 30px;
margin-top: 30px;
}
.comparison-item {
text-align: center;
}
.comparison-label {
font-size: 18px;
font-weight: bold;
margin-bottom: 15px;
color: #333;
}
.comparison-swatch {
height: 150px;
border-radius: 12px;
border: 3px solid #e0e0e0;
margin-bottom: 15px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
font-size: 16px;
padding: 10px;
}
.comparison-code {
font-family: monospace;
font-size: 13px;
color: #666;
background: #f5f5f5;
padding: 10px 15px;
border-radius: 6px;
display: inline-block;
}
.note-colors {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
}
.note-card {
border-radius: 12px;
padding: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.note-card:hover {
transform: translateY(-5px);
box-shadow: 0 5px 20px rgba(0,0,0,0.15);
}
.note-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 10px;
}
.note-content {
font-size: 14px;
color: #666;
}
.recommendation {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
border-radius: 12px;
text-align: center;
margin-bottom: 50px;
}
.recommendation h2 {
color: white;
margin: 0 0 20px 0;
}
.recommendation p {
font-size: 18px;
color: #f0f0f0;
}
.winner {
border: 3px solid #667eea;
}
</style>
</head>
<body>
<h1>🎨 Visualisation des Couleurs - Options Modernes</h1>
<div class="recommendation">
<h2>🏆 RECOMMANDATION PRINCIPALE : SLATE (GRIS-BLEU)</h2>
<p>Plus professionnel, moins fatigant, moderne - Sans dégradés !</p>
</div>
<!-- COMPARAISON : AVANT / APRÈS -->
<div class="section">
<h2>📊 Comparaison : Bleu Actuel vs Slate Moderne</h2>
<div class="comparison-section">
<div class="comparison-grid">
<div class="comparison-item">
<div class="comparison-label">Bleu Keep Actuel</div>
<div class="comparison-swatch" style="background: #356AC0;">
#356AC0
</div>
<span class="comparison-code">Bleu saturé</span>
</div>
<div class="comparison-item">
<div class="comparison-label">Slate Moderne (NOUVEAU)</div>
<div class="comparison-swatch" style="background: #7A8A9A;">
#7A8A9A
</div>
<span class="comparison-code">Gris-bleu élégant</span>
</div>
</div>
</div>
</div>
<!-- THEME SLATE - LIGHT MODE -->
<div class="section">
<h2>✨ Option 1 : Slate (Gris-Bleu) - Mode Light</h2>
<div class="theme-comparison">
<div class="theme-box winner">
<div class="theme-header">
<span class="theme-name">🏆 SLATE LIGHT</span>
</div>
<div class="color-swatches">
<div class="swatch">
<div class="color-box" style="background: #F8F9FB;"></div>
<div class="color-info">
<div class="color-label">Background</div>
<div class="color-value">#F8F9FB</div>
</div>
</div>
<div class="swatch">
<div class="color-box" style="background: #FFFFFF;"></div>
<div class="color-info">
<div class="color-label">Card</div>
<div class="color-value">#FFFFFF</div>
</div>
</div>
<div class="swatch">
<div class="color-box" style="background: #F3F4F6;"></div>
<div class="color-info">
<div class="color-label">Sidebar</div>
<div class="color-value">#F3F4F6</div>
</div>
</div>
<div class="swatch">
<div class="color-box" style="background: #3B4252;"></div>
<div class="color-info">
<div class="color-label">Texte (foreground)</div>
<div class="color-value">#3B4252</div>
</div>
</div>
<div class="swatch">
<div class="color-box" style="background: #7A8A9A;"></div>
<div class="color-info">
<div class="color-label">Primary (boutons)</div>
<div class="color-value">#7A8A9A</div>
</div>
</div>
<div class="swatch">
<div class="color-box" style="background: #E5E7EB;"></div>
<div class="color-info">
<div class="color-label">Border</div>
<div class="color-value">#E5E7EB</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- THEME SLATE - DARK MODE -->
<div class="section">
<h2>🌙 Option 1 : Slate (Gris-Bleu) - Mode Dark</h2>
<div class="theme-comparison">
<div class="theme-box winner">
<div class="theme-header">
<span class="theme-name">🏆 SLATE DARK</span>
</div>
<div class="color-swatches">
<div class="swatch">
<div class="color-box" style="background: #1F2937;"></div>
<div class="color-info">
<div class="color-label">Background</div>
<div class="color-value">#1F2937</div>
</div>
</div>
<div class="swatch">
<div class="color-box" style="background: #2D3748;"></div>
<div class="color-info">
<div class="color-label">Card</div>
<div class="color-value">#2D3748</div>
</div>
</div>
<div class="swatch">
<div class="color-box" style="background: #1A202C;"></div>
<div class="color-info">
<div class="color-label">Sidebar</div>
<div class="color-value">#1A202C</div>
</div>
</div>
<div class="swatch">
<div class="color-box" style="background: #F7FAFC;"></div>
<div class="color-info">
<div class="color-label">Texte (foreground)</div>
<div class="color-value">#F7FAFC</div>
</div>
</div>
<div class="swatch">
<div class="color-box" style="background: #9CA3AF;"></div>
<div class="color-info">
<div class="color-label">Primary (boutons)</div>
<div class="color-value">#9CA3AF</div>
</div>
</div>
<div class="swatch">
<div class="color-box" style="background: #4A5568;"></div>
<div class="color-info">
<div class="color-label">Border</div>
<div class="color-value">#4A5568</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- COULEURS DES NOTES -->
<div class="section">
<h2>📝 Couleurs des Notes (Light Mode)</h2>
<div class="note-colors">
<div class="note-card" style="background: #FFFFFF; border: 1px solid #E5E7EB;">
<div class="note-title">Default</div>
<div class="note-content">Note blanche standard</div>
</div>
<div class="note-card" style="background: #FEF2F2; border: 1px solid #FEE2E2;">
<div class="note-title" style="color: #7F1D1D;">Red</div>
<div class="note-content" style="color: #991B1B;">Note rouge</div>
</div>
<div class="note-card" style="background: #FFF7ED; border: 1px solid #FFEDD5;">
<div class="note-title" style="color: #9A3412;">Orange</div>
<div class="note-content" style="color: #C2410C;">Note orange</div>
</div>
<div class="note-card" style="background: #FEFCE8; border: 1px solid #FEF9C3;">
<div class="note-title" style="color: #854D0E;">Yellow</div>
<div class="note-content" style="color: #A16207;">Note jaune</div>
</div>
<div class="note-card" style="background: #ECFDF5; border: 1px solid #D1FAE5;">
<div class="note-title" style="color: #065F46;">Green (Emerald)</div>
<div class="note-content" style="color: #047857;">Note verte</div>
</div>
<div class="note-card" style="background: #F0FDFA; border: 1px solid #CCFBF1;">
<div class="note-title" style="color: #0F766E;">Teal</div>
<div class="note-content" style="color: #115E59;">Note teal</div>
</div>
<div class="note-card" style="background: #EFF6FF; border: 1px solid #DBEAFE;">
<div class="note-title" style="color: #1E40AF;">Blue (Sky)</div>
<div class="note-content" style="color: #1D4ED8;">Note bleue</div>
</div>
<div class="note-card" style="background: #EEF2FF; border: 1px solid #E0E7FF;">
<div class="note-title" style="color: #4338CA;">Indigo</div>
<div class="note-content" style="color: #4338CA;">Note indigo</div>
</div>
<div class="note-card" style="background: #F5F3FF; border: 1px solid #EDE9FE;">
<div class="note-title" style="color: #7C3AED;">Violet</div>
<div class="note-content" style="color: #7C3AED;">Note violette</div>
</div>
<div class="note-card" style="background: #FAF5FF; border: 1px solid #F3E8FF;">
<div class="note-title" style="color: #9333EA;">Purple</div>
<div class="note-content" style="color: #9333EA;">Note pourpre</div>
</div>
<div class="note-card" style="background: #FDF2F8; border: 1px solid #FCE7F3;">
<div class="note-title" style="color: #BE185D;">Pink</div>
<div class="note-content" style="color: #BE185D;">Note rose</div>
</div>
<div class="note-card" style="background: #FFF1F2; border: 1px solid #FFE4E6;">
<div class="note-title" style="color: #E11D48;">Rose</div>
<div class="note-content" style="color: #E11D48;">Note rose rougeâtre</div>
</div>
<div class="note-card" style="background: #F5F5F4; border: 1px solid #E7E5E4;">
<div class="note-title" style="color: #71717A;">Gray</div>
<div class="note-content" style="color: #71717A;">Note grise</div>
</div>
</div>
</div>
<!-- AUTRES OPTIONS -->
<div class="section">
<h2>🔄 Autres Options de Thème</h2>
<div class="theme-comparison">
<div class="theme-box">
<div class="theme-header">
<span class="theme-name">⚫ MONOCHROME</span>
</div>
<div class="color-swatches">
<div class="swatch">
<div class="color-box" style="background: #FFFFFF;"></div>
<div class="color-info">
<div class="color-label">Background</div>
<div class="color-value">#FFFFFF</div>
</div>
</div>
<div class="swatch">
<div class="color-box" style="background: #262626;"></div>
<div class="color-info">
<div class="color-label">Primary</div>
<div class="color-value">#262626</div>
</div>
</div>
</div>
</div>
<div class="theme-box">
<div class="theme-header">
<span class="theme-name">💜 INDIGO (Violet)</span>
</div>
<div class="color-swatches">
<div class="swatch">
<div class="color-box" style="background: #F9F9FB;"></div>
<div class="color-info">
<div class="color-label">Background</div>
<div class="color-value">#F9F9FB</div>
</div>
</div>
<div class="swatch">
<div class="color-box" style="background: #7C3AED;"></div>
<div class="color-info">
<div class="color-label">Primary</div>
<div class="color-value">#7C3AED</div>
</div>
</div>
</div>
</div>
<div class="theme-box">
<div class="theme-header">
<span class="theme-name">🌊 TEAL (Turquoise)</span>
</div>
<div class="color-swatches">
<div class="swatch">
<div class="color-box" style="background: #F9FAFB;"></div>
<div class="color-info">
<div class="color-label">Background</div>
<div class="color-value">#F9FAFB</div>
</div>
</div>
<div class="swatch">
<div class="color-box" style="background: #0F766E;"></div>
<div class="color-info">
<div class="color-label">Primary</div>
<div class="color-value">#0F766E</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="section" style="text-align: center; margin-top: 60px;">
<h2>💬 Votre Choix Ramez ?</h2>
<p style="font-size: 18px; color: #666; margin-bottom: 30px;">
Regardez les couleurs, choisissez ce que vous préférez, puis dites-moi :
</p>
<div style="display: flex; gap: 20px; justify-content: center; flex-wrap: wrap;">
<div style="background: #10B981; color: white; padding: 15px 30px; border-radius: 8px; font-weight: bold; font-size: 16px;">
✅ SLATE (mon choix)
</div>
<div style="background: #6366F1; color: white; padding: 15px 30px; border-radius: 8px; font-weight: bold; font-size: 16px;">
💜 INDIGO
</div>
<div style="background: #0F766E; color: white; padding: 15px 30px; border-radius: 8px; font-weight: bold; font-size: 16px;">
🌊 TEAL
</div>
<div style="background: #262626; color: white; padding: 15px 30px; border-radius: 8px; font-weight: bold; font-size: 16px;">
⚫ MONOCHROME
</div>
</div>
</div>
<div style="text-align: center; margin-top: 40px; padding-bottom: 40px; color: #999; font-size: 14px;">
<p>💡 Ouvrez ce fichier dans votre navigateur pour voir toutes les couleurs !</p>
<p>Double-cliquez sur : keep-notes/VISUALISATION-COULEURS.html</p>
</div>
</body>
</html>

View File

@@ -1,27 +0,0 @@
const fs = require('fs');
function updateLocale(file, lang) {
const content = fs.readFileSync(file, 'utf8');
const data = JSON.parse(content);
if (lang === 'fr') {
data.ai.clarifyDesc = "Rendre le propos plus clair et compréhensible";
data.ai.shortenDesc = "Résumer le texte et aller à l'essentiel";
data.ai.improve = "Améliorer la rédaction";
data.ai.improveDesc = "Corriger les fautes et le style";
data.ai.toMarkdown = "Formater en Markdown";
data.ai.toMarkdownDesc = "Ajouter des titres, des puces et structurer le texte";
} else if (lang === 'en') {
data.ai.clarifyDesc = "Make the text clearer and easier to understand";
data.ai.shortenDesc = "Summarize the text and get to the point";
data.ai.improve = "Improve writing";
data.ai.improveDesc = "Fix grammar and enhance style";
data.ai.toMarkdown = "Format as Markdown";
data.ai.toMarkdownDesc = "Add headings, bullet points and structure the text";
}
fs.writeFileSync(file, JSON.stringify(data, null, 2));
}
updateLocale('./locales/fr.json', 'fr');
updateLocale('./locales/en.json', 'en');

View File

@@ -1,14 +0,0 @@
#!/bin/bash
# Services à corriger
services=(
"lib/ai/services/batch-organization.service.ts"
"lib/ai/services/embedding.service.ts"
"lib/ai/services/auto-label-creation.service.ts"
"lib/ai/services/contextual-auto-tag.service.ts"
"lib/ai/services/notebook-suggestion.service.ts"
"lib/ai/services/notebook-summary.service.ts"
)
echo "Services to fix:"
printf '%s\n' "${services[@]}"

View File

@@ -1,70 +0,0 @@
import re
# 1. Update types.ts
with open('lib/ai/types.ts', 'r') as f:
types_content = f.read()
types_content = types_content.replace(
'generateTags(content: string): Promise<TagSuggestion[]>',
'generateTags(content: string, language?: string): Promise<TagSuggestion[]>'
)
with open('lib/ai/types.ts', 'w') as f:
f.write(types_content)
# 2. Update OllamaProvider
with open('lib/ai/providers/ollama.ts', 'r') as f:
ollama_content = f.read()
ollama_content = ollama_content.replace(
'async generateTags(content: string): Promise<TagSuggestion[]>',
'async generateTags(content: string, language: string = "en"): Promise<TagSuggestion[]>'
)
# Replace the hardcoded prompt build logic
prompt_logic = """
const promptText = language === 'fa'
? `متن زیر را تحلیل کن و مفاهیم کلیدی را به عنوان برچسب استخراج کن (حداکثر ۱-۳ کلمه).\nقوانین:\n- کلمات ربط را حذف کن.\n- عبارات ترکیبی را حفظ کن.\n- حداکثر ۵ برچسب.\nپاسخ فقط به صورت لیست JSON با فرمت [{"tag": "string", "confidence": number}]\nمتن: "${content}"`
: language === 'fr'
? `Analyse la note suivante et extrais les concepts clés sous forme de tags courts (1-3 mots max).\nRègles:\n- Pas de mots de liaison.\n- Garde les expressions composées ensemble.\n- Normalise en minuscules sauf noms propres.\n- Maximum 5 tags.\nRéponds UNIQUEMENT sous forme de liste JSON d'objets : [{"tag": "string", "confidence": number}].\nContenu de la note: "${content}"`
: `Analyze the following note and extract key concepts as short tags (1-3 words max).\nRules:\n- No stop words.\n- Keep compound expressions together.\n- Lowercase unless proper noun.\n- Max 5 tags.\nRespond ONLY as a JSON list of objects: [{"tag": "string", "confidence": number}].\nNote content: "${content}"`;
const response = await fetch(`${this.baseUrl}/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: this.modelName,
prompt: promptText,
stream: false,
}),
});
"""
# The original has:
# const response = await fetch(`${this.baseUrl}/generate`, {
# method: 'POST',
# headers: { 'Content-Type': 'application/json' },
# body: JSON.stringify({
# model: this.modelName,
# prompt: `Analyse la note suivante...
ollama_content = re.sub(
r'const response = await fetch\(`\$\{this\.baseUrl\}/generate`.*?\}\),\n\s*\}\);',
prompt_logic.strip(),
ollama_content,
flags=re.DOTALL
)
with open('lib/ai/providers/ollama.ts', 'w') as f:
f.write(ollama_content)
# 3. Update route.ts
with open('app/api/ai/tags/route.ts', 'r') as f:
route_content = f.read()
route_content = route_content.replace(
'const tags = await provider.generateTags(content);',
'const tags = await provider.generateTags(content, language);'
)
with open('app/api/ai/tags/route.ts', 'w') as f:
f.write(route_content)

View File

@@ -1,25 +0,0 @@
with open('app/api/labels/[id]/route.ts', 'r') as f:
content = f.read()
# Fix targetUserId logic
content = content.replace(
'if (name && name.trim() !== currentLabel.name && currentLabel.userId) {',
'const targetUserIdPut = currentLabel.userId || currentLabel.notebook?.userId || session.user.id;\n if (name && name.trim() !== currentLabel.name && targetUserIdPut) {'
)
content = content.replace(
'userId: currentLabel.userId,',
'userId: targetUserIdPut,'
)
content = content.replace(
'if (label.userId) {',
'const targetUserIdDel = label.userId || label.notebook?.userId || session.user.id;\n if (targetUserIdDel) {'
)
content = content.replace(
'userId: label.userId,',
'userId: targetUserIdDel,'
)
with open('app/api/labels/[id]/route.ts', 'w') as f:
f.write(content)

View File

@@ -1,18 +0,0 @@
with open('hooks/use-auto-tagging.ts', 'r') as f:
content = f.read()
if 'useLanguage' not in content:
content = "import { useLanguage } from '@/lib/i18n'\n" + content
content = content.replace(
'export function useAutoTagging(notebookId?: string | null) {',
'export function useAutoTagging(notebookId?: string | null) {\n const { language } = useLanguage();'
)
content = content.replace(
"language: document.documentElement.lang || 'en',",
"language: language || document.documentElement.lang || 'en',"
)
with open('hooks/use-auto-tagging.ts', 'w') as f:
f.write(content)

View File

@@ -1,41 +0,0 @@
import re
files_to_fix = [
'components/note-inline-editor.tsx',
'components/notes-tabs-view.tsx',
'components/note-card.tsx'
]
replacement_func = """import { faIR } from 'date-fns/locale'
function getDateLocale(language: string) {
if (language === 'fr') return fr
if (language === 'fa') return faIR
return enUS
}"""
for file in files_to_fix:
with open(file, 'r') as f:
content = f.read()
# 1. Replace the getDateLocale function
content = re.sub(
r'function getDateLocale\(language: string\) \{\s*if \(language === \'fr\'\) return fr\s*return enUS\s*\}',
"function getDateLocale(language: string) {\n if (language === 'fr') return fr;\n if (language === 'fa') return require('date-fns/locale').faIR;\n return enUS;\n}",
content
)
# Also fix translations for "Modifiée" and "Créée" in inline editor (they are currently hardcoded)
if 'note-inline-editor.tsx' in file:
content = content.replace(
"<span>Modifiée {formatDistanceToNow(new Date(note.updatedAt), { addSuffix: true, locale: dateLocale })}</span>",
"<span>{t('notes.modified') || 'Modifiée'} {formatDistanceToNow(new Date(note.updatedAt), { addSuffix: true, locale: dateLocale })}</span>"
)
content = content.replace(
"<span>Créée {formatDistanceToNow(new Date(note.createdAt), { addSuffix: true, locale: dateLocale })}</span>",
"<span>{t('notes.created') || 'Créée'} {formatDistanceToNow(new Date(note.createdAt), { addSuffix: true, locale: dateLocale })}</span>"
)
with open(file, 'w') as f:
f.write(content)

View File

@@ -1,102 +0,0 @@
import re
with open('components/label-management-dialog.tsx', 'r') as f:
content = f.read()
# Add useNoteRefresh import
if 'useNoteRefresh' not in content:
content = content.replace("import { useLanguage } from '@/lib/i18n'", "import { useLanguage } from '@/lib/i18n'\nimport { useNoteRefresh } from '@/context/NoteRefreshContext'")
# Add useNoteRefresh to component
content = content.replace("const { t } = useLanguage()", "const { t } = useLanguage()\n const { triggerRefresh } = useNoteRefresh()\n const [confirmDeleteId, setConfirmDeleteId] = useState<string | null>(null)")
# Modify handleDeleteLabel
old_delete = """ const handleDeleteLabel = async (id: string) => {
if (confirm(t('labels.confirmDelete'))) {
try {
await deleteLabel(id)
} catch (error) {
console.error('Failed to delete label:', error)
}
}
}"""
new_delete = """ const handleDeleteLabel = async (id: string) => {
try {
await deleteLabel(id)
triggerRefresh()
setConfirmDeleteId(null)
} catch (error) {
console.error('Failed to delete label:', error)
}
}"""
content = content.replace(old_delete, new_delete)
# Also adding triggerRefresh() on addLabel and updateLabel:
content = content.replace(
"await addLabel(trimmed, 'gray')",
"await addLabel(trimmed, 'gray')\n triggerRefresh()"
)
content = content.replace(
"await updateLabel(id, { color })",
"await updateLabel(id, { color })\n triggerRefresh()"
)
# Inline confirm UI: Change the Trash2 button area
old_div = """ <div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-muted-foreground hover:text-foreground"
onClick={() => setEditingColorId(isEditing ? null : label.id)}
title={t('labels.changeColor')}
>
<Palette className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-red-400 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-950/20"
onClick={() => handleDeleteLabel(label.id)}
title={t('labels.deleteTooltip')}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>"""
new_div = """ {confirmDeleteId === label.id ? (
<div className="flex items-center gap-2">
<span className="text-xs text-red-500 font-medium">{t('labels.confirmDeleteShort') || 'Confirmer ?'}</span>
<Button variant="ghost" size="sm" className="h-7 px-2 text-xs" onClick={() => setConfirmDeleteId(null)}>
{t('common.cancel') || 'Annuler'}
</Button>
<Button variant="destructive" size="sm" className="h-7 px-2 text-xs" onClick={() => handleDeleteLabel(label.id)}>
{t('common.delete') || 'Supprimer'}
</Button>
</div>
) : (
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-muted-foreground hover:text-foreground"
onClick={() => setEditingColorId(isEditing ? null : label.id)}
title={t('labels.changeColor')}
>
<Palette className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-red-400 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-950/20"
onClick={() => setConfirmDeleteId(label.id)}
title={t('labels.deleteTooltip')}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
)}"""
content = content.replace(old_div, new_div)
with open('components/label-management-dialog.tsx', 'w') as f:
f.write(content)

View File

@@ -1,22 +0,0 @@
with open('components/label-management-dialog.tsx', 'r') as f:
content = f.read()
# Add language to useLanguage()
content = content.replace(
'const { t } = useLanguage()',
'const { t, language } = useLanguage()'
)
# Add dir to Dialog
content = content.replace(
'<Dialog>',
'<Dialog dir={language === \'fa\' || language === \'ar\' ? \'rtl\' : \'ltr\'}>'
)
content = content.replace(
'<Dialog open={open} onOpenChange={onOpenChange}>',
'<Dialog open={open} onOpenChange={onOpenChange} dir={language === \'fa\' || language === \'ar\' ? \'rtl\' : \'ltr\'}>'
)
with open('components/label-management-dialog.tsx', 'w') as f:
f.write(content)

View File

@@ -1,7 +0,0 @@
with open('components/notebooks-list.tsx', 'r') as f:
content = f.read()
content = content.replace('right-3', 'end-3')
with open('components/notebooks-list.tsx', 'w') as f:
f.write(content)

View File

@@ -1,47 +0,0 @@
import json
def update_json(filepath, updates):
with open(filepath, 'r+', encoding='utf-8') as f:
data = json.load(f)
for key, val in updates.items():
keys = key.split('.')
d = data
for k in keys[:-1]:
if k not in d: d[k] = {}
d = d[k]
d[keys[-1]] = val
f.seek(0)
json.dump(data, f, ensure_ascii=False, indent=2)
f.truncate()
fa_updates = {
'notes.viewTabs': 'نمایش زبانه‌ای',
'notes.viewCards': 'نمایش کارتی',
'labels.filter': 'فیلتر بر اساس برچسب',
'labels.title': 'برچسب‌ها',
'general.clear': 'پاک کردن'
}
fr_updates = {
'notes.viewTabs': 'Vue par onglets',
'notes.viewCards': 'Vue par cartes'
}
en_updates = {
'notes.viewTabs': 'Tabs View',
'notes.viewCards': 'Cards View'
}
update_json('locales/fa.json', fa_updates)
update_json('locales/fr.json', fr_updates)
update_json('locales/en.json', en_updates)
# Now update label-filter.tsx to add explicit dir to wrapping div
with open('components/label-filter.tsx', 'r') as f:
content = f.read()
content = content.replace(
'<div className={cn("flex items-center gap-2", className ? "" : "")}>',
'<div dir={language === \'fa\' || language === \'ar\' ? \'rtl\' : \'ltr\'} className={cn("flex items-center gap-2", className ? "" : "")}>'
)
with open('components/label-filter.tsx', 'w') as f:
f.write(content)

View File

@@ -1,10 +0,0 @@
with open('components/label-filter.tsx', 'r') as f:
content = f.read()
content = content.replace(
'<Button\n variant="outline"',
'<Button\n dir={language === \'fa\' || language === \'ar\' ? \'rtl\' : \'ltr\'}\n variant="outline"'
)
with open('components/label-filter.tsx', 'w') as f:
f.write(content)

View File

@@ -1,19 +0,0 @@
import re
with open('components/label-filter.tsx', 'r') as f:
content = f.read()
# Add language to useLanguage()
content = content.replace(
'const { t } = useLanguage()',
'const { t, language } = useLanguage()'
)
# Add dir to DropdownMenu
content = content.replace(
'<DropdownMenu>',
'<DropdownMenu dir={language === \'fa\' || language === \'ar\' ? \'rtl\' : \'ltr\'}>'
)
with open('components/label-filter.tsx', 'w') as f:
f.write(content)

View File

@@ -1,11 +0,0 @@
with open('hooks/use-auto-tagging.ts', 'r') as f:
content = f.read()
# Make sure we add `const { language } = useLanguage();`
content = content.replace(
'export function useAutoTagging({ content, notebookId, enabled = true }: UseAutoTaggingProps) {',
'export function useAutoTagging({ content, notebookId, enabled = true }: UseAutoTaggingProps) {\n const { language } = useLanguage();'
)
with open('hooks/use-auto-tagging.ts', 'w') as f:
f.write(content)

View File

@@ -1,18 +0,0 @@
with open('components/note-input.tsx', 'r') as f:
content = f.read()
old_call = """ const { suggestions, isAnalyzing } = useAutoTagging({
content: type === 'text' ? fullContentForAI : '',
enabled: type === 'text' && isExpanded
})"""
new_call = """ const { suggestions, isAnalyzing } = useAutoTagging({
content: type === 'text' ? fullContentForAI : '',
enabled: type === 'text' && isExpanded,
notebookId: currentNotebookId
})"""
content = content.replace(old_call, new_call)
with open('components/note-input.tsx', 'w') as f:
f.write(content)

View File

@@ -1,22 +0,0 @@
import re
with open('components/notebooks-list.tsx', 'r') as f:
content = f.read()
# 1. Add `language` to `useLanguage`
content = content.replace("const { t } = useLanguage()", "const { t, language } = useLanguage()")
# 2. Add `dir=\"auto\"` and logical properties to active notebook (isExpanded section)
# Replace pl-12 pr-4 with ps-12 pe-4, mr-2 with me-2, ml-2 with ms-2, rounded-r-full with rounded-e-full, text-left with text-start
content = content.replace("rounded-r-full", "rounded-e-full")
content = content.replace("pl-12", "ps-12").replace("pr-4", "pe-4")
content = content.replace("mr-2", "me-2").replace("ml-2", "ms-2")
content = content.replace("text-left", "text-start")
content = content.replace("pr-24", "pe-24")
# 3. Format numbers: ((notebook as any).notesCount) -> new Intl.NumberFormat(language).format((notebook as any).notesCount)
# Look for: ({(notebook as any).notesCount})
content = content.replace("({(notebook as any).notesCount})", "({new Intl.NumberFormat(language).format((notebook as any).notesCount)})")
with open('components/notebooks-list.tsx', 'w') as f:
f.write(content)

View File

@@ -1,68 +0,0 @@
with open('lib/ai/providers/ollama.ts', 'r') as f:
content = f.read()
# Restore generateTitles and generateText completely
# I will find the boundaries of generateTitles and generateText and replace them.
import re
# We will cut the string from async generateTitles to the end of class, and replace it manually.
start_index = content.find('async generateTitles(prompt: string): Promise<TitleSuggestion[]> {')
if start_index != -1:
content = content[:start_index] + """async generateTitles(prompt: string): Promise<TitleSuggestion[]> {
try {
const response = await fetch(`${this.baseUrl}/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: this.modelName,
prompt: `${prompt}\\n\\nRéponds UNIQUEMENT sous forme de tableau JSON : [{"title": "string", "confidence": number}]`,
stream: false,
}),
});
if (!response.ok) throw new Error(`Ollama error: ${response.statusText}`);
const data = await response.json();
const text = data.response;
// Extraire le JSON de la réponse
const jsonMatch = text.match(/\[\s*\{[\s\S]*\}\s*\]/);
if (jsonMatch) {
return JSON.parse(jsonMatch[0]);
}
return [];
} catch (e) {
console.error('Erreur génération titres Ollama:', e);
return [];
}
}
async generateText(prompt: string): Promise<string> {
try {
const response = await fetch(`${this.baseUrl}/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: this.modelName,
prompt: prompt,
stream: false,
}),
});
if (!response.ok) throw new Error(`Ollama error: ${response.statusText}`);
const data = await response.json();
return data.response.trim();
} catch (e) {
console.error('Erreur génération texte Ollama:', e);
throw e;
}
}
}
"""
with open('lib/ai/providers/ollama.ts', 'w') as f:
f.write(content)

View File

@@ -1,10 +0,0 @@
with open('components/sidebar.tsx', 'r') as f:
content = f.read()
content = content.replace('rounded-r-full', 'rounded-e-full')
content = content.replace('mr-2', 'me-2')
content = content.replace('pl-4', 'ps-4')
content = content.replace('pr-3', 'pe-3')
with open('components/sidebar.tsx', 'w') as f:
f.write(content)

View File

@@ -1,10 +0,0 @@
with open('components/notes-tabs-view.tsx', 'r') as f:
content = f.read()
content = content.replace('rounded-l-xl', 'rounded-s-xl')
content = content.replace('pr-1', 'pe-1')
content = content.replace('pr-3', 'pe-3')
content = content.replace('ml-2', 'ms-2')
with open('components/notes-tabs-view.tsx', 'w') as f:
f.write(content)

View File

@@ -1,34 +0,0 @@
import json
import os
def update_locale(file, updates):
if not os.path.exists(file):
return
with open(file, 'r', encoding='utf-8') as f:
data = json.load(f)
if 'notes' not in data:
data['notes'] = {}
for k, v in updates.items():
data['notes'][k] = v
with open(file, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
fa_updates = {
'modified': 'ویرایش شده',
'created': 'ایجاد شده'
}
en_updates = {
'modified': 'Modified',
'created': 'Created'
}
fr_updates = {
'modified': 'Modifiée',
'created': 'Créée'
}
update_locale('locales/fa.json', fa_updates)
update_locale('locales/en.json', en_updates)
update_locale('locales/fr.json', fr_updates)

View File

@@ -1,301 +0,0 @@
'use client'
import { useState, useEffect, useRef, useCallback, memo, useMemo } from 'react';
import { Note } from '@/lib/types';
import { NoteCard } from './note-card';
import { NoteEditor } from './note-editor';
import { updateFullOrderWithoutRevalidation } from '@/app/actions/notes';
import { useResizeObserver } from '@/hooks/use-resize-observer';
import { useNotebookDrag } from '@/context/notebook-drag-context';
import { useLanguage } from '@/lib/i18n';
interface MasonryGridProps {
notes: Note[];
onEdit?: (note: Note, readOnly?: boolean) => void;
}
interface MasonryItemProps {
note: Note;
onEdit: (note: Note, readOnly?: boolean) => void;
onResize: () => void;
onDragStart?: (noteId: string) => void;
onDragEnd?: () => void;
isDragging?: boolean;
}
function getSizeClasses(size: string = 'small') {
switch (size) {
case 'medium':
return 'w-full sm:w-full lg:w-2/3 xl:w-2/4 2xl:w-2/5';
case 'large':
return 'w-full';
case 'small':
default:
return 'w-full sm:w-1/2 lg:w-1/3 xl:w-1/4 2xl:w-1/5';
}
}
const MasonryItem = memo(function MasonryItem({ note, onEdit, onResize, onDragStart, onDragEnd, isDragging }: MasonryItemProps) {
const resizeRef = useResizeObserver(onResize);
const sizeClasses = getSizeClasses(note.size);
return (
<div
className={`masonry-item absolute p-2 ${sizeClasses}`}
data-id={note.id}
ref={resizeRef as any}
>
<div className="masonry-item-content relative">
<NoteCard
note={note}
onEdit={onEdit}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
isDragging={isDragging}
/>
</div>
</div>
);
}, (prev, next) => {
// Custom comparison to avoid re-render on function prop changes if note data is same
return prev.note.id === next.note.id && prev.note.order === next.note.order && prev.isDragging === next.isDragging;
});
export function MasonryGrid({ notes, onEdit }: MasonryGridProps) {
const { t } = useLanguage();
const [editingNote, setEditingNote] = useState<{ note: Note; readOnly?: boolean } | null>(null);
const { startDrag, endDrag, draggedNoteId } = useNotebookDrag();
// Use external onEdit if provided, otherwise use internal state
const handleEdit = useCallback((note: Note, readOnly?: boolean) => {
if (onEdit) {
onEdit(note, readOnly);
} else {
setEditingNote({ note, readOnly });
}
}, [onEdit]);
const pinnedGridRef = useRef<HTMLDivElement>(null);
const othersGridRef = useRef<HTMLDivElement>(null);
const pinnedMuuri = useRef<any>(null);
const othersMuuri = useRef<any>(null);
// Memoize filtered and sorted notes to avoid recalculation on every render
const pinnedNotes = useMemo(
() => notes.filter(n => n.isPinned).sort((a, b) => a.order - b.order),
[notes]
);
const othersNotes = useMemo(
() => notes.filter(n => !n.isPinned).sort((a, b) => a.order - b.order),
[notes]
);
// CRITICAL: Sync editingNote when underlying note changes (e.g., after moving to notebook)
// This ensures the NoteEditor gets the updated note with the new notebookId
useEffect(() => {
if (!editingNote) return;
// Find the updated version of the currently edited note in the notes array
const updatedNote = notes.find(n => n.id === editingNote.note.id);
if (updatedNote) {
// Check if any key properties changed (especially notebookId)
const notebookIdChanged = updatedNote.notebookId !== editingNote.note.notebookId;
if (notebookIdChanged) {
// Update the editingNote with the new data
setEditingNote(prev => prev ? { ...prev, note: updatedNote } : null);
}
}
}, [notes, editingNote]);
const handleDragEnd = useCallback(async (grid: any) => {
if (!grid) return;
const items = grid.getItems();
const ids = items
.map((item: any) => item.getElement()?.getAttribute('data-id'))
.filter((id: any): id is string => !!id);
try {
// Save order to database WITHOUT revalidating the page
// Muuri has already updated the visual layout, so we don't need to reload
await updateFullOrderWithoutRevalidation(ids);
} catch (error) {
console.error('Failed to persist order:', error);
}
}, []);
const refreshLayout = useCallback(() => {
// Use requestAnimationFrame for smoother updates
requestAnimationFrame(() => {
if (pinnedMuuri.current) {
pinnedMuuri.current.refreshItems().layout();
}
if (othersMuuri.current) {
othersMuuri.current.refreshItems().layout();
}
});
}, []);
// Initialize Muuri grids once on mount and sync when needed
useEffect(() => {
let isMounted = true;
let muuriInitialized = false;
const initMuuri = async () => {
// Prevent duplicate initialization
if (muuriInitialized) return;
muuriInitialized = true;
// Import web-animations-js polyfill
await import('web-animations-js');
// Dynamic import of Muuri to avoid SSR window error
const MuuriClass = (await import('muuri')).default;
if (!isMounted) return;
// Detect if we are on a touch device (mobile behavior)
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
const isMobileWidth = window.innerWidth < 768;
const isMobile = isTouchDevice || isMobileWidth;
const layoutOptions = {
dragEnabled: true,
// Use drag handle for mobile devices to allow smooth scrolling
// On desktop, whole card is draggable (no handle needed)
dragHandle: isMobile ? '.muuri-drag-handle' : undefined,
dragContainer: document.body,
dragStartPredicate: {
distance: 10,
delay: 0,
},
dragPlaceholder: {
enabled: true,
createElement: (item: any) => {
const el = item.getElement().cloneNode(true);
el.style.opacity = '0.5';
return el;
},
},
dragAutoScroll: {
targets: [window],
speed: (item: any, target: any, intersection: any) => {
return intersection * 20;
},
},
};
// Initialize pinned grid
if (pinnedGridRef.current && !pinnedMuuri.current) {
pinnedMuuri.current = new MuuriClass(pinnedGridRef.current, layoutOptions)
.on('dragEnd', () => handleDragEnd(pinnedMuuri.current));
}
// Initialize others grid
if (othersGridRef.current && !othersMuuri.current) {
othersMuuri.current = new MuuriClass(othersGridRef.current, layoutOptions)
.on('dragEnd', () => handleDragEnd(othersMuuri.current));
}
};
initMuuri();
return () => {
isMounted = false;
pinnedMuuri.current?.destroy();
othersMuuri.current?.destroy();
pinnedMuuri.current = null;
othersMuuri.current = null;
};
// Only run once on mount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Synchronize items when notes change (e.g. searching, adding)
useEffect(() => {
requestAnimationFrame(() => {
if (pinnedMuuri.current) {
pinnedMuuri.current.refreshItems().layout();
}
if (othersMuuri.current) {
othersMuuri.current.refreshItems().layout();
}
});
}, [notes]);
return (
<div className="masonry-container">
{pinnedNotes.length > 0 && (
<div className="mb-8">
<h2 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-3 px-2">{t('notes.pinned')}</h2>
<div ref={pinnedGridRef} className="relative min-h-[100px]">
{pinnedNotes.map(note => (
<MasonryItem
key={note.id}
note={note}
onEdit={handleEdit}
onResize={refreshLayout}
onDragStart={startDrag}
onDragEnd={endDrag}
isDragging={draggedNoteId === note.id}
/>
))}
</div>
</div>
)}
{othersNotes.length > 0 && (
<div>
{pinnedNotes.length > 0 && (
<h2 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-3 px-2">{t('notes.others')}</h2>
)}
<div ref={othersGridRef} className="relative min-h-[100px]">
{othersNotes.map(note => (
<MasonryItem
key={note.id}
note={note}
onEdit={handleEdit}
onResize={refreshLayout}
onDragStart={startDrag}
onDragEnd={endDrag}
isDragging={draggedNoteId === note.id}
/>
))}
</div>
</div>
)}
{editingNote && (
<NoteEditor
note={editingNote.note}
readOnly={editingNote.readOnly}
onClose={() => setEditingNote(null)}
/>
)}
<style jsx global>{`
.masonry-item {
display: block;
position: absolute;
z-index: 1;
}
.masonry-item.muuri-item-dragging {
z-index: 3;
}
.masonry-item.muuri-item-releasing {
z-index: 2;
}
.masonry-item.muuri-item-hidden {
z-index: 0;
}
.masonry-item-content {
position: relative;
width: 100%;
height: 100%;
}
`}</style>
</div>
);
}

Binary file not shown.

View File

@@ -1,301 +0,0 @@
'use client'
import { useState, useEffect, useRef, useCallback, memo, useMemo } from 'react';
import { Note } from '@/lib/types';
import { NoteCard } from './note-card';
import { NoteEditor } from './note-editor';
import { updateFullOrderWithoutRevalidation } from '@/app/actions/notes';
import { useResizeObserver } from '@/hooks/use-resize-observer';
import { useNotebookDrag } from '@/context/notebook-drag-context';
import { useLanguage } from '@/lib/i18n';
interface MasonryGridProps {
notes: Note[];
onEdit?: (note: Note, readOnly?: boolean) => void;
}
interface MasonryItemProps {
note: Note;
onEdit: (note: Note, readOnly?: boolean) => void;
onResize: () => void;
onDragStart?: (noteId: string) => void;
onDragEnd?: () => void;
isDragging?: boolean;
}
function getSizeClasses(size: string = 'small') {
switch (size) {
case 'medium':
return 'w-full sm:w-full lg:w-2/3 xl:w-2/4 2xl:w-2/5';
case 'large':
return 'w-full';
case 'small':
default:
return 'w-full sm:w-1/2 lg:w-1/3 xl:w-1/4 2xl:w-1/5';
}
}
const MasonryItem = memo(function MasonryItem({ note, onEdit, onResize, onDragStart, onDragEnd, isDragging }: MasonryItemProps) {
const resizeRef = useResizeObserver(onResize);
const sizeClasses = getSizeClasses(note.size);
return (
<div
className={`masonry-item absolute p-2 ${sizeClasses}`}
data-id={note.id}
ref={resizeRef as any}
>
<div className="masonry-item-content relative">
<NoteCard
note={note}
onEdit={onEdit}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
isDragging={isDragging}
/>
</div>
</div>
);
}, (prev, next) => {
// Custom comparison to avoid re-render on function prop changes if note data is same
return prev.note.id === next.note.id && prev.note.order === next.note.order && prev.isDragging === next.isDragging;
});
export function MasonryGrid({ notes, onEdit }: MasonryGridProps) {
const { t } = useLanguage();
const [editingNote, setEditingNote] = useState<{ note: Note; readOnly?: boolean } | null>(null);
const { startDrag, endDrag, draggedNoteId } = useNotebookDrag();
// Use external onEdit if provided, otherwise use internal state
const handleEdit = useCallback((note: Note, readOnly?: boolean) => {
if (onEdit) {
onEdit(note, readOnly);
} else {
setEditingNote({ note, readOnly });
}
}, [onEdit]);
const pinnedGridRef = useRef<HTMLDivElement>(null);
const othersGridRef = useRef<HTMLDivElement>(null);
const pinnedMuuri = useRef<any>(null);
const othersMuuri = useRef<any>(null);
// Memoize filtered and sorted notes to avoid recalculation on every render
const pinnedNotes = useMemo(
() => notes.filter(n => n.isPinned).sort((a, b) => a.order - b.order),
[notes]
);
const othersNotes = useMemo(
() => notes.filter(n => !n.isPinned).sort((a, b) => a.order - b.order),
[notes]
);
// CRITICAL: Sync editingNote when underlying note changes (e.g., after moving to notebook)
// This ensures the NoteEditor gets the updated note with the new notebookId
useEffect(() => {
if (!editingNote) return;
// Find the updated version of the currently edited note in the notes array
const updatedNote = notes.find(n => n.id === editingNote.note.id);
if (updatedNote) {
// Check if any key properties changed (especially notebookId)
const notebookIdChanged = updatedNote.notebookId !== editingNote.note.notebookId;
if (notebookIdChanged) {
// Update the editingNote with the new data
setEditingNote(prev => prev ? { ...prev, note: updatedNote } : null);
}
}
}, [notes, editingNote]);
const handleDragEnd = useCallback(async (grid: any) => {
if (!grid) return;
const items = grid.getItems();
const ids = items
.map((item: any) => item.getElement()?.getAttribute('data-id'))
.filter((id: any): id is string => !!id);
try {
// Save order to database WITHOUT revalidating the page
// Muuri has already updated the visual layout, so we don't need to reload
await updateFullOrderWithoutRevalidation(ids);
} catch (error) {
console.error('Failed to persist order:', error);
}
}, []);
const refreshLayout = useCallback(() => {
// Use requestAnimationFrame for smoother updates
requestAnimationFrame(() => {
if (pinnedMuuri.current) {
pinnedMuuri.current.refreshItems().layout();
}
if (othersMuuri.current) {
othersMuuri.current.refreshItems().layout();
}
});
}, []);
// Initialize Muuri grids once on mount and sync when needed
useEffect(() => {
let isMounted = true;
let muuriInitialized = false;
const initMuuri = async () => {
// Prevent duplicate initialization
if (muuriInitialized) return;
muuriInitialized = true;
// Import web-animations-js polyfill
await import('web-animations-js');
// Dynamic import of Muuri to avoid SSR window error
const MuuriClass = (await import('muuri')).default;
if (!isMounted) return;
// Detect if we are on a touch device (mobile behavior)
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
const isMobileWidth = window.innerWidth < 768;
const isMobile = isTouchDevice || isMobileWidth;
const layoutOptions = {
dragEnabled: true,
// Use drag handle for mobile devices to allow smooth scrolling
// On desktop, whole card is draggable (no handle needed)
dragHandle: isMobile ? '.muuri-drag-handle' : undefined,
dragContainer: document.body,
dragStartPredicate: {
distance: 10,
delay: 0,
},
dragPlaceholder: {
enabled: true,
createElement: (item: any) => {
const el = item.getElement().cloneNode(true);
el.style.opacity = '0.5';
return el;
},
},
dragAutoScroll: {
targets: [window],
speed: (item: any, target: any, intersection: any) => {
return intersection * 20;
},
},
};
// Initialize pinned grid
if (pinnedGridRef.current && !pinnedMuuri.current) {
pinnedMuuri.current = new MuuriClass(pinnedGridRef.current, layoutOptions)
.on('dragEnd', () => handleDragEnd(pinnedMuuri.current));
}
// Initialize others grid
if (othersGridRef.current && !othersMuuri.current) {
othersMuuri.current = new MuuriClass(othersGridRef.current, layoutOptions)
.on('dragEnd', () => handleDragEnd(othersMuuri.current));
}
};
initMuuri();
return () => {
isMounted = false;
pinnedMuuri.current?.destroy();
othersMuuri.current?.destroy();
pinnedMuuri.current = null;
othersMuuri.current = null;
};
// Only run once on mount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Synchronize items when notes change (e.g. searching, adding)
useEffect(() => {
requestAnimationFrame(() => {
if (pinnedMuuri.current) {
pinnedMuuri.current.refreshItems().layout();
}
if (othersMuuri.current) {
othersMuuri.current.refreshItems().layout();
}
});
}, [notes]);
return (
<div className="masonry-container">
{pinnedNotes.length > 0 && (
<div className="mb-8">
<h2 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-3 px-2">{t('notes.pinned')}</h2>
<div ref={pinnedGridRef} className="relative min-h-[100px]">
{pinnedNotes.map(note => (
<MasonryItem
key={note.id}
note={note}
onEdit={handleEdit}
onResize={refreshLayout}
onDragStart={startDrag}
onDragEnd={endDrag}
isDragging={draggedNoteId === note.id}
/>
))}
</div>
</div>
)}
{othersNotes.length > 0 && (
<div>
{pinnedNotes.length > 0 && (
<h2 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-3 px-2">{t('notes.others')}</h2>
)}
<div ref={othersGridRef} className="relative min-h-[100px]">
{othersNotes.map(note => (
<MasonryItem
key={note.id}
note={note}
onEdit={handleEdit}
onResize={refreshLayout}
onDragStart={startDrag}
onDragEnd={endDrag}
isDragging={draggedNoteId === note.id}
/>
))}
</div>
</div>
)}
{editingNote && (
<NoteEditor
note={editingNote.note}
readOnly={editingNote.readOnly}
onClose={() => setEditingNote(null)}
/>
)}
<style jsx global>{`
.masonry-item {
display: block;
position: absolute;
z-index: 1;
}
.masonry-item.muuri-item-dragging {
z-index: 3;
}
.masonry-item.muuri-item-releasing {
z-index: 2;
}
.masonry-item.muuri-item-hidden {
z-index: 0;
}
.masonry-item-content {
position: relative;
width: 100%;
height: 100%;
}
`}</style>
</div>
);
}

View File

@@ -1,301 +0,0 @@
'use client'
import { useState, useEffect, useRef, useCallback, memo, useMemo } from 'react';
import { Note } from '@/lib/types';
import { NoteCard } from './note-card';
import { NoteEditor } from './note-editor';
import { updateFullOrderWithoutRevalidation } from '@/app/actions/notes';
import { useResizeObserver } from '@/hooks/use-resize-observer';
import { useNotebookDrag } from '@/context/notebook-drag-context';
import { useLanguage } from '@/lib/i18n';
interface MasonryGridProps {
notes: Note[];
onEdit?: (note: Note, readOnly?: boolean) => void;
}
interface MasonryItemProps {
note: Note;
onEdit: (note: Note, readOnly?: boolean) => void;
onResize: () => void;
onDragStart?: (noteId: string) => void;
onDragEnd?: () => void;
isDragging?: boolean;
}
function getSizeClasses(size: string = 'small') {
switch (size) {
case 'medium':
return 'w-full sm:w-full lg:w-2/3 xl:w-2/4 2xl:w-2/5';
case 'large':
return 'w-full';
case 'small':
default:
return 'w-full sm:w-1/2 lg:w-1/3 xl:w-1/4 2xl:w-1/5';
}
}
const MasonryItem = memo(function MasonryItem({ note, onEdit, onResize, onDragStart, onDragEnd, isDragging }: MasonryItemProps) {
const resizeRef = useResizeObserver(onResize);
const sizeClasses = getSizeClasses(note.size);
return (
<div
className={`masonry-item absolute p-2 ${sizeClasses}`}
data-id={note.id}
ref={resizeRef as any}
>
<div className="masonry-item-content relative">
<NoteCard
note={note}
onEdit={onEdit}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
isDragging={isDragging}
/>
</div>
</div>
);
}, (prev, next) => {
// Custom comparison to avoid re-render on function prop changes if note data is same
return prev.note.id === next.note.id && prev.note.order === next.note.order && prev.isDragging === next.isDragging;
});
export function MasonryGrid({ notes, onEdit }: MasonryGridProps) {
const { t } = useLanguage();
const [editingNote, setEditingNote] = useState<{ note: Note; readOnly?: boolean } | null>(null);
const { startDrag, endDrag, draggedNoteId } = useNotebookDrag();
// Use external onEdit if provided, otherwise use internal state
const handleEdit = useCallback((note: Note, readOnly?: boolean) => {
if (onEdit) {
onEdit(note, readOnly);
} else {
setEditingNote({ note, readOnly });
}
}, [onEdit]);
const pinnedGridRef = useRef<HTMLDivElement>(null);
const othersGridRef = useRef<HTMLDivElement>(null);
const pinnedMuuri = useRef<any>(null);
const othersMuuri = useRef<any>(null);
// Memoize filtered and sorted notes to avoid recalculation on every render
const pinnedNotes = useMemo(
() => notes.filter(n => n.isPinned).sort((a, b) => a.order - b.order),
[notes]
);
const othersNotes = useMemo(
() => notes.filter(n => !n.isPinned).sort((a, b) => a.order - b.order),
[notes]
);
// CRITICAL: Sync editingNote when underlying note changes (e.g., after moving to notebook)
// This ensures the NoteEditor gets the updated note with the new notebookId
useEffect(() => {
if (!editingNote) return;
// Find the updated version of the currently edited note in the notes array
const updatedNote = notes.find(n => n.id === editingNote.note.id);
if (updatedNote) {
// Check if any key properties changed (especially notebookId)
const notebookIdChanged = updatedNote.notebookId !== editingNote.note.notebookId;
if (notebookIdChanged) {
// Update the editingNote with the new data
setEditingNote(prev => prev ? { ...prev, note: updatedNote } : null);
}
}
}, [notes, editingNote]);
const handleDragEnd = useCallback(async (grid: any) => {
if (!grid) return;
const items = grid.getItems();
const ids = items
.map((item: any) => item.getElement()?.getAttribute('data-id'))
.filter((id: any): id is string => !!id);
try {
// Save order to database WITHOUT revalidating the page
// Muuri has already updated the visual layout, so we don't need to reload
await updateFullOrderWithoutRevalidation(ids);
} catch (error) {
console.error('Failed to persist order:', error);
}
}, []);
const refreshLayout = useCallback(() => {
// Use requestAnimationFrame for smoother updates
requestAnimationFrame(() => {
if (pinnedMuuri.current) {
pinnedMuuri.current.refreshItems().layout();
}
if (othersMuuri.current) {
othersMuuri.current.refreshItems().layout();
}
});
}, []);
// Initialize Muuri grids once on mount and sync when needed
useEffect(() => {
let isMounted = true;
let muuriInitialized = false;
const initMuuri = async () => {
// Prevent duplicate initialization
if (muuriInitialized) return;
muuriInitialized = true;
// Import web-animations-js polyfill
await import('web-animations-js');
// Dynamic import of Muuri to avoid SSR window error
const MuuriClass = (await import('muuri')).default;
if (!isMounted) return;
// Detect if we are on a touch device (mobile behavior)
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
const isMobileWidth = window.innerWidth < 768;
const isMobile = isTouchDevice || isMobileWidth;
const layoutOptions = {
dragEnabled: true,
// Use drag handle for mobile devices to allow smooth scrolling
// On desktop, whole card is draggable (no handle needed)
dragHandle: isMobile ? '.muuri-drag-handle' : undefined,
dragContainer: document.body,
dragStartPredicate: {
distance: 10,
delay: 0,
},
dragPlaceholder: {
enabled: true,
createElement: (item: any) => {
const el = item.getElement().cloneNode(true);
el.style.opacity = '0.5';
return el;
},
},
dragAutoScroll: {
targets: [window],
speed: (item: any, target: any, intersection: any) => {
return intersection * 20;
},
},
};
// Initialize pinned grid
if (pinnedGridRef.current && !pinnedMuuri.current) {
pinnedMuuri.current = new MuuriClass(pinnedGridRef.current, layoutOptions)
.on('dragEnd', () => handleDragEnd(pinnedMuuri.current));
}
// Initialize others grid
if (othersGridRef.current && !othersMuuri.current) {
othersMuuri.current = new MuuriClass(othersGridRef.current, layoutOptions)
.on('dragEnd', () => handleDragEnd(othersMuuri.current));
}
};
initMuuri();
return () => {
isMounted = false;
pinnedMuuri.current?.destroy();
othersMuuri.current?.destroy();
pinnedMuuri.current = null;
othersMuuri.current = null;
};
// Only run once on mount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Synchronize items when notes change (e.g. searching, adding)
useEffect(() => {
requestAnimationFrame(() => {
if (pinnedMuuri.current) {
pinnedMuuri.current.refreshItems().layout();
}
if (othersMuuri.current) {
othersMuuri.current.refreshItems().layout();
}
});
}, [notes]);
return (
<div className="masonry-container">
{pinnedNotes.length > 0 && (
<div className="mb-8">
<h2 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-3 px-2">{t('notes.pinned')}</h2>
<div ref={pinnedGridRef} className="relative min-h-[100px]">
{pinnedNotes.map(note => (
<MasonryItem
key={note.id}
note={note}
onEdit={handleEdit}
onResize={refreshLayout}
onDragStart={startDrag}
onDragEnd={endDrag}
isDragging={draggedNoteId === note.id}
/>
))}
</div>
</div>
)}
{othersNotes.length > 0 && (
<div>
{pinnedNotes.length > 0 && (
<h2 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-3 px-2">{t('notes.others')}</h2>
)}
<div ref={othersGridRef} className="relative min-h-[100px]">
{othersNotes.map(note => (
<MasonryItem
key={note.id}
note={note}
onEdit={handleEdit}
onResize={refreshLayout}
onDragStart={startDrag}
onDragEnd={endDrag}
isDragging={draggedNoteId === note.id}
/>
))}
</div>
</div>
)}
{editingNote && (
<NoteEditor
note={editingNote.note}
readOnly={editingNote.readOnly}
onClose={() => setEditingNote(null)}
/>
)}
<style jsx global>{`
.masonry-item {
display: block;
position: absolute;
z-index: 1;
}
.masonry-item.muuri-item-dragging {
z-index: 3;
}
.masonry-item.muuri-item-releasing {
z-index: 2;
}
.masonry-item.muuri-item-hidden {
z-index: 0;
}
.masonry-item-content {
position: relative;
width: 100%;
height: 100%;
}
`}</style>
</div>
);
}

View File

@@ -1,301 +0,0 @@
'use client'
import { useState, useEffect, useRef, useCallback, memo, useMemo } from 'react';
import { Note } from '@/lib/types';
import { NoteCard } from './note-card';
import { NoteEditor } from './note-editor';
import { updateFullOrderWithoutRevalidation } from '@/app/actions/notes';
import { useResizeObserver } from '@/hooks/use-resize-observer';
import { useNotebookDrag } from '@/context/notebook-drag-context';
import { useLanguage } from '@/lib/i18n';
interface MasonryGridProps {
notes: Note[];
onEdit?: (note: Note, readOnly?: boolean) => void;
}
interface MasonryItemProps {
note: Note;
onEdit: (note: Note, readOnly?: boolean) => void;
onResize: () => void;
onDragStart?: (noteId: string) => void;
onDragEnd?: () => void;
isDragging?: boolean;
}
function getSizeClasses(size: string = 'small') {
switch (size) {
case 'medium':
return 'w-full sm:w-full lg:w-2/3 xl:w-2/4 2xl:w-2/5';
case 'large':
return 'w-full';
case 'small':
default:
return 'w-full sm:w-1/2 lg:w-1/3 xl:w-1/4 2xl:w-1/5';
}
}
const MasonryItem = memo(function MasonryItem({ note, onEdit, onResize, onDragStart, onDragEnd, isDragging }: MasonryItemProps) {
const resizeRef = useResizeObserver(onResize);
const sizeClasses = getSizeClasses(note.size);
return (
<div
className={`masonry-item absolute p-2 ${sizeClasses}`}
data-id={note.id}
ref={resizeRef as any}
>
<div className="masonry-item-content relative">
<NoteCard
note={note}
onEdit={onEdit}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
isDragging={isDragging}
/>
</div>
</div>
);
}, (prev, next) => {
// Custom comparison to avoid re-render on function prop changes if note data is same
return prev.note.id === next.note.id && prev.note.order === next.note.order && prev.isDragging === next.isDragging;
});
export function MasonryGrid({ notes, onEdit }: MasonryGridProps) {
const { t } = useLanguage();
const [editingNote, setEditingNote] = useState<{ note: Note; readOnly?: boolean } | null>(null);
const { startDrag, endDrag, draggedNoteId } = useNotebookDrag();
// Use external onEdit if provided, otherwise use internal state
const handleEdit = useCallback((note: Note, readOnly?: boolean) => {
if (onEdit) {
onEdit(note, readOnly);
} else {
setEditingNote({ note, readOnly });
}
}, [onEdit]);
const pinnedGridRef = useRef<HTMLDivElement>(null);
const othersGridRef = useRef<HTMLDivElement>(null);
const pinnedMuuri = useRef<any>(null);
const othersMuuri = useRef<any>(null);
// Memoize filtered and sorted notes to avoid recalculation on every render
const pinnedNotes = useMemo(
() => notes.filter(n => n.isPinned).sort((a, b) => a.order - b.order),
[notes]
);
const othersNotes = useMemo(
() => notes.filter(n => !n.isPinned).sort((a, b) => a.order - b.order),
[notes]
);
// CRITICAL: Sync editingNote when underlying note changes (e.g., after moving to notebook)
// This ensures the NoteEditor gets the updated note with the new notebookId
useEffect(() => {
if (!editingNote) return;
// Find the updated version of the currently edited note in the notes array
const updatedNote = notes.find(n => n.id === editingNote.note.id);
if (updatedNote) {
// Check if any key properties changed (especially notebookId)
const notebookIdChanged = updatedNote.notebookId !== editingNote.note.notebookId;
if (notebookIdChanged) {
// Update the editingNote with the new data
setEditingNote(prev => prev ? { ...prev, note: updatedNote } : null);
}
}
}, [notes, editingNote]);
const handleDragEnd = useCallback(async (grid: any) => {
if (!grid) return;
const items = grid.getItems();
const ids = items
.map((item: any) => item.getElement()?.getAttribute('data-id'))
.filter((id: any): id is string => !!id);
try {
// Save order to database WITHOUT revalidating the page
// Muuri has already updated the visual layout, so we don't need to reload
await updateFullOrderWithoutRevalidation(ids);
} catch (error) {
console.error('Failed to persist order:', error);
}
}, []);
const refreshLayout = useCallback(() => {
// Use requestAnimationFrame for smoother updates
requestAnimationFrame(() => {
if (pinnedMuuri.current) {
pinnedMuuri.current.refreshItems().layout();
}
if (othersMuuri.current) {
othersMuuri.current.refreshItems().layout();
}
});
}, []);
// Initialize Muuri grids once on mount and sync when needed
useEffect(() => {
let isMounted = true;
let muuriInitialized = false;
const initMuuri = async () => {
// Prevent duplicate initialization
if (muuriInitialized) return;
muuriInitialized = true;
// Import web-animations-js polyfill
await import('web-animations-js');
// Dynamic import of Muuri to avoid SSR window error
const MuuriClass = (await import('muuri')).default;
if (!isMounted) return;
// Detect if we are on a touch device (mobile behavior)
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
const isMobileWidth = window.innerWidth < 768;
const isMobile = isTouchDevice || isMobileWidth;
const layoutOptions = {
dragEnabled: true,
// Use drag handle for mobile devices to allow smooth scrolling
// On desktop, whole card is draggable (no handle needed)
dragHandle: isMobile ? '.muuri-drag-handle' : undefined,
dragContainer: document.body,
dragStartPredicate: {
distance: 10,
delay: 0,
},
dragPlaceholder: {
enabled: true,
createElement: (item: any) => {
const el = item.getElement().cloneNode(true);
el.style.opacity = '0.5';
return el;
},
},
dragAutoScroll: {
targets: [window],
speed: (item: any, target: any, intersection: any) => {
return intersection * 20;
},
},
};
// Initialize pinned grid
if (pinnedGridRef.current && !pinnedMuuri.current) {
pinnedMuuri.current = new MuuriClass(pinnedGridRef.current, layoutOptions)
.on('dragEnd', () => handleDragEnd(pinnedMuuri.current));
}
// Initialize others grid
if (othersGridRef.current && !othersMuuri.current) {
othersMuuri.current = new MuuriClass(othersGridRef.current, layoutOptions)
.on('dragEnd', () => handleDragEnd(othersMuuri.current));
}
};
initMuuri();
return () => {
isMounted = false;
pinnedMuuri.current?.destroy();
othersMuuri.current?.destroy();
pinnedMuuri.current = null;
othersMuuri.current = null;
};
// Only run once on mount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Synchronize items when notes change (e.g. searching, adding)
useEffect(() => {
requestAnimationFrame(() => {
if (pinnedMuuri.current) {
pinnedMuuri.current.refreshItems().layout();
}
if (othersMuuri.current) {
othersMuuri.current.refreshItems().layout();
}
});
}, [notes]);
return (
<div className="masonry-container">
{pinnedNotes.length > 0 && (
<div className="mb-8">
<h2 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-3 px-2">{t('notes.pinned')}</h2>
<div ref={pinnedGridRef} className="relative min-h-[100px]">
{pinnedNotes.map(note => (
<MasonryItem
key={note.id}
note={note}
onEdit={handleEdit}
onResize={refreshLayout}
onDragStart={startDrag}
onDragEnd={endDrag}
isDragging={draggedNoteId === note.id}
/>
))}
</div>
</div>
)}
{othersNotes.length > 0 && (
<div>
{pinnedNotes.length > 0 && (
<h2 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-3 px-2">{t('notes.others')}</h2>
)}
<div ref={othersGridRef} className="relative min-h-[100px]">
{othersNotes.map(note => (
<MasonryItem
key={note.id}
note={note}
onEdit={handleEdit}
onResize={refreshLayout}
onDragStart={startDrag}
onDragEnd={endDrag}
isDragging={draggedNoteId === note.id}
/>
))}
</div>
</div>
)}
{editingNote && (
<NoteEditor
note={editingNote.note}
readOnly={editingNote.readOnly}
onClose={() => setEditingNote(null)}
/>
)}
<style jsx global>{`
.masonry-item {
display: block;
position: absolute;
z-index: 1;
}
.masonry-item.muuri-item-dragging {
z-index: 3;
}
.masonry-item.muuri-item-releasing {
z-index: 2;
}
.masonry-item.muuri-item-hidden {
z-index: 0;
}
.masonry-item-content {
position: relative;
width: 100%;
height: 100%;
}
`}</style>
</div>
);
}

View File

@@ -1 +0,0 @@
{"content":"This is a test note about artificial intelligence and machine learning in Python"}

View File

@@ -1,8 +0,0 @@
// Test Memory Echo API
async function testMemoryEcho() {
const res = await fetch('http://localhost:3000/api/ai/echo');
const data = await res.json();
console.log('Memory Echo Response:', JSON.stringify(data, null, 2));
}
testMemoryEcho();

View File

@@ -1 +0,0 @@
{"text":"This is a test paragraph that needs to be rewritten. It contains multiple sentences and should be improved.","mode":"clarify"}

View File

@@ -1,47 +0,0 @@
import json
def update_json(filepath, updates):
with open(filepath, 'r+', encoding='utf-8') as f:
data = json.load(f)
for key, val in updates.items():
keys = key.split('.')
d = data
for k in keys[:-1]:
if k not in d: d[k] = {}
d = d[k]
d[keys[-1]] = val
f.seek(0)
json.dump(data, f, ensure_ascii=False, indent=2)
f.truncate()
fa_updates = {
'sidebar.editLabels': 'ویرایش برچسب‌ها',
'sidebar.edit': 'ویرایش یادداشت',
'labels.confirmDeleteShort': 'تایید؟',
'common.cancel': 'لغو',
'common.delete': 'حذف',
'labels.editLabels': 'ویرایش برچسب‌ها',
'labels.editLabelsDescription': 'برچسب‌های خود را مدیریت کنید',
'labels.newLabelPlaceholder': 'برچسب جدید...',
'labels.loading': 'در حال بارگذاری...',
'labels.noLabelsFound': 'برچسبی یافت نشد',
'labels.changeColor': 'تغییر رنگ',
'labels.deleteTooltip': 'حذف برچسب',
}
fr_updates = {
'labels.confirmDeleteShort': 'Confirmer ?',
'common.cancel': 'Annuler',
'common.delete': 'Supprimer'
}
en_updates = {
'labels.confirmDeleteShort': 'Confirm?',
'common.cancel': 'Cancel',
'common.delete': 'Delete'
}
update_json('locales/fa.json', fa_updates)
update_json('locales/fr.json', fr_updates)
update_json('locales/en.json', en_updates)

View File

@@ -1,17 +0,0 @@
with open('components/notes-view-toggle.tsx', 'r') as f:
content = f.read()
# Add language to useLanguage()
content = content.replace(
'const { t } = useLanguage()',
'const { t, language } = useLanguage()'
)
# Add dir to div wrapper
content = content.replace(
'className={cn(\n \'inline-flex rounded-full border border-border bg-muted/40 p-0.5 shadow-sm\',\n className\n )}',
'dir={language === \'fa\' || language === \'ar\' ? \'rtl\' : \'ltr\'}\n className={cn(\n \'inline-flex rounded-full border border-border bg-muted/40 p-0.5 shadow-sm\',\n className\n )}'
)
with open('components/notes-view-toggle.tsx', 'w') as f:
f.write(content)