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:
@@ -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.
|
||||
@@ -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 !** 🎨🚀
|
||||
@@ -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* 💻
|
||||
@@ -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* 💻
|
||||
@@ -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 ?** 🚀
|
||||
@@ -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* 💻
|
||||
@@ -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>
|
||||
@@ -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.
@@ -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[@]}"
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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.
@@ -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");
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Note" ADD COLUMN "images" TEXT;
|
||||
@@ -1,5 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Note" ADD COLUMN "reminder" DATETIME;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Note_reminder_idx" ON "Note"("reminder");
|
||||
@@ -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;
|
||||
@@ -1,3 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Note" ADD COLUMN "reminderLocation" TEXT;
|
||||
ALTER TABLE "Note" ADD COLUMN "reminderRecurrence" TEXT;
|
||||
@@ -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");
|
||||
@@ -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");
|
||||
@@ -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;
|
||||
@@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Note" ADD COLUMN "links" TEXT;
|
||||
@@ -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");
|
||||
@@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Note" ADD COLUMN "embedding" TEXT;
|
||||
@@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "UserAISettings" ADD COLUMN "showRecentNotes" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -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;
|
||||
@@ -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");
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
{"content":"This is a test note about artificial intelligence and machine learning in Python"}
|
||||
@@ -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();
|
||||
@@ -1 +0,0 @@
|
||||
{"text":"This is a test paragraph that needs to be rewritten. It contains multiple sentences and should be improved.","mode":"clarify"}
|
||||
Reference in New Issue
Block a user