diff --git a/MCP-GUIDE.md b/MCP-GUIDE.md new file mode 100644 index 0000000..460bd79 --- /dev/null +++ b/MCP-GUIDE.md @@ -0,0 +1,760 @@ +# Guide Complet MCP (Model Context Protocol) + +## 📘 Table des MatiĂšres + +1. [Introduction au MCP](#introduction) +2. [Architecture du Serveur](#architecture) +3. [Configuration et Installation](#configuration) +4. [Utilisation avec N8N](#utilisation-n8n) +5. [API Endpoints](#api-endpoints) +6. [Exemples de RequĂȘtes](#exemples) +7. [Outils Disponibles](#outils) +8. [Troubleshooting](#troubleshooting) + +--- + +## 1. Introduction au MCP {#introduction} + +Le **Model Context Protocol (MCP)** est un protocole standardisĂ© permettant aux modĂšles de langage (LLMs) d'interagir avec des applications externes via des outils structurĂ©s. + +### Qu'est-ce que MCP ? + +- **Protocol Version**: 2025-06-18 +- **Transport**: Streamable HTTP (remplace l'ancien HTTP+SSE) +- **Format**: JSON-RPC 2.0 +- **Architecture**: Client-Serveur avec session management + +### Pourquoi utiliser MCP ? + +- ✅ Communication standardisĂ©e entre LLMs et applications +- ✅ Outils typĂ©s avec validation de schĂ©ma +- ✅ Support des sessions et de la reconnexion +- ✅ Compatible avec N8N, Claude Desktop, et autres clients MCP + +--- + +## 2. Architecture du Serveur {#architecture} + +### Structure du Projet + +``` +Keep/ +├── mcp-server/ +│ ├── index-sse.js # Serveur MCP principal +│ ├── package.json # DĂ©pendances MCP SDK +│ └── start-sse.ps1 # Script de dĂ©marrage +├── keep-notes/ +│ ├── prisma/ +│ │ └── dev.db # Base de donnĂ©es SQLite +│ └── ... # Application Next.js +└── MCP-GUIDE.md # Ce guide +``` + +### Composants ClĂ©s + +#### 1. **Serveur MCP** (`index-sse.js`) +- Port: **3001** +- Endpoint principal: `/sse` +- Base de donnĂ©es: Prisma + SQLite partagĂ©e avec keep-notes +- Transport: `StreamableHTTPServerTransport` + +#### 2. **Serveur Next.js** (`keep-notes`) +- Port: **3000** +- Interface utilisateur web +- Partage la mĂȘme base de donnĂ©es que MCP + +#### 3. **Base de donnĂ©es Prisma** +```prisma +model Note { + id String @id @default(uuid()) + title String? + content String + type String @default("text") + color String @default("default") + checkItems String? // JSON + labels String? // JSON + images String? // JSON + isPinned Boolean @default(false) + isArchived Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} +``` + +--- + +## 3. Configuration et Installation {#configuration} + +### PrĂ©requis + +- Node.js 18+ +- npm ou pnpm +- AccĂšs rĂ©seau sur ports 3000 et 3001 + +### Installation + +```bash +# 1. Installer les dĂ©pendances MCP +cd mcp-server +npm install + +# 2. VĂ©rifier Prisma Client +cd ../keep-notes +npx prisma generate +``` + +### DĂ©marrage + +#### Serveur MCP +```powershell +# Option 1: Script PowerShell +cd mcp-server +.\start-sse.ps1 + +# Option 2: Commande directe +node index-sse.js +``` + +#### Serveur Next.js +```bash +cd keep-notes +npm run dev +``` + +### VĂ©rification + +```powershell +# Tester le serveur MCP +Invoke-RestMethod -Uri "http://localhost:3001/" | ConvertTo-Json + +# RĂ©sultat attendu: +{ + "name": "Memento MCP SSE Server", + "version": "1.0.0", + "status": "running", + "endpoints": { + "sse": "/sse", + "message": "/message" + } +} +``` + +--- + +## 4. Utilisation avec N8N {#utilisation-n8n} + +### Configuration du NƓud MCP Client + +#### Étape 1: Ajouter le nƓud +1. Glisser-dĂ©poser **"MCP Client"** dans le workflow +2. SĂ©lectionner **"HTTP Streamable"** comme transport +3. Configurer l'endpoint + +#### Étape 2: Configuration de base + +```yaml +Server Transport: HTTP Streamable +MCP Endpoint URL: http://192.168.1.10:3001/sse +Authentication: None +``` + +⚠ **Important**: Utiliser l'IP locale correcte, pas `192.168.110` ou `127.0.0.1` + +#### Étape 3: DĂ©tecter l'IP locale + +```powershell +# Windows +ipconfig + +# Chercher "Adresse IPv4" pour votre adaptateur rĂ©seau +# Exemple: 192.168.1.10, 172.26.64.1, etc. +``` + +#### Étape 4: SĂ©lectionner un outil + +Une fois connectĂ©, N8N charge automatiquement la liste des 9 outils disponibles: +- `create_note` +- `get_notes` +- `get_note` +- `update_note` +- `delete_note` +- `search_notes` +- `get_labels` +- `toggle_pin` +- `toggle_archive` + +--- + +## 5. API Endpoints {#api-endpoints} + +### Health Check + +**GET** `/` + +VĂ©rifier l'Ă©tat du serveur. + +```bash +curl http://localhost:3001/ +``` + +**RĂ©ponse:** +```json +{ + "name": "Memento MCP SSE Server", + "version": "1.0.0", + "status": "running", + "endpoints": { + "sse": "/sse", + "message": "/message" + } +} +``` + +### MCP Endpoint + +**GET/POST** `/sse` + +Endpoint principal pour toutes les communications MCP. + +#### Initialisation (POST) + +```bash +curl -X POST http://localhost:3001/sse \ + -H "Content-Type: application/json" \ + -H "Accept: text/event-stream" \ + -d '{ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-06-18", + "capabilities": {}, + "clientInfo": { + "name": "n8n-mcp-client", + "version": "1.0.0" + } + } + }' +``` + +**RĂ©ponse SSE Stream:** +``` +event: message +data: {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-06-18","capabilities":{"tools":{}},"serverInfo":{"name":"memento-mcp-server","version":"1.0.0"}}} + +event: message +data: {"jsonrpc":"2.0","method":"initialized"} +``` + +#### Stream SSE (GET) + +```bash +curl -H "Accept: text/event-stream" \ + -H "Mcp-Session-Id: YOUR_SESSION_ID" \ + http://localhost:3001/sse +``` + +--- + +## 6. Exemples de RequĂȘtes {#exemples} + +### Liste des Outils + +```json +POST /sse +Content-Type: application/json +Mcp-Session-Id: abc123... + +{ + "jsonrpc": "2.0", + "id": 2, + "method": "tools/list", + "params": {} +} +``` + +**RĂ©ponse:** +```json +{ + "jsonrpc": "2.0", + "id": 2, + "result": { + "tools": [ + { + "name": "create_note", + "description": "Create a new note in Memento", + "inputSchema": { + "type": "object", + "properties": { + "title": { "type": "string" }, + "content": { "type": "string" }, + "color": { "type": "string", "default": "default" } + }, + "required": ["content"] + } + } + // ... 8 autres outils + ] + } +} +``` + +### CrĂ©er une Note + +```json +POST /sse +Content-Type: application/json +Mcp-Session-Id: abc123... + +{ + "jsonrpc": "2.0", + "id": 3, + "method": "tools/call", + "params": { + "name": "create_note", + "arguments": { + "title": "Ma premiĂšre note via MCP", + "content": "Contenu de ma note créée depuis N8N", + "color": "blue", + "labels": ["mcp", "test"] + } + } +} +``` + +**RĂ©ponse:** +```json +{ + "jsonrpc": "2.0", + "id": 3, + "result": { + "content": [ + { + "type": "text", + "text": "{\"id\":\"uuid-123\",\"title\":\"Ma premiĂšre note via MCP\",\"content\":\"Contenu...\",\"color\":\"blue\",\"labels\":[\"mcp\",\"test\"],\"createdAt\":\"2026-01-04T...\"}" + } + ] + } +} +``` + +### RĂ©cupĂ©rer Toutes les Notes + +```json +POST /sse +Content-Type: application/json +Mcp-Session-Id: abc123... + +{ + "jsonrpc": "2.0", + "id": 4, + "method": "tools/call", + "params": { + "name": "get_notes", + "arguments": {} + } +} +``` + +### Rechercher des Notes + +```json +POST /sse + +{ + "jsonrpc": "2.0", + "id": 5, + "method": "tools/call", + "params": { + "name": "search_notes", + "arguments": { + "query": "rĂ©union" + } + } +} +``` + +### Épingler/DĂ©sĂ©pingler une Note + +```json +POST /sse + +{ + "jsonrpc": "2.0", + "id": 6, + "method": "tools/call", + "params": { + "name": "toggle_pin", + "arguments": { + "id": "uuid-123" + } + } +} +``` + +--- + +## 7. Outils Disponibles {#outils} + +### 1. `create_note` + +CrĂ©er une nouvelle note. + +**ParamĂštres:** +- `title` (string, optionnel) - Titre de la note +- `content` (string, **requis**) - Contenu de la note +- `color` (string) - Couleur parmi: default, red, orange, yellow, green, teal, blue, purple, pink, gray +- `type` (string) - Type: "text" ou "checklist" +- `checkItems` (array) - Items de checklist (si type=checklist) +- `labels` (array[string]) - Tags/labels +- `isPinned` (boolean) - Épingler la note +- `isArchived` (boolean) - Archiver la note +- `images` (array[string]) - Images base64 + +**Exemple:** +```json +{ + "title": "Liste de courses", + "content": "", + "type": "checklist", + "checkItems": [ + {"id": "1", "text": "Lait", "checked": false}, + {"id": "2", "text": "Pain", "checked": false} + ], + "color": "yellow", + "labels": ["shopping"] +} +``` + +### 2. `get_notes` + +RĂ©cupĂ©rer toutes les notes non archivĂ©es. + +**ParamĂštres:** Aucun + +**Retour:** Array de notes + +### 3. `get_note` + +RĂ©cupĂ©rer une note spĂ©cifique par ID. + +**ParamĂštres:** +- `id` (string, **requis**) - UUID de la note + +### 4. `update_note` + +Mettre Ă  jour une note existante. + +**ParamĂštres:** +- `id` (string, **requis**) - UUID de la note +- `title` (string, optionnel) - Nouveau titre +- `content` (string, optionnel) - Nouveau contenu +- `color` (string, optionnel) - Nouvelle couleur +- `checkItems` (array, optionnel) - Nouveaux items +- `labels` (array, optionnel) - Nouveaux labels +- `images` (array, optionnel) - Nouvelles images + +### 5. `delete_note` + +Supprimer dĂ©finitivement une note. + +**ParamĂštres:** +- `id` (string, **requis**) - UUID de la note + +⚠ **Attention:** Suppression irrĂ©versible + +### 6. `search_notes` + +Rechercher des notes par mots-clĂ©s. + +**ParamĂštres:** +- `query` (string, **requis**) - Texte Ă  rechercher + +**Recherche dans:** +- Titres +- Contenus +- Labels +- Items de checklist + +**Exemple:** +```json +{ + "query": "rĂ©union 2026" +} +``` + +### 7. `get_labels` + +RĂ©cupĂ©rer tous les labels uniques utilisĂ©s. + +**ParamĂštres:** Aucun + +**Retour:** +```json +{ + "content": [ + { + "type": "text", + "text": "[\"work\",\"personal\",\"urgent\",\"mcp\"]" + } + ] +} +``` + +### 8. `toggle_pin` + +Épingler/dĂ©sĂ©pingler une note. + +**ParamĂštres:** +- `id` (string, **requis**) - UUID de la note + +**Comportement:** Si Ă©pinglĂ©e → dĂ©sĂ©pingle, si non Ă©pinglĂ©e → Ă©pingle + +### 9. `toggle_archive` + +Archiver/dĂ©sarchiver une note. + +**ParamĂštres:** +- `id` (string, **requis**) - UUID de la note + +**Comportement:** Si archivĂ©e → dĂ©sarchive, si non archivĂ©e → archive + +--- + +## 8. Troubleshooting {#troubleshooting} + +### ❌ "Could not connect to your MCP server" + +**Causes possibles:** +1. Serveur MCP non dĂ©marrĂ© +2. IP incorrecte dans N8N +3. Firewall bloque le port 3001 + +**Solutions:** + +```powershell +# 1. VĂ©rifier si le serveur tourne +Get-Process -Name node | Where-Object { + (Get-NetTCPConnection -OwningProcess $_.Id -ErrorAction SilentlyContinue).LocalPort -eq 3001 +} + +# 2. DĂ©tecter votre IP +ipconfig | Select-String "IPv4" + +# 3. Tester la connexion +Invoke-RestMethod -Uri "http://localhost:3001/" + +# 4. Tester depuis l'IP rĂ©seau +Invoke-RestMethod -Uri "http://192.168.1.10:3001/" +``` + +### ❌ "Could not load list" + +**Cause:** Serveur MCP ne rĂ©pond pas correctement au protocole Streamable HTTP + +**Solution:** + +1. VĂ©rifier la version du SDK: +```bash +cd mcp-server +npm list @modelcontextprotocol/sdk +# Doit ĂȘtre >= 1.0.4 +``` + +2. RedĂ©marrer le serveur: +```powershell +# Tuer tous les processus node +Get-Process -Name node | Stop-Process -Force + +# Relancer +cd mcp-server +node index-sse.js +``` + +### ❌ Port 3001 dĂ©jĂ  utilisĂ© + +```powershell +# Trouver le processus +Get-Process -Name node | Where-Object { + (Get-NetTCPConnection -OwningProcess $_.Id).LocalPort -eq 3001 +} | Stop-Process -Force +``` + +### ❌ Base de donnĂ©es verrouillĂ©e + +**Erreur:** `SQLITE_BUSY: database is locked` + +**Solution:** + +```powershell +# ArrĂȘter tous les serveurs +Get-Process -Name node | Stop-Process -Force + +# VĂ©rifier qu'aucun processus n'accĂšde Ă  la DB +lsof keep-notes/prisma/dev.db # Linux/Mac +handle dev.db # Windows + +# RedĂ©marrer les serveurs +``` + +### ❌ "Invalid session ID" + +**Cause:** Session expirĂ©e ou non initialisĂ©e + +**Solution:** Relancer la connexion depuis N8N (bouton "Execute Node") + +### 🔍 Logs de DĂ©bogage + +Le serveur MCP affiche des logs dĂ©taillĂ©s: + +``` +New SSE connection from: 192.168.1.10 +Session initialized: abc-123-def +Received message: {"jsonrpc":"2.0","id":1,"method":"tools/call",...} +Transport closed for session abc-123-def +``` + +**Activer plus de logs:** +```javascript +// Dans index-sse.js, ajouter: +console.log('Request body:', JSON.stringify(req.body, null, 2)); +console.log('Response:', JSON.stringify(result, null, 2)); +``` + +--- + +## 9. Workflow N8N Exemple + +### Exemple: CrĂ©er une note Ă  partir d'un email + +``` +[Email Trigger] + ↓ +[MCP Client] → create_note + ‱ title: {{ $json.subject }} + ‱ content: {{ $json.body }} + ‱ labels: ["email", "auto"] + ‱ color: "blue" + ↓ +[Send Notification] +``` + +### Exemple: Recherche et mise Ă  jour + +``` +[HTTP Request] (webhook) + ↓ +[MCP Client] → search_notes + ‱ query: {{ $json.keyword }} + ↓ +[Code Node] (filtrer rĂ©sultats) + ↓ +[MCP Client] → update_note + ‱ id: {{ $json.noteId }} + ‱ labels: [...$json.existingLabels, "processed"] +``` + +### Exemple: Backup quotidien + +``` +[Schedule Trigger] (daily 2am) + ↓ +[MCP Client] → get_notes + ↓ +[Convert to File] (JSON) + ↓ +[Save to Dropbox/Drive] +``` + +--- + +## 10. AvancĂ© + +### Sessions et Reconnexion + +Le serveur gĂšre automatiquement les sessions: +- GĂ©nĂšre un UUID unique par session +- Retourne `Mcp-Session-Id` dans le header de rĂ©ponse +- Accepte la reconnexion avec le mĂȘme session ID +- Nettoie automatiquement les sessions fermĂ©es + +### Streaming SSE + +Le serveur peut envoyer des notifications au client via SSE: + +```javascript +// CĂŽtĂ© serveur (exemple futur) +transport.send({ + jsonrpc: '2.0', + method: 'notifications/resources/updated', + params: { uri: 'notes://123' } +}); +``` + +### SĂ©curitĂ© + +⚠ **Important pour la production:** + +1. **Bind Ă  localhost uniquement:** +```javascript +app.listen(PORT, '127.0.0.1'); // Pas 0.0.0.0 +``` + +2. **Ajouter authentication:** +```javascript +app.use((req, res, next) => { + const token = req.headers.authorization; + if (token !== 'Bearer SECRET_TOKEN') { + return res.status(401).send('Unauthorized'); + } + next(); +}); +``` + +3. **Validation Origin:** +```javascript +const allowedOrigins = ['http://localhost:3000']; +if (!allowedOrigins.includes(req.headers.origin)) { + return res.status(403).send('Forbidden'); +} +``` + +--- + +## 11. Ressources + +### Documentation Officielle +- MCP Spec: https://modelcontextprotocol.io/specification/2025-06-18/basic/transports +- TypeScript SDK: https://github.com/modelcontextprotocol/typescript-sdk + +### Exemples de Code +- MCP Examples: https://github.com/modelcontextprotocol/typescript-sdk/tree/main/examples/server + +### Support +- GitHub Issues: https://github.com/modelcontextprotocol/typescript-sdk/issues +- Discord: https://discord.gg/modelcontextprotocol + +--- + +## 12. Changelog + +### Version 1.0.0 (2026-01-04) +- ✅ ImplĂ©mentation Streamable HTTP transport +- ✅ 9 outils de gestion de notes +- ✅ Support des sessions +- ✅ IntĂ©gration Prisma +- ✅ Compatible N8N + +### AmĂ©liorations Futures +- [ ] Authentication OAuth +- [ ] WebSocket transport +- [ ] Notifications temps rĂ©el +- [ ] Backup/restore automatique +- [ ] Rate limiting + +--- + +**Auteur:** MCP Memento Server +**Version:** 1.0.0 +**Date:** 2026-01-04 +**Licence:** MIT diff --git a/keep-notes/components/note-input.tsx b/keep-notes/components/note-input.tsx new file mode 100644 index 0000000..ccfed15 --- /dev/null +++ b/keep-notes/components/note-input.tsx @@ -0,0 +1,521 @@ +'use client' + +import { useState, useRef, useEffect } from 'react' +import { Card } from '@/components/ui/card' +import { Input } from '@/components/ui/input' +import { Textarea } from '@/components/ui/textarea' +import { Button } from '@/components/ui/button' +import { + CheckSquare, + X, + Bell, + Image, + UserPlus, + Palette, + Archive, + MoreVertical, + Undo2, + Redo2 +} from 'lucide-react' +import { createNote } from '@/app/actions/notes' +import { CheckItem, NOTE_COLORS, NoteColor } from '@/lib/types' +import { Checkbox } from '@/components/ui/checkbox' +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@/components/ui/tooltip' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu' +import { cn } from '@/lib/utils' +import { useUndoRedo } from '@/hooks/useUndoRedo' + +interface NoteState { + title: string + content: string + checkItems: CheckItem[] + images: string[] +} + +export function NoteInput() { + const [isExpanded, setIsExpanded] = useState(false) + const [type, setType] = useState<'text' | 'checklist'>('text') + const [isSubmitting, setIsSubmitting] = useState(false) + const [color, setColor] = useState('default') + const [isArchived, setIsArchived] = useState(false) + const fileInputRef = useRef(null) + + // Undo/Redo state management + const { + state: noteState, + setState: setNoteState, + undo, + redo, + canUndo, + canRedo, + clear: clearHistory + } = useUndoRedo({ + title: '', + content: '', + checkItems: [], + images: [] + }) + + const { title, content, checkItems, images } = noteState + + // Debounced state updates for performance + const updateTitle = (newTitle: string) => { + setNoteState(prev => ({ ...prev, title: newTitle })) + } + + const updateContent = (newContent: string) => { + setNoteState(prev => ({ ...prev, content: newContent })) + } + + const updateCheckItems = (newCheckItems: CheckItem[]) => { + setNoteState(prev => ({ ...prev, checkItems: newCheckItems })) + } + + const updateImages = (newImages: string[]) => { + setNoteState(prev => ({ ...prev, images: newImages })) + } + + // Keyboard shortcuts + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (!isExpanded) return + + if ((e.ctrlKey || e.metaKey) && e.key === 'z') { + e.preventDefault() + undo() + } + if ((e.ctrlKey || e.metaKey) && e.key === 'y') { + e.preventDefault() + redo() + } + } + + window.addEventListener('keydown', handleKeyDown) + return () => window.removeEventListener('keydown', handleKeyDown) + }, [isExpanded, undo, redo]) + + const handleImageUpload = (e: React.ChangeEvent) => { + const files = e.target.files + if (!files) return + + // Validate file types + const validTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'] + const maxSize = 5 * 1024 * 1024 // 5MB + + Array.from(files).forEach(file => { + // Validation + if (!validTypes.includes(file.type)) { + alert(`Invalid file type: ${file.name}. Only JPEG, PNG, GIF, and WebP are allowed.`) + return + } + + if (file.size > maxSize) { + alert(`File too large: ${file.name}. Maximum size is 5MB.`) + return + } + + const reader = new FileReader() + reader.onloadend = () => { + updateImages([...images, reader.result as string]) + } + reader.onerror = () => { + alert(`Failed to read file: ${file.name}`) + } + reader.readAsDataURL(file) + }) + + // Reset input + e.target.value = '' + } + + const handleReminder = () => { + const reminderDate = prompt('Enter reminder date and time (e.g., 2026-01-10 14:30):') + if (!reminderDate) return + + try { + const date = new Date(reminderDate) + if (isNaN(date.getTime())) { + alert('Invalid date format. Please use format: YYYY-MM-DD HH:MM') + return + } + + if (date < new Date()) { + alert('Reminder date must be in the future') + return + } + + // TODO: Store reminder in database and implement notification system + alert(`Reminder set for: ${date.toLocaleString()}\n\nNote: Reminder system will be fully implemented in the next update.`) + } catch (error) { + alert('Failed to set reminder. Please try again.') + } + } + + const handleSubmit = async () => { + // Validation + if (type === 'text' && !content.trim()) { + alert('Please enter some content for your note') + return + } + if (type === 'checklist' && checkItems.length === 0) { + alert('Please add at least one item to your checklist') + return + } + if (type === 'checklist' && checkItems.every(item => !item.text.trim())) { + alert('Checklist items cannot be empty') + return + } + + setIsSubmitting(true) + try { + await createNote({ + title: title.trim() || undefined, + content: type === 'text' ? content : '', + type, + checkItems: type === 'checklist' ? checkItems : undefined, + color, + isArchived, + images: images.length > 0 ? images : undefined, + }) + + // Reset form and history + setNoteState({ + title: '', + content: '', + checkItems: [], + images: [] + }) + clearHistory() + setIsExpanded(false) + setType('text') + setColor('default') + setIsArchived(false) + } catch (error) { + console.error('Failed to create note:', error) + alert('Failed to create note. Please try again.') + } finally { + setIsSubmitting(false) + } + } + + const handleAddCheckItem = () => { + updateCheckItems([ + ...checkItems, + { id: Date.now().toString(), text: '', checked: false }, + ]) + } + + const handleUpdateCheckItem = (id: string, text: string) => { + updateCheckItems( + checkItems.map(item => (item.id === id ? { ...item, text } : item)) + ) + } + + const handleRemoveCheckItem = (id: string) => { + updateCheckItems(checkItems.filter(item => item.id !== id)) + } + + const handleClose = () => { + setIsExpanded(false) + setNoteState({ + title: '', + content: '', + checkItems: [], + images: [] + }) + clearHistory() + setType('text') + setColor('default') + setIsArchived(false) + } + + if (!isExpanded) { + return ( + +
+ setIsExpanded(true)} + readOnly + value="" + className="border-0 focus-visible:ring-0 cursor-text" + /> + +
+
+ ) + } + + const colorClasses = NOTE_COLORS[color] || NOTE_COLORS.default + + return ( + +
+ updateTitle(e.target.value)} + className="border-0 focus-visible:ring-0 text-base font-semibold" + /> + + {/* Image Preview */} + {images.length > 0 && ( +
+ {images.map((img, idx) => ( +
+ {`Upload + +
+ ))} +
+ )} + + {type === 'text' ? ( +