chore: clean up repo - remove docs, tool configs, test files, images

Removed from repo and disk:
- _bmad/, _bmad-output/, .agent/, .claude/, .cursor/, .gemini/,
  .kilocode/, .opencode/, .playwright-mcp/, .ralph/, .github/, bmalph/
- docs/ directory (outdated architecture docs)
- Root markdown files (18 obsolete docs)
- Root images (8 screenshots/test images)
- MCP server docs and N8N workflow JSONs (7 files)
- memento-note junk (test files, temp files, color guides, old DB)
- Root package-lock.json, package.json.bak

Updated .gitignore to prevent re-tracking.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Sepehr Ramezani
2026-04-20 21:07:10 +02:00
parent aa6a214f37
commit a98a60928c
642 changed files with 30 additions and 115008 deletions

View File

@@ -1,397 +0,0 @@
# 🐳 Docker Deployment Guide for Proxmox
Complete guide to deploy Memento on Proxmox using Docker Compose.
## 📋 Prerequisites
### On Your Proxmox Host:
- Proxmox VE 7.x or 8.x
- Docker and Docker Compose installed
- At least 2GB RAM available (4GB+ recommended for AI features)
- 10GB disk space available
### Optional for AI Features:
- **For OpenAI**: Valid API key
- **For Ollama (Local AI)**: 8GB+ RAM, 4+ CPU cores recommended
## 🚀 Quick Start
### 1. Prepare Environment Files
Create a `.env` file in the `keep-notes` directory:
```bash
cd /path/to/keep-notes
cat > .env << 'EOF'
# Required: Generate a random secret
NEXTAUTH_SECRET=$(openssl rand -base64 32)
NEXTAUTH_URL=http://your-domain.com:3000
# Optional: OpenAI API Key
# OPENAI_API_KEY=sk-your-key-here
# Optional: Ollama Configuration (if using local AI)
# OLLAMA_BASE_URL=http://ollama:11434
# OLLAMA_MODEL=granite4:latest
# Optional: Custom Session Max Age (in seconds)
NEXTAUTH_SESSION_MAX_AGE=604800
EOF
```
### 2. Build and Start Containers
```bash
# Build the Docker image
docker compose build
# Start the application
docker compose up -d
# View logs
docker compose logs -f keep-notes
```
### 3. Access the Application
Open your browser and navigate to:
- **http://YOUR_PROXMOX_IP:3000**
## 🔧 Configuration Options
### Without Reverse Proxy (Basic)
Edit `docker-compose.yml`:
```yaml
environment:
- NEXTAUTH_URL=http://your-ip:3000
- NEXTAUTH_SECRET=your-random-secret
ports:
- "3000:3000"
```
### With Nginx Reverse Proxy (Recommended)
#### 1. Create Nginx Configuration
```nginx
# /etc/nginx/conf.d/keep-notes.conf
server {
listen 80;
server_name notes.yourdomain.com;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Max upload size for images
client_max_body_size 10M;
}
```
#### 2. Update docker-compose.yml
```yaml
environment:
- NEXTAUTH_URL=https://notes.yourdomain.com
```
#### 3. Restart Container
```bash
docker compose down
docker compose up -d
```
### With SSL/HTTPS (Let's Encrypt)
```bash
# Install certbot
apt install certbot python3-certbot-nginx
# Get certificate
certbot --nginx -d notes.yourdomain.com
# Auto-renewal (cron)
echo "0 0,12 * * * root certbot renew --quiet" | tee /etc/cron.d/certbot-renew
```
## 🤖 AI Features Setup
### Option 1: OpenAI (Cloud)
1. Get API key from https://platform.openai.com/api-keys
2. Add to `.env`:
```bash
OPENAI_API_KEY=sk-your-key-here
```
3. Restart: `docker compose restart`
### Option 2: Ollama (Local AI)
#### 1. Enable Ollama in docker-compose.yml
Uncomment the `ollama` service section in `docker-compose.yml`:
```yaml
ollama:
image: ollama/ollama:latest
container_name: keep-ollama
restart: unless-stopped
ports:
- "11434:11434"
volumes:
- ollama-data:/root/.ollama
networks:
- keep-network
```
Uncomment volume:
```yaml
volumes:
ollama-data:
driver: local
```
#### 2. Add Environment Variables
```yaml
keep-notes:
environment:
- OLLAMA_BASE_URL=http://ollama:11434
- OLLAMA_MODEL=granite4:latest
```
#### 3. Start and Pull Model
```bash
docker compose up -d
docker compose exec -it ollama ollama pull granite4
```
### Option 3: Custom OpenAI-compatible API
If you have a custom API (like LocalAI, LM Studio, etc.):
```bash
# Add to .env or docker-compose.yml
OPENAI_API_BASE_URL=http://your-api-host:port/v1
OPENAI_API_KEY=any-key-here
```
## 📊 Resource Recommendations
### Minimal Setup (Without AI)
- **CPU**: 1 core
- **RAM**: 512MB
- **Disk**: 5GB
### Recommended Setup (With OpenAI)
- **CPU**: 1-2 cores
- **RAM**: 1-2GB
- **Disk**: 10GB
### AI Setup (With Ollama)
- **CPU**: 4+ cores
- **RAM**: 8GB+
- **Disk**: 20GB+
## 🗄️ Database Backup
### Backup SQLite Database
```bash
# Create backup script
cat > /path/to/backup-keep.sh << 'EOF'
#!/bin/bash
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/path/to/backups"
CONTAINER_NAME="keep-notes"
# Create backup directory
mkdir -p $BACKUP_DIR
# Backup database
docker exec $CONTAINER_NAME \
cp /app/prisma/dev.db /app/prisma/backup_$DATE.db
# Copy from container to host
docker cp $CONTAINER_NAME:/app/prisma/backup_$DATE.db \
$BACKUP_DIR/keep-notes_$DATE.db
# Keep last 7 days
find $BACKUP_DIR -name "keep-notes_*.db" -mtime +7 -delete
echo "Backup completed: keep-notes_$DATE.db"
EOF
chmod +x /path/to/backup-keep.sh
# Add to crontab (daily backup at 2 AM)
crontab -e
# Add: 0 2 * * * /path/to/backup-keep.sh
```
### Restore Database
```bash
# Stop container
docker compose down
# Restore database
cp /path/to/backups/keep-notes_YYYYMMDD_HHMMSS.db \
keep-notes/prisma/dev.db
# Start container
docker compose up -d
```
## 🔄 Updating the Application
```bash
# Pull latest changes
git pull
# Rebuild image
docker compose build
# Restart with new image
docker compose down
docker compose up -d
# Clean up old images
docker image prune -a -f
```
## 🐛 Troubleshooting
### Container Won't Start
```bash
# Check logs
docker compose logs keep-notes
# Check container status
docker compose ps
# Enter container for debugging
docker compose exec keep-notes sh
```
### Database Errors
```bash
# Fix database permissions
docker compose exec keep-notes \
chown -R nextjs:nodejs /app/prisma
# Regenerate Prisma client
docker compose exec keep-notes \
npx prisma generate
# Run migrations
docker compose exec keep-notes \
npx prisma migrate deploy
```
### AI Features Not Working
```bash
# Check Ollama status
docker compose logs ollama
# Test Ollama connection
docker compose exec keep-notes \
curl http://ollama:11434/api/tags
# Check environment variables
docker compose exec keep-notes env | grep -E "OLLAMA|OPENAI"
```
### Performance Issues
```bash
# Check resource usage
docker stats keep-notes
# Increase resources in docker-compose.yml
deploy:
resources:
limits:
cpus: '4'
memory: 4G
```
## 🔒 Security Best Practices
1. **Change NEXTAUTH_SECRET**: Never use the default value
2. **Use HTTPS**: Always use SSL in production
3. **Limit Resources**: Prevent container from using all system resources
4. **Regular Updates**: Keep Docker image and dependencies updated
5. **Backups**: Set up automated database backups
6. **Firewall**: Only expose necessary ports (3000 or reverse proxy port)
## 📱 Proxmox LXC Container Setup
### Create LXC Container (Recommended)
```bash
# In Proxmox shell
pveam available
pveam update
# Create Ubuntu 22.04 container
pct create 999 local:vztmpl/ubuntu-22.04-standard_22.04-1_amd64.tar.zst \
--hostname keep-notes \
--storage local-lvm \
--cores 2 \
--memory 2048 \
--swap 512 \
--net0 name=eth0,bridge=vmbr0,ip=dhcp
# Start container
pct start 999
# Enter container
pct enter 999
# Install Docker inside LXC
apt update && apt upgrade -y
apt install -y curl git
curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh
usermod -aG docker ubuntu
# Enable nested containerization for LXC
# Edit /etc/pve/lxc/999.conf on Proxmox host
# Add: features: nesting=1,keyctl=1
```
Then deploy Memento as described above.
## 📚 Additional Resources
- [Next.js Deployment](https://nextjs.org/docs/deployment)
- [Docker Compose Reference](https://docs.docker.com/compose/)
- [Prisma Docker Guide](https://www.prisma.io/docs/guides/deployment/docker)
- [Proxmox LXC Documentation](https://pve.proxmox.com/wiki/Linux_Container)
## 💡 Tips
1. **Use Volumes**: Always use Docker volumes for persistent data
2. **Health Checks**: Enable health checks for auto-restart
3. **Log Rotation**: Prevent disk filling with logs
4. **Monitoring**: Use Portainer or similar for easy management
5. **Testing**: Test in staging environment before production
---
**Need Help?** Check the main README or open an issue on GitHub.

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,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,459 +0,0 @@
# Résumé Complet de l'Implémentation Story 11-2 avec Méthode BMAD
Date: 2026-01-17
Projet: Keep - Application de notes
Story: 11-2 - Improve Settings UX
## 📋 Aperçu Général
La story 11-2 vise à améliorer l'UX des paramètres (Settings UX) en implémentant plusieurs fonctionnalités manquantes et en déployant les pages mises à jour.
## ✅ Ce Qui a Été Accompli
### 1. **Fonctionnalités Implémentées**
#### ✅ Serveur Actions Créés
- **`updateEmailNotifications(enabled: boolean)`** - Met à jour les notifications par email dans la table User
- **`updatePrivacyAnalytics(enabled: boolean)`** - Met à jour les analytics anonymes dans la table User
- **`updateTheme(theme: string)`** - Met à jour le thème de l'utilisateur (light/dark/auto)
- **`updateLanguage(language: string)`** - Met à jour la langue préférée
- **`updateFontSize(fontSize: string)`** - Met à jour la taille de police
- **`updateShowRecentNotes(showRecentNotes: boolean)`** - Met à jour l'affichage des notes récentes
**Fichier:** `keep-notes/app/actions/profile.ts`
#### ✅ Composant SettingsSearch Fonctionnel
- Filtrage en temps réel par label ET description
- Recherche insensible à la casse
- Bouton de réinitialisation (X)
- Support clavier (Escape pour effacer)
- État vide quand aucun résultat
- Accessibilité WCAG 2.1 AA
**Fichier:** `keep-notes/components/settings/SettingsSearch.tsx`
#### ✅ Mise à jour du Schéma de Base de Données
- Ajout de `emailNotifications` (Boolean) à la table User
- Ajout de `anonymousAnalytics` (Boolean) à la table User
- Migration SQL créée mais NON appliquée
**Fichier:** `keep-notes/prisma/schema.prisma`
**Migration:** `keep-notes/prisma/migrations/20260117000000_add_user_preferences_fields/migration.sql`
#### ✅ Pages de Paramètres Créées (mais NON déployées)
- **`page-new.tsx`** pour General Settings avec notifications et privacy
- **`profile-form-new.tsx`** pour Profile Settings avec toutes les fonctionnalités
- **`page-new.tsx`** pour Appearance Settings avec persistance du thème
**Emplacements:**
- `keep-notes/app/(main)/settings/general/page-new.tsx`
- `keep-notes/app/(main)/settings/profile/profile-form-new.tsx`
- `keep-notes/app/(main)/settings/appearance/page-new.tsx`
## 🚧 Étapes Restantes (Comment les Implémenter avec BMAD)
Voici les étapes restantes pour compléter l'implémentation avec la méthode BMAD :
### Étape 1: Régénérer le Client Prisma
**Problème:** Le build échoue avec "EPERM: operation not permitted" sur `query_engine-windows.dll.node`
**Solution BMAD:**
1. Arrêter tous les processus Node.js/Next.js en cours
2. Supprimer le dossier `prisma/client-generated`
3. Régénérer le client Prisma
```bash
# Dans le dossier keep-notes
# Arrêter tous les processus (Ctrl+C dans les terminaux)
# Supprimer le dossier client-generated
rm -rf prisma/client-generated
# Ou sur Windows: rmdir /s /q prisma\client-generated
# Régénérer le client Prisma
npx prisma generate
# Ou utiliser npm
npm run prisma:generate
```
### Étape 2: Appliquer la Migration de Base de Données
**Action:** Exécuter la migration SQL pour ajouter les nouveaux champs à la table User
```bash
# Dans le dossier keep-notes
npx prisma migrate deploy
# Ou appliquer manuellement la migration
# Ouvrir: prisma/migrations/20260117000000_add_user_preferences_fields/migration.sql
# Exécuter le SQL sur la base de données
```
**Contenu de la migration:**
```sql
ALTER TABLE "User" ADD COLUMN "emailNotifications" BOOLEAN NOT NULL DEFAULT 0;
ALTER TABLE "User" ADD COLUMN "anonymousAnalytics" BOOLEAN NOT NULL DEFAULT 1;
```
### Étape 3: Déployer les Pages de Paramètres (Méthode Safe Replace)
#### 3.1: Déployer General Settings Page
```bash
# Dans keep-notes/app/(main)/settings/general
# Backup du fichier actuel
mv page.tsx page-old.tsx
# Copier le nouveau fichier
cp page-new.tsx page.tsx
# Vérifier le build
cd ../../../..
npm run build
```
**Vérifications:**
- [ ] La page se charge correctement à `/settings/general`
- [ ] Les sections (Language, Notifications, Privacy) s'affichent
- [ ] Le toggle "Email Notifications" fonctionne
- [ ] Le toggle "Anonymous Analytics" fonctionne
- [ ] Les paramètres se sauvegardent dans la base de données
#### 3.2: Déployer Profile Settings Form
```bash
# Dans keep-notes/app/(main)/settings/profile
# Backup du fichier actuel
mv profile-form.tsx profile-form-old.tsx
# Copier le nouveau fichier
cp profile-form-new.tsx profile-form.tsx
# Vérifier le build
cd ../../../..
npm run build
```
**Vérifications:**
- [ ] Le formulaire de profil se charge correctement
- [ ] La langue peut être changée
- [ ] La taille de police peut être changée
- [ ] Le toggle "Show Recent Notes" fonctionne
- [ ] Les paramètres se sauvegardent correctement
#### 3.3: Déployer Appearance Settings Page
```bash
# Dans keep-notes/app/(main)/settings/appearance
# Backup du fichier actuel
mv page.tsx page-old.tsx
# Créer le nouveau fichier avec persistance du thème
# (Utiliser le contenu documenté dans cette section)
# Vérifier le build
cd ../../../..
npm run build
```
**Contenu à créer (page.tsx):**
```typescript
'use client'
import { useState, useEffect } from 'react'
import { toast } from 'sonner'
import { SettingsNav, SettingsSection, SettingSelect } from '@/components/settings'
import { useLanguage } from '@/lib/i18n'
import { updateTheme } from '@/app/actions/profile'
export default function AppearanceSettingsPage() {
const { t } = useLanguage()
const [theme, setTheme] = useState('auto')
// Load theme from database/localStorage on mount
useEffect(() => {
const loadTheme = async () => {
try {
const result = await updateTheme(localStorage.getItem('theme') || 'auto')
if (!result?.error) {
setTheme(localStorage.getItem('theme') || 'auto')
}
} catch (error) {
console.error('Failed to load theme:', error)
}
}
loadTheme()
}, [])
const handleThemeChange = async (value: string) => {
setTheme(value)
localStorage.setItem('theme', value)
applyTheme(value)
try {
const result = await updateTheme(value)
if (result?.error) {
toast.error(t('appearance.themeUpdateFailed'))
} else {
toast.success(t('appearance.themeUpdateSuccess'))
}
} catch (error) {
toast.error(error?.message || t('appearance.themeUpdateFailed'))
}
}
const applyTheme = (themeValue: string) => {
const root = document.documentElement
if (themeValue === 'dark') {
root.classList.add('dark')
root.classList.remove('light')
} else if (themeValue === 'light') {
root.classList.add('light')
root.classList.remove('dark')
} else {
// Auto: use system preference
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
if (prefersDark) {
root.classList.add('dark')
root.classList.remove('light')
} else {
root.classList.add('light')
root.classList.remove('dark')
}
}
}
return (
<div className="container mx-auto py-10 px-4 max-w-6xl">
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
<aside className="lg:col-span-1">
<SettingsNav currentSection="appearance" />
</aside>
<main className="lg:col-span-3 space-y-6">
<div>
<h1 className="text-3xl font-bold mb-2">{t('appearance.title')}</h1>
<p className="text-gray-600 dark:text-gray-400">
{t('appearance.description')}
</p>
</div>
<SettingsSection
title="Theme"
icon={<span className="text-2xl">🎨</span>}
description={t('appearance.themeDescription')}
>
<div className="space-y-4">
<label htmlFor="theme" className="text-sm font-medium">
{t('appearance.colorScheme')}
</label>
<SettingSelect
id="theme"
value={theme}
options={[
{ value: 'light', label: t('appearance.light') },
{ value: 'dark', label: t('appearance.dark') },
{ value: 'auto', label: t('appearance.auto') },
]}
onChange={handleThemeChange}
/>
<p className="text-sm text-muted-foreground">
{t('appearance.themeDescription')}
</p>
</div>
</SettingsSection>
</main>
</div>
</div>
)
}
```
**Vérifications:**
- [ ] La page se charge correctement à `/settings/appearance`
- [ ] Le thème se charge depuis localStorage au démarrage
- [ ] Le changement de thème fonctionne immédiatement
- [ ] Le thème se sauvegarde dans la base de données
- [ ] Le thème persiste après rechargement de la page
- [ ] Le thème persiste après fermeture/ouverture du navigateur
### Étape 4: Mettre à Jour le Sprint Status
Une fois toutes les fonctionnalités implémentées et testées, mettre à jour le fichier de statut:
```bash
# Ouvrir le fichier
_bmad-output/implementation-artifacts/sprint-status.yaml
# Mettre à jour le statut de la story 11-2
11-2-improve-settings-ux:
status: done # Passer de "in-progress" à "done"
completion: 100% # Passer de 60% à 100%
```
## 📊 Structure BMAD pour Chaque Étape
Chaque étape devrait suivre le format BMAD:
### Format de Story BMAD
```yaml
Story: X.Y - [Titre de la Story]
Status: [backlog | ready-for-dev | in-progress | review | done]
## Story
As a [type d'utilisateur],
I want [ce que je veux],
So that [pourquoi je veux cela].
## Acceptance Criteria
1. [Given] Condition préalable
2. [When] Action
3. [Then] Résultat attendu
## Tasks / Subtasks
- [ ] Tâche 1
- [ ] Sous-tâche 1.1
- [ ] Sous-tâche 1.2
- [ ] Tâche 2
- [ ] Sous-tâche 2.1
- [ ] Sous-tâche 2.2
## Dev Notes
### Implementation Context
[Explication du contexte et fichiers impliqués]
### Deployment Strategy
[Stratégie de déploiement]
### Testing Requirements
[Conditions de test à vérifier]
### References
[Liens vers les fichiers pertinents]
```
## 📝 Résumé des Stories BMAD Créées
### ✅ Story 11.2.1: Deploy General Settings Page Update
- **Statut:** backlog (prête à être exécutée)
- **Objectif:** Déployer la page General Settings avec notifications et privacy
- **Fichiers:** `keep-notes/app/(main)/settings/general/`
### ✅ Story 11.2.2: Implement Functional SettingsSearch Component
- **Statut:** backlog (complétée mais NON déployée)
- **Objectif:** Implémenter la recherche fonctionnelle dans SettingsSearch
- **Fichiers:** `keep-notes/components/settings/SettingsSearch.tsx`
### ✅ Story 11.2.3: Deploy Appearance Settings Page Update
- **Statut:** backlog (prête à être exécutée)
- **Objectif:** Déployer la page Appearance Settings avec persistance du thème
- **Fichiers:** `keep-notes/app/(main)/settings/appearance/`
### ✅ Story 11.2.4: Deploy Profile Settings Form Update (À créer)
- **Statut:** backlog (à créer)
- **Objectif:** Déployer le formulaire de profil avec toutes les fonctionnalités
- **Fichiers:** `keep-notes/app/(main)/settings/profile/profile-form.tsx`
### ✅ Story 11.2.5: Update Story 11-2 to Done Status (À créer)
- **Statut:** backlog (à créer)
- **Objectif:** Mettre à jour le statut de la story 11-2 à "done"
- **Fichiers:** `_bmad-output/implementation-artifacts/sprint-status.yaml`
## 🎯 Checklist Finale de Déploiement
Avant de marquer la story 11-2 comme "done", vérifier:
### Vérifications Techniques
- [ ] Build réussi sans erreurs (`npm run build`)
- [ ] Pas d'erreurs TypeScript
- [ ] Pas d'erreurs de linting (`npm run lint`)
- [ ] Client Prisma régénéré avec succès
- [ ] Migration de base de données appliquée
### Vérifications Fonctionnelles
- [ ] Page General Settings fonctionne
- [ ] Toggle "Email Notifications" fonctionne et sauvegarde
- [ ] Toggle "Anonymous Analytics" fonctionne et sauvegarde
- [ ] Page Profile Settings fonctionne
- [ ] Changement de langue fonctionne et sauvegarde
- [ ] Changement de taille de police fonctionne et sauvegarde
- [ ] Toggle "Show Recent Notes" fonctionne et sauvegarde
- [ ] Page Appearance Settings fonctionne
- [ ] Changement de thème fonctionne immédiatement
- [ ] Thème se sauvegarde dans la base de données
- [ ] Thème se charge depuis localStorage au démarrage
- [ ] Thème persiste après rechargement de page
- [ ] Thème persiste après fermeture/ouverture du navigateur
### Vérifications UI/UX
- [ ] SettingsSearch filtre correctement les sections
- [ ] Recherche fonctionne par label ET description
- [ ] Recherche est insensible à la casse
- [ ] Bouton de réinitialisation (X) fonctionne
- [ ] Escape key efface la recherche
- [ ] État vide s'affiche quand aucun résultat
- [ ] Toast notifications s'affichent pour succès/erreur
- [ ] Design responsive sur mobile/tablet/desktop
### Vérifications d'Accessibilité
- [ ] Navigation au clavier fonctionne
- [ ] Focus visible sur les éléments interactifs
- [ ] ARIA labels présents
- [ ] Contraste de couleurs conforme WCAG AA
- [ ] Screen readers peuvent lire les labels
## 🚀 Comment Continuer
### Option 1: Suivre les Étapes Manuellement
1. Résoudre le problème Prisma (Étape 1)
2. Appliquer la migration (Étape 2)
3. Déployer les pages une par une (Étape 3)
4. Vérifier toutes les fonctionnalités (Checklist)
5. Mettre à jour le statut (Étape 4)
### Option 2: Exécuter une Story BMAD
Créer et exécuter les stories BMAD restantes en suivant le format:
```bash
# Exemple pour créer une story
_bmad/workflows/4-implementation/dev-story/workflow.yaml
# Spécifier:
# - La story à exécuter (ex: 11-2-3-deploy-appearance-settings-page)
# - L'étape (ex: deployment)
# - Les fichiers à modifier
```
## 📚 Références
**Fichiers Implémentés:**
- `keep-notes/app/actions/profile.ts` - Server actions
- `keep-notes/components/settings/SettingsSearch.tsx` - Composant de recherche
- `keep-notes/app/(main)/settings/general/page-new.tsx` - Page General (non déployée)
- `keep-notes/app/(main)/settings/profile/profile-form-new.tsx` - Profile form (non déployé)
- `keep-notes/app/(main)/settings/appearance/page.tsx` - Page Appearance (à créer)
- `keep-notes/prisma/schema.prisma` - Schéma de base de données
- `keep-notes/prisma/migrations/20260117000000_add_user_preferences_fields/migration.sql` - Migration
**Documentation BMAD:**
- `_bmad-output/implementation-artifacts/11-2-improve-settings-ux.md` - Story principale
- `_bmad-output/implementation-artifacts/11-2-1-deploy-general-settings-page.md` - Story déployement General
- `_bmad-output/implementation-artifacts/11-2-2-implement-functional-settingssearch.md` - Story SettingsSearch
- `_bmad-output/implementation-artifacts/11-2-3-deploy-appearance-settings-page.md` - Story déployement Appearance
## 🎉 Conclusion
L'implémentation de la story 11-2 est à **80% complète**. Les fonctionnalités principales ont été créées et testées localement. Les étapes restantes sont principalement des tâches de déploiement et de vérification.
**Prochaines Actions Prioritaires:**
1. Résoudre le problème Prisma (permissions)
2. Régénérer le client Prisma
3. Appliquer la migration de base de données
4. Déployer les pages de paramètres
5. Vérifier toutes les fonctionnalités
6. Mettre à jour le statut à "done"
La méthode BMAD a été appliquée pour documenter chaque étape avec des critères d'acceptation, des notes d'implémentation et des exigences de tests. Chaque story peut être exécutée indépendamment en suivant le format standard BMAD.

Binary file not shown.

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,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,92 +0,0 @@
/**
* Playwright test script to diagnose MasonryGrid hot reload issue
*/
const { chromium } = require('playwright');
async function testMasonryLayout() {
console.log('🔍 Starting Masonry layout diagnosis...');
const browser = await chromium.launch({
headless: false, // Show browser for visual inspection
slowMo: 50, // Slow down actions for better visibility
});
const context = await browser.newContext({
viewport: { width: 1920, height: 1080 }
});
const page = await context.newPage();
// Navigate to the app
await page.goto('http://localhost:3000');
// Wait for page to load
await page.waitForTimeout(3000);
// Take screenshot of initial state
await page.screenshot({ path: 'masonry-before.png', fullPage: true });
console.log('📸 Screenshot saved: masonry-before.png');
// Check DOM for MasonryItem elements
const masonryItems = await page.$$eval('.masonry-item', (items) => {
return items.map(item => ({
hasDataSize: item.hasAttribute('data-size'),
dataSize: item.getAttribute('data-size'),
hasStyle: item.hasAttribute('style'),
style: item.getAttribute('style'),
className: item.className,
computedWidth: window.getComputedStyle(item).width,
}));
});
console.log('🎯 MasonryItem analysis:');
console.table(masonryItems);
// Check which items have inline styles
const itemsWithInlineStyle = masonryItems.filter(item => item.hasStyle);
console.log(`✓ Items with inline style attribute: ${itemsWithInlineStyle.length}/${masonryItems.length}`);
// Check which items have data-size attribute
const itemsWithDataSize = masonryItems.filter(item => item.hasDataSize);
console.log(`✓ Items with data-size attribute: ${itemsWithDataSize.length}/${masonryItems.length}`);
// Analyze computed widths
const uniqueWidths = [...new Set(masonryItems.map(item => item.computedWidth))];
console.log(`📏 Unique computed widths found:`, uniqueWidths);
// Check if Muuri is initialized
const muuriStatus = await page.evaluate(() => {
const gridElement = document.querySelector('.muuri-item');
if (!gridElement) return 'No Muuri grid found';
const gridRect = gridElement.getBoundingClientRect();
const muuriContainer = gridElement.parentElement;
if (muuriContainer) {
const containerRect = muuriContainer.getBoundingClientRect();
return {
gridRect,
containerRect,
isMuuriInitialized: true,
};
}
return 'Muuri not initialized';
});
console.log('\n🧩 Muuri grid status:', muuriStatus);
// Take second screenshot after analysis
await page.screenshot({ path: 'masonry-after-analysis.png', fullPage: true });
console.log('📸 Screenshot saved: masonry-after-analysis.png');
// Keep browser open for manual inspection
console.log('\n⏳ Keeping browser open for 30 seconds for manual inspection...');
await page.waitForTimeout(30000);
await browser.close();
console.log('✅ Test complete');
}
testMasonryLayout().catch(console.error);

Binary file not shown.

View File

@@ -1,20 +0,0 @@
-- CreateTable
CREATE TABLE "Note" (
"id" TEXT NOT NULL PRIMARY KEY,
"title" TEXT,
"content" TEXT NOT NULL,
"color" TEXT NOT NULL DEFAULT 'default',
"isPinned" BOOLEAN NOT NULL DEFAULT false,
"isArchived" BOOLEAN NOT NULL DEFAULT false,
"type" TEXT NOT NULL DEFAULT 'text',
"checkItems" JSONB,
"labels" JSONB,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
-- CreateIndex
CREATE INDEX "Note_isPinned_idx" ON "Note"("isPinned");
-- CreateIndex
CREATE INDEX "Note_isArchived_idx" ON "Note"("isArchived");

View File

@@ -1,23 +0,0 @@
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Note" (
"id" TEXT NOT NULL PRIMARY KEY,
"title" TEXT,
"content" TEXT NOT NULL,
"color" TEXT NOT NULL DEFAULT 'default',
"isPinned" BOOLEAN NOT NULL DEFAULT false,
"isArchived" BOOLEAN NOT NULL DEFAULT false,
"type" TEXT NOT NULL DEFAULT 'text',
"checkItems" TEXT,
"labels" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
INSERT INTO "new_Note" ("checkItems", "color", "content", "createdAt", "id", "isArchived", "isPinned", "labels", "title", "type", "updatedAt") SELECT "checkItems", "color", "content", "createdAt", "id", "isArchived", "isPinned", "labels", "title", "type", "updatedAt" FROM "Note";
DROP TABLE "Note";
ALTER TABLE "new_Note" RENAME TO "Note";
CREATE INDEX "Note_isPinned_idx" ON "Note"("isPinned");
CREATE INDEX "Note_isArchived_idx" ON "Note"("isArchived");
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@@ -1,25 +0,0 @@
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Note" (
"id" TEXT NOT NULL PRIMARY KEY,
"title" TEXT,
"content" TEXT NOT NULL,
"color" TEXT NOT NULL DEFAULT 'default',
"isPinned" BOOLEAN NOT NULL DEFAULT false,
"isArchived" BOOLEAN NOT NULL DEFAULT false,
"type" TEXT NOT NULL DEFAULT 'text',
"checkItems" TEXT,
"labels" TEXT,
"order" INTEGER NOT NULL DEFAULT 0,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
INSERT INTO "new_Note" ("checkItems", "color", "content", "createdAt", "id", "isArchived", "isPinned", "labels", "title", "type", "updatedAt") SELECT "checkItems", "color", "content", "createdAt", "id", "isArchived", "isPinned", "labels", "title", "type", "updatedAt" FROM "Note";
DROP TABLE "Note";
ALTER TABLE "new_Note" RENAME TO "Note";
CREATE INDEX "Note_isPinned_idx" ON "Note"("isPinned");
CREATE INDEX "Note_isArchived_idx" ON "Note"("isArchived");
CREATE INDEX "Note_order_idx" ON "Note"("order");
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "Note" ADD COLUMN "images" TEXT;

View File

@@ -1,5 +0,0 @@
-- AlterTable
ALTER TABLE "Note" ADD COLUMN "reminder" DATETIME;
-- CreateIndex
CREATE INDEX "Note_reminder_idx" ON "Note"("reminder");

View File

@@ -1,29 +0,0 @@
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Note" (
"id" TEXT NOT NULL PRIMARY KEY,
"title" TEXT,
"content" TEXT NOT NULL,
"color" TEXT NOT NULL DEFAULT 'default',
"isPinned" BOOLEAN NOT NULL DEFAULT false,
"isArchived" BOOLEAN NOT NULL DEFAULT false,
"type" TEXT NOT NULL DEFAULT 'text',
"checkItems" TEXT,
"labels" TEXT,
"images" TEXT,
"reminder" DATETIME,
"isMarkdown" BOOLEAN NOT NULL DEFAULT false,
"order" INTEGER NOT NULL DEFAULT 0,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
INSERT INTO "new_Note" ("checkItems", "color", "content", "createdAt", "id", "images", "isArchived", "isPinned", "labels", "order", "reminder", "title", "type", "updatedAt") SELECT "checkItems", "color", "content", "createdAt", "id", "images", "isArchived", "isPinned", "labels", "order", "reminder", "title", "type", "updatedAt" FROM "Note";
DROP TABLE "Note";
ALTER TABLE "new_Note" RENAME TO "Note";
CREATE INDEX "Note_isPinned_idx" ON "Note"("isPinned");
CREATE INDEX "Note_isArchived_idx" ON "Note"("isArchived");
CREATE INDEX "Note_order_idx" ON "Note"("order");
CREATE INDEX "Note_reminder_idx" ON "Note"("reminder");
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@@ -1,3 +0,0 @@
-- AlterTable
ALTER TABLE "Note" ADD COLUMN "reminderLocation" TEXT;
ALTER TABLE "Note" ADD COLUMN "reminderRecurrence" TEXT;

View File

@@ -1,90 +0,0 @@
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT,
"email" TEXT NOT NULL,
"emailVerified" DATETIME,
"image" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
-- CreateTable
CREATE TABLE "Account" (
"userId" TEXT NOT NULL,
"type" TEXT NOT NULL,
"provider" TEXT NOT NULL,
"providerAccountId" TEXT NOT NULL,
"refresh_token" TEXT,
"access_token" TEXT,
"expires_at" INTEGER,
"token_type" TEXT,
"scope" TEXT,
"id_token" TEXT,
"session_state" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
PRIMARY KEY ("provider", "providerAccountId"),
CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "Session" (
"sessionToken" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"expires" DATETIME NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "VerificationToken" (
"identifier" TEXT NOT NULL,
"token" TEXT NOT NULL,
"expires" DATETIME NOT NULL,
PRIMARY KEY ("identifier", "token")
);
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Note" (
"id" TEXT NOT NULL PRIMARY KEY,
"title" TEXT,
"content" TEXT NOT NULL,
"color" TEXT NOT NULL DEFAULT 'default',
"isPinned" BOOLEAN NOT NULL DEFAULT false,
"isArchived" BOOLEAN NOT NULL DEFAULT false,
"type" TEXT NOT NULL DEFAULT 'text',
"checkItems" TEXT,
"labels" TEXT,
"images" TEXT,
"reminder" DATETIME,
"reminderRecurrence" TEXT,
"reminderLocation" TEXT,
"isMarkdown" BOOLEAN NOT NULL DEFAULT false,
"userId" TEXT,
"order" INTEGER NOT NULL DEFAULT 0,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "Note_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
INSERT INTO "new_Note" ("checkItems", "color", "content", "createdAt", "id", "images", "isArchived", "isMarkdown", "isPinned", "labels", "order", "reminder", "reminderLocation", "reminderRecurrence", "title", "type", "updatedAt") SELECT "checkItems", "color", "content", "createdAt", "id", "images", "isArchived", "isMarkdown", "isPinned", "labels", "order", "reminder", "reminderLocation", "reminderRecurrence", "title", "type", "updatedAt" FROM "Note";
DROP TABLE "Note";
ALTER TABLE "new_Note" RENAME TO "Note";
CREATE INDEX "Note_isPinned_idx" ON "Note"("isPinned");
CREATE INDEX "Note_isArchived_idx" ON "Note"("isArchived");
CREATE INDEX "Note_order_idx" ON "Note"("order");
CREATE INDEX "Note_reminder_idx" ON "Note"("reminder");
CREATE INDEX "Note_userId_idx" ON "Note"("userId");
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-- CreateIndex
CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken");

View File

@@ -1,16 +0,0 @@
-- CreateTable
CREATE TABLE "Label" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"color" TEXT NOT NULL DEFAULT 'gray',
"userId" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "Label_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "Label_name_key" ON "Label"("name");
-- CreateIndex
CREATE INDEX "Label_userId_idx" ON "Label"("userId");

View File

@@ -1,35 +0,0 @@
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Note" (
"id" TEXT NOT NULL PRIMARY KEY,
"title" TEXT,
"content" TEXT NOT NULL,
"color" TEXT NOT NULL DEFAULT 'default',
"isPinned" BOOLEAN NOT NULL DEFAULT false,
"isArchived" BOOLEAN NOT NULL DEFAULT false,
"type" TEXT NOT NULL DEFAULT 'text',
"checkItems" TEXT,
"labels" TEXT,
"images" TEXT,
"reminder" DATETIME,
"isReminderDone" BOOLEAN NOT NULL DEFAULT false,
"reminderRecurrence" TEXT,
"reminderLocation" TEXT,
"isMarkdown" BOOLEAN NOT NULL DEFAULT false,
"userId" TEXT,
"order" INTEGER NOT NULL DEFAULT 0,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "Note_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
INSERT INTO "new_Note" ("checkItems", "color", "content", "createdAt", "id", "images", "isArchived", "isMarkdown", "isPinned", "labels", "order", "reminder", "reminderLocation", "reminderRecurrence", "title", "type", "updatedAt", "userId") SELECT "checkItems", "color", "content", "createdAt", "id", "images", "isArchived", "isMarkdown", "isPinned", "labels", "order", "reminder", "reminderLocation", "reminderRecurrence", "title", "type", "updatedAt", "userId" FROM "Note";
DROP TABLE "Note";
ALTER TABLE "new_Note" RENAME TO "Note";
CREATE INDEX "Note_isPinned_idx" ON "Note"("isPinned");
CREATE INDEX "Note_isArchived_idx" ON "Note"("isArchived");
CREATE INDEX "Note_order_idx" ON "Note"("order");
CREATE INDEX "Note_reminder_idx" ON "Note"("reminder");
CREATE INDEX "Note_userId_idx" ON "Note"("userId");
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "Note" ADD COLUMN "links" TEXT;

View File

@@ -1,14 +0,0 @@
/*
Warnings:
- A unique constraint covering the columns `[name,userId]` on the table `Label` will be added. If there are existing duplicate values, this will fail.
*/
-- DropIndex
DROP INDEX "Label_name_key";
-- AlterTable
ALTER TABLE "User" ADD COLUMN "password" TEXT;
-- CreateIndex
CREATE UNIQUE INDEX "Label_name_userId_key" ON "Label"("name", "userId");

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "Note" ADD COLUMN "embedding" TEXT;

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "UserAISettings" ADD COLUMN "showRecentNotes" BOOLEAN NOT NULL DEFAULT false;

View File

@@ -1,7 +0,0 @@
-- AlterTable
ALTER TABLE "Note" ADD COLUMN "autoGenerated" BOOLEAN;
ALTER TABLE "Note" ADD COLUMN "aiProvider" TEXT;
ALTER TABLE "Note" ADD COLUMN "aiConfidence" INTEGER;
ALTER TABLE "Note" ADD COLUMN "language" TEXT;
ALTER TABLE "Note" ADD COLUMN "languageConfidence" REAL;
ALTER TABLE "Note" ADD COLUMN "lastAiAnalysis" DATETIME;

View File

@@ -1,26 +0,0 @@
-- CreateTable
CREATE TABLE "AiFeedback" (
"id" TEXT NOT NULL PRIMARY KEY,
"noteId" TEXT NOT NULL,
"userId" TEXT,
"feedbackType" TEXT NOT NULL,
"feature" TEXT NOT NULL,
"originalContent" TEXT NOT NULL,
"correctedContent" TEXT,
"metadata" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY ("noteId") REFERENCES "Note"("id") ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateIndex
CREATE INDEX "AiFeedback_noteId_idx" ON "AiFeedback"("noteId");
-- CreateIndex
CREATE INDEX "AiFeedback_userId_idx" ON "AiFeedback"("userId");
-- CreateIndex
CREATE INDEX "AiFeedback_feature_idx" ON "AiFeedback"("feature");
-- CreateIndex
CREATE INDEX "AiFeedback_createdAt_idx" ON "AiFeedback"("createdAt");

View File

@@ -1,4 +0,0 @@
-- AlterTable
ALTER TABLE "UserAISettings" ADD COLUMN "emailNotifications" BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE "UserAISettings" ADD COLUMN "desktopNotifications" BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE "UserAISettings" ADD COLUMN "anonymousAnalytics" BOOLEAN NOT NULL DEFAULT false;

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"}