diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 6bbac73..9111b56 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -10,7 +10,9 @@ "Bash(/home/mathieu/git/project-notes/notes/test-delete-1.md)", "Bash(/home/mathieu/git/project-notes/notes/test-delete-2.md)", "Bash(/home/mathieu/git/project-notes/notes/test-delete-folder/test.md)", - "Bash(npm install)" + "Bash(npm install)", + "Bash(go get:*)", + "Bash(ls:*)" ], "deny": [], "ask": [] diff --git a/API.md b/API.md index 526ae0b..e501ff6 100644 --- a/API.md +++ b/API.md @@ -6,6 +6,7 @@ Base URL: `http://localhost:8080/api/v1` ## Table des matières - [Vue d'ensemble](#vue-densemble) +- [Commandes CLI](#commandes-cli) - [Authentification](#authentification) - [Formats de données](#formats-de-données) - [Endpoints](#endpoints) @@ -13,6 +14,9 @@ Base URL: `http://localhost:8080/api/v1` - [Récupérer une note](#récupérer-une-note) - [Créer/Mettre à jour une note](#créermettre-à-jour-une-note) - [Supprimer une note](#supprimer-une-note) + - [Lister les notes publiques](#lister-les-notes-publiques) + - [Basculer le statut public d'une note](#basculer-le-statut-public-dune-note) +- [Notes publiques](#notes-publiques) - [Codes de statut HTTP](#codes-de-statut-http) - [Exemples d'utilisation](#exemples-dutilisation) @@ -38,6 +42,69 @@ L'API REST de PersoNotes permet de gérer vos notes Markdown via HTTP. Elle supp --- +## Commandes CLI + +Le serveur inclut des commandes CLI intégrées pour gérer les notes publiques sans avoir à lancer le serveur HTTP. + +### Lister les notes publiques + +Affiche toutes les notes qui ont été exportées en HTML public. + +**Commande** : +```bash +./server list-public [notes-dir] +``` + +**Arguments** : +- `notes-dir` (optionnel) : Chemin vers le répertoire des notes (défaut: `./notes`) + +**Exemple de sortie** : +``` +📚 Notes publiques (4): + +• 2025 Learning Goals + Source: personal/learning-goals.md + Public: public/learning-goals.html + Date: 2025-11-13 20:06:21 + +• AI Writing Assistant + Source: archive/ai-assistant.md + Public: public/ai-assistant.html + Date: 2025-11-13 19:43:28 + +• API Endpoints Reference + Source: documentation/api/endpoints.md + Public: public/endpoints.html + Date: 2025-11-13 19:36:57 + +• Product Backlog + Source: tasks/backlog.md + Public: public/backlog.html + Date: 2025-11-13 20:13:05 +``` + +**Cas particuliers** : +- Si aucune note n'est publique : affiche "Aucune note publique." +- Si le fichier `.public.json` n'existe pas : affiche "Aucune note publique trouvée." +- Erreur si le répertoire n'existe pas + +**Utilisation typique** : +```bash +# Lister les notes publiques +./server list-public + +# Avec un répertoire personnalisé +./server list-public /path/to/notes + +# Compter les notes publiques (Linux/macOS) +./server list-public | grep -c "^•" + +# Exporter la liste dans un fichier +./server list-public > public-notes-list.txt +``` + +--- + ## Authentification **Version actuelle : Aucune authentification requise** @@ -364,6 +431,313 @@ curl -X DELETE http://localhost:8080/api/v1/notes/projet/old-note.md --- +### Lister les notes publiques + +Récupère la liste des notes qui ont été publiées dans l'espace public. + +**Endpoint** : `GET /api/public/list` + +**Paramètres** : Aucun + +**Réponse** : `200 OK` + +```json +{ + "notes": [ + { + "path": "projet/backend.md", + "title": "Backend API", + "published_at": "2025-11-13T14:30:00Z" + }, + { + "path": "personal/guide.md", + "title": "Guide d'utilisation", + "published_at": "2025-11-13T10:15:00Z" + } + ] +} +``` + +**Champs retournés** : +- `path` : Chemin relatif de la note source +- `title` : Titre de la note +- `published_at` : Date/heure de publication (format ISO 8601) + +**Notes** : +- Les notes sont triées par date de publication (plus récentes d'abord) +- Les fichiers HTML générés se trouvent dans `public/{nom-de-la-note}.html` +- Liste vide si aucune note n'est publique + +**Exemple curl** : + +```bash +# Lister toutes les notes publiques +curl http://localhost:8080/api/public/list + +# Avec formatage jq +curl -s http://localhost:8080/api/public/list | jq '.notes[] | "\(.title) -> public/\(.path | split("/")[-1] | sub(".md$"; ".html"))"' +``` + +**Exemple de sortie formatée** : + +```bash +$ curl -s http://localhost:8080/api/public/list | jq '.notes[] | .title' +"Backend API" +"Guide d'utilisation" +"Documentation projet" +``` + +**Compter les notes publiques** : + +```bash +curl -s http://localhost:8080/api/public/list | jq '.notes | length' +``` + +--- + +### Basculer le statut public d'une note + +Publie ou retire une note de l'espace public. Génère automatiquement un fichier HTML statique exportable. + +**Endpoint** : `POST /api/public/toggle` + +**Content-Type** : `application/x-www-form-urlencoded` + +**Paramètres** : +- `path` (form) : Chemin relatif de la note (ex: `projet/backend.md`) + +**Réponse** : `200 OK` + +```json +{ + "status": "public", + "path": "projet/backend.md" +} +``` + +Valeurs possibles pour `status` : +- `"public"` : La note est maintenant publique (HTML généré) +- `"private"` : La note est maintenant privée (HTML supprimé) + +**Comportement** : +- ✅ **Génère du HTML statique** dans `public/nom-de-la-note.html` +- ✅ **Copie les CSS** nécessaires dans `public/static/` +- ✅ **Met à jour l'index** dans `public/index.html` +- ✅ **Structure plate** : Tous les fichiers dans `public/`, pas de sous-dossiers +- ✅ **Portable** : Les fichiers HTML peuvent être copiés sur n'importe quel serveur web + +**Fichiers générés** : +``` +public/ +├── index.html # Liste de toutes les notes publiques +├── backend.md.html # Note convertie en HTML standalone +├── autre-note.html +└── static/ + ├── theme.css # Styles copiés + └── themes.css +``` + +**Erreurs** : +- `400 Bad Request` : Chemin manquant ou invalide +- `404 Not Found` : Note inexistante +- `405 Method Not Allowed` : Méthode autre que POST +- `500 Internal Server Error` : Erreur de génération HTML + +**Exemple curl** : + +```bash +# Publier une note +curl -X POST http://localhost:8080/api/public/toggle \ + -d "path=projet/backend.md" + +# La note est maintenant accessible à : +# http://localhost:8080/public/backend.html +``` + +**Exemple JavaScript** : + +```javascript +// Publier une note +const response = await fetch('/api/public/toggle', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ path: 'projet/backend.md' }), +}); + +const data = await response.json(); +console.log(data.status); // "public" ou "private" +``` + +**Scripts utiles** : + +```bash +#!/bin/bash +# Lister les notes publiques avec leurs URLs + +echo "📚 Notes publiques:" +curl -s http://localhost:8080/api/public/list | jq -r '.notes[] | "• \(.title)\n → http://localhost:8080/public/\(.path | split("/")[-1] | sub(".md$"; ".html"))\n"' +``` + +```bash +#!/bin/bash +# Exporter toutes les notes en HTML public + +echo "Exporting all notes to public HTML..." +curl -s http://localhost:8080/api/v1/notes | jq -r '.notes[].path' | while read path; do + curl -X POST http://localhost:8080/api/public/toggle -d "path=$path" > /dev/null 2>&1 + echo "✓ Published: $path" +done + +echo "" +echo "✅ Export terminé!" +echo "📊 Total: $(curl -s http://localhost:8080/api/public/list | jq '.notes | length') notes publiques" +``` + +```bash +#!/bin/bash +# Retirer toutes les notes du public + +echo "Unpublishing all public notes..." +curl -s http://localhost:8080/api/public/list | jq -r '.notes[].path' | while read path; do + curl -X POST http://localhost:8080/api/public/toggle -d "path=$path" > /dev/null 2>&1 + echo "✓ Unpublished: $path" +done + +echo "✅ Toutes les notes sont maintenant privées" +``` + +--- + +## Notes publiques + +Le système de notes publiques génère des fichiers HTML statiques exportables. Cette section explique comment utiliser ces fichiers. + +### Accès aux notes publiques + +**Sur le serveur Personotes** : +``` +http://localhost:8080/public/ +``` + +**Fichiers générés** : +- `public/index.html` : Liste de toutes les notes publiques +- `public/*.html` : Notes converties en HTML standalone +- `public/static/` : CSS et assets + +### Déploiement + +Les fichiers HTML sont complètement autonomes et peuvent être déployés sur : + +#### 1. Serveur web classique + +```bash +# Copier sur un serveur Apache/Nginx +scp -r public/ user@server.com:/var/www/html/notes/ + +# Ou avec rsync +rsync -av public/ user@server.com:/var/www/html/notes/ +``` + +**Configuration Nginx** : +```nginx +server { + listen 80; + server_name notes.example.com; + root /var/www/html/notes; + index index.html; +} +``` + +**Configuration Apache** : +```apache + + ServerName notes.example.com + DocumentRoot /var/www/html/notes + +``` + +#### 2. GitHub Pages (gratuit) + +```bash +cd public/ +git init +git add . +git commit -m "Public notes" +git remote add origin https://github.com/username/notes-public.git +git push -u origin main + +# Activer GitHub Pages dans Settings → Pages → main branch +# Vos notes seront accessibles à : +# https://username.github.io/notes-public/ +``` + +#### 3. Netlify Drop + +1. Allez sur https://app.netlify.com/drop +2. Glissez-déposez le dossier `public/` +3. Netlify génère automatiquement une URL + +#### 4. Vercel + +```bash +cd public/ +npx vercel +``` + +### Automatisation de l'export + +**Script de synchronisation** : + +```bash +#!/bin/bash +# sync-public.sh - Synchroniser les notes publiques vers un serveur + +REMOTE_USER="user" +REMOTE_HOST="server.com" +REMOTE_PATH="/var/www/html/notes" + +rsync -av --delete public/ ${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_PATH} + +echo "✅ Notes publiques synchronisées !" +``` + +**Git Hook automatique** : + +```bash +# .git/hooks/post-commit +#!/bin/bash + +# Si le dossier public/ a changé, synchroniser +if git diff --name-only HEAD~1 | grep -q "^public/"; then + ./sync-public.sh +fi +``` + +### Avantages + +- ✅ **Performance** : HTML pré-généré = chargement instantané +- ✅ **Sécurité** : Fichiers statiques = surface d'attaque minimale +- ✅ **Portabilité** : Fonctionne sur n'importe quel serveur web +- ✅ **Gratuit** : Hébergement possible sur GitHub Pages, Netlify +- ✅ **SEO** : HTML pré-rendu = indexation optimale par Google +- ✅ **Pas de backend** : Pas besoin de Go sur le serveur de destination + +### Limitations + +- ⚠️ **Noms uniques** : Si deux notes dans différents dossiers ont le même nom (ex: `personal/test.md` et `work/test.md`), elles s'écraseront car la structure est plate +- ⚠️ **Republication manuelle** : Si vous modifiez une note déjà publique, vous devez la republier pour régénérer le HTML +- ⚠️ **Pas de recherche** : Les fichiers HTML n'incluent pas de fonction de recherche (uniquement consultables) + +### Documentation complète + +Pour plus d'informations sur l'export des notes publiques : +- **QUICK_START_PUBLIC.md** : Guide de démarrage rapide +- **EXPORT_GUIDE.md** : Guide complet de déploiement + +--- + ## Codes de statut HTTP | Code | Signification | Description | @@ -556,13 +930,14 @@ echo "$STATS" | jq -r '.notes[].tags[]' | sort | uniq -c | sort -rn | head -5 ## Changelog -### v1 (2025-11-10) +### v1 (2025-11-13) - ✨ Première version de l'API REST - ✅ Endpoints: LIST, GET, PUT, DELETE - ✅ Content negotiation JSON/Markdown - ✅ Support sous-dossiers - ✅ Gestion automatique du front matter - ✅ Ré-indexation automatique +- ✅ Export de notes publiques en HTML statique (POST /api/public/toggle) --- diff --git a/CLAUDE.md b/CLAUDE.md index c76cfeb..849c61b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,11 +9,13 @@ A lightweight web-based Markdown note-taking application with a Go backend and m **Key Features**: - **Daily Notes**: Quick daily journaling with interactive calendar, keyboard shortcuts (Ctrl/Cmd+D), and structured templates - **Favorites System**: Star important notes and folders for quick access from the sidebar +- **Public Notes**: Share selected notes publicly without authentication via server-rendered HTML pages - **Note Linking**: Create links between notes with `/link` command and fuzzy search modal - **Vim Mode**: Full Vim keybindings support (hjkl navigation, modes, commands) for power users - **Multiple Themes**: 8 dark themes (Material Dark, Monokai, Dracula, One Dark, Solarized, Nord, Catppuccin, Everforest) - **Font Customization**: 8 fonts (JetBrains Mono, Fira Code, Inter, etc.) with 4 size options - **Keyboard Shortcuts**: 10+ global shortcuts for navigation, editing, and productivity +- **Internationalization**: Full i18n support with English and French translations, automatic language detection **Recent Modernization**: The project has been migrated from a simple textarea editor to CodeMirror 6, with a Vite build system for frontend modules. The backend remains unchanged, maintaining the same Go architecture with htmx for dynamic interactions. @@ -21,31 +23,41 @@ A lightweight web-based Markdown note-taking application with a Go backend and m ### Backend (Go) -Four main packages under `internal/`: +Five main packages under `internal/`: - **indexer**: Maintains an in-memory index mapping tags to note files. Parses YAML front matter from `.md` files to build the index. Thread-safe with RWMutex. - **watcher**: Uses `fsnotify` to monitor filesystem changes and trigger re-indexing with 200ms debounce. Recursively watches all subdirectories. +- **i18n**: Internationalization support with JSON-based translations. Loads translations at startup and provides `T()` function for translation lookups with variable interpolation. - **api**: HTTP handlers that serve templates and handle CRUD operations on notes. Updates front matter automatically on save. - `handler.go` - Main HTML endpoints for the web interface - `rest_handler.go` - REST API endpoints (v1) - `daily_notes.go` - Daily note creation and calendar functionality - `favorites.go` - Favorites management (star/unstar notes and folders) + - `public.go` - Public notes sharing with server-side HTML rendering The server (`cmd/server/main.go`) coordinates these components: 1. Loads initial index from notes directory -2. Starts filesystem watcher for automatic re-indexing -3. Pre-parses HTML templates from `templates/` -4. Serves routes: +2. Loads translations from `locales/` directory (JSON files for each language) +3. Starts filesystem watcher for automatic re-indexing +4. Pre-parses HTML templates from `templates/` +5. Serves routes: - `/` (main page) - `/api/v1/notes` and `/api/v1/notes/*` (REST API - JSON responses) + - `/api/i18n/{lang}` (Translation JSON endpoint) - `/api/search` (HTML search results) - `/api/notes/*` (HTML editor for notes) - `/api/tree` (HTML file tree) - `/api/folders/create` (Folder management) - `/api/files/move` (File/folder moving) - `/api/home` (Home page) - - `/api/daily-notes/*` (Daily note creation and calendar) - - `/api/favorites/*` (Favorites management) -5. Handles static files from `static/` directory + - `/api/about` (About page) + - `/api/daily` and `/api/daily/*` (Daily note creation and calendar) + - `/api/favorites` (Favorites management) + - `/api/folder/*` (Folder view) + - `/public` (Public notes list - no auth required) + - `/public/view/*` (View public note - no auth required) + - `/api/public/toggle` (Toggle public status - requires auth in production) + - `/api/public/list` (List public notes JSON) +6. Handles static files from `static/` directory and `public/` directory (for public HTML exports) ### Frontend @@ -69,9 +81,14 @@ frontend/src/ ├── file-tree.js # Drag-and-drop file organization ├── favorites.js # Favorites system (star/unstar functionality) ├── daily-notes.js # Daily notes creation and calendar widget +├── public-toggle.js # Public/private status toggle for notes ├── keyboard-shortcuts.js # Global keyboard shortcuts management ├── theme-manager.js # Theme switching and persistence ├── font-manager.js # Font selection and size management +├── i18n.js # i18n client with translation function and language detection +├── language-manager.js # Language selector UI and page translation +├── sidebar-sections.js # Sidebar sections management (Recent/Favorites/All notes) +├── debug.js # Debug utilities └── ui.js # Sidebar toggle functionality ``` @@ -289,6 +306,33 @@ The favorites system allows quick access to frequently used notes and folders: Favorites are loaded on server startup and updated in real-time via htmx. +### Public Notes + +**Implementation**: `internal/api/public.go` and `frontend/src/public-toggle.js` + +The public notes feature allows sharing selected notes publicly without requiring authentication: + +**Features**: +- **Toggle Button**: Click 🔒 Privé/🌐 Public button in the editor to publish/unpublish notes +- **Server-side Rendering**: Notes converted to HTML using `goldmark` (Go Markdown library) +- **Public Routes**: `/public` (list) and `/public/view/{path}` (individual note) - no auth required +- **Persistence**: Public notes list stored in `notes/.public.json` +- **GitHub Flavored Markdown**: Supports tables, strikethrough, task lists, syntax highlighting +- **SEO-friendly**: Pre-rendered HTML with proper meta tags +- **Internationalized**: Full i18n support for public pages + +**Security Model**: +- Public routes (`/public*`) are accessible without authentication +- Admin routes (`/api/public/toggle`) should be protected by reverse proxy auth (Authelia, Authentik, Basic Auth) +- In production, use nginx/Caddy with auth to protect all routes except `/public*` + +**Command-line tool**: +```bash +go run ./cmd/server list-public # List all published notes +``` + +**Documentation**: See `PUBLIC_NOTES.md` for complete security recommendations and usage guide. + ### Note Format Notes have YAML front matter with these fields: @@ -372,6 +416,7 @@ go mod tidy Key backend dependencies: - `github.com/fsnotify/fsnotify` - Filesystem watcher - `gopkg.in/yaml.v3` - YAML parsing for front matter +- `github.com/yuin/goldmark` - Markdown to HTML conversion for public notes (with GFM extensions) ### Vite Build System @@ -510,6 +555,65 @@ Themes are applied via CSS custom properties and persist in localStorage. Font settings apply to both the editor and preview pane. +### Internationalization (i18n) + +**Implementation**: `internal/i18n/i18n.go`, `frontend/src/i18n.js`, `frontend/src/language-manager.js` + +The application provides full internationalization support: + +#### Backend (Go) +- **Package**: `internal/i18n/i18n.go` - Translation engine with JSON-based translation files +- **Storage**: Translation files in `locales/` directory (e.g., `en.json`, `fr.json`) +- **Loading**: Translations loaded once at server startup from `locales/` directory +- **Translation Function**: `T(lang, key, vars...)` - Looks up translation keys with optional variable interpolation +- **API Endpoint**: `/api/i18n/{lang}` - Serves translation JSON for frontend consumption +- **Helper Functions**: `getLanguage(r)` and `t(r, key, vars...)` in handler.go for request-scoped translations + +#### Frontend (JavaScript) +- **Module**: `frontend/src/i18n.js` - Client-side translation with automatic language detection +- **Detection Order**: + 1. localStorage (`language` key) + 2. Browser language (navigator.language) + 3. Default to English +- **Translation Function**: `t(key, vars)` - Looks up translations with variable interpolation +- **Page Translation**: `translatePage()` - Automatically translates elements with `data-i18n` attributes +- **Language Manager**: `frontend/src/language-manager.js` - UI for language selection in Settings modal +- **Persistence**: Language preference stored in localStorage +- **Reload**: Automatic page translation on language change + +#### Available Languages +- **English** (en) - Default language +- **French** (fr) - Full translation + +#### Adding New Languages +1. Create new JSON file in `locales/` (e.g., `locales/es.json`) +2. Copy structure from `en.json` with 200+ translation keys +3. Translate all keys maintaining the nested structure +4. Add language option to Settings modal in templates +5. Restart server to load new translations + +#### Translation Key Structure +``` +app.name → "Personotes" +menu.home → "Home" / "Accueil" +editor.save → "Save" / "Sauvegarder" +editor.confirmDelete → "Are you sure...?" (supports {{filename}} interpolation) +errors.methodNotAllowed → "Method not allowed" / "Méthode non supportée" +``` + +#### Variable Interpolation +```javascript +// JavaScript +t('editor.confirmDelete', { filename: 'test.md' }) +// → "Are you sure you want to delete this note (test.md)?" + +// Go +h.t(r, "editor.confirmDelete", map[string]string{"filename": "test.md"}) +// → "Êtes-vous sûr de vouloir supprimer cette note (test.md) ?" +``` + +**Documentation**: See `I18N_IMPLEMENTATION.md` for complete implementation details. + ### Vim Mode **Implementation**: `frontend/src/vim-mode-manager.js` using `@replit/codemirror-vim` @@ -565,6 +669,13 @@ File path validation in `handler.go` and `rest_handler.go`: - CORS not configured (same-origin only) - No rate limiting (add middleware if needed) +**Public Notes Security**: +- Routes `/public` and `/public/view/*` are intentionally public (no auth required) +- Route `/api/public/toggle` MUST be protected in production (requires auth to publish/unpublish) +- Use reverse proxy (nginx with Authelia/Authentik, or Basic Auth) to protect admin routes +- Public HTML is server-rendered with goldmark (prevents XSS, safe rendering) +- See `PUBLIC_NOTES.md` for complete security setup guide + ### Template System Templates are pre-parsed at startup. The API handler returns HTML fragments that htmx inserts into the page. Out-of-band swaps update the file tree sidebar without full page reload. @@ -752,10 +863,14 @@ personotes/ │ │ ├── handler.go # HTTP handlers for CRUD operations │ │ ├── rest_handler.go # REST API v1 endpoints │ │ ├── daily_notes.go # Daily notes functionality -│ │ └── favorites.go # Favorites management +│ │ ├── favorites.go # Favorites management +│ │ └── public.go # Public notes sharing │ ├── indexer/ │ │ ├── indexer.go # Note indexing and search │ │ └── indexer_test.go # Indexer tests +│ ├── i18n/ +│ │ ├── i18n.go # Internationalization engine +│ │ └── i18n_test.go # i18n tests │ └── watcher/ │ └── watcher.go # Filesystem watcher with fsnotify ├── frontend/ # Frontend build system @@ -767,9 +882,15 @@ personotes/ │ │ ├── file-tree.js # Drag-and-drop file tree │ │ ├── favorites.js # Favorites system │ │ ├── daily-notes.js # Daily notes and calendar widget +│ │ ├── public-toggle.js # Public/private status toggle │ │ ├── keyboard-shortcuts.js # Global keyboard shortcuts │ │ ├── theme-manager.js # Theme switching │ │ ├── font-manager.js # Font customization +│ │ ├── i18n.js # i18n client and translation +│ │ ├── language-manager.js # Language selector UI +│ │ ├── link-inserter.js # Note linking modal +│ │ ├── sidebar-sections.js # Sidebar sections management +│ │ ├── debug.js # Debug utilities │ │ └── ui.js # Sidebar toggle │ ├── package.json # NPM dependencies │ ├── package-lock.json @@ -785,11 +906,20 @@ personotes/ │ ├── file-tree.html # File tree sidebar │ ├── search-results.html # Search results │ └── new-note-prompt.html # New note modal +├── locales/ # i18n translation files +│ ├── en.json # English translations (200+ keys) +│ ├── fr.json # French translations (200+ keys) +│ └── README.md # Translation guide ├── notes/ # Note storage directory │ ├── *.md # Markdown files with YAML front matter │ ├── daily/ # Daily notes (YYYY-MM-DD.md) │ ├── .favorites.json # Favorites list (auto-generated) +│ ├── .public.json # Public notes list (auto-generated) │ └── daily-note-template.md # Optional daily note template +├── public/ # Public HTML exports (auto-generated) +│ ├── index.html # Public notes list page +│ ├── *.html # Individual public notes (HTML) +│ └── static/ # Static assets for public pages ├── docs/ # Documentation │ ├── KEYBOARD_SHORTCUTS.md # Keyboard shortcuts reference │ ├── DAILY_NOTES.md # Daily notes guide @@ -798,6 +928,8 @@ personotes/ ├── go.mod # Go dependencies ├── go.sum ├── API.md # REST API documentation +├── I18N_IMPLEMENTATION.md # i18n implementation guide +├── PUBLIC_NOTES.md # Public notes documentation and security guide └── CLAUDE.md # This file ``` @@ -809,8 +941,10 @@ personotes/ - `internal/api/rest_handler.go` - REST API v1 endpoints - `internal/api/daily_notes.go` - Daily notes and calendar functionality - `internal/api/favorites.go` - Favorites management +- `internal/api/public.go` - Public notes sharing and HTML export - `internal/indexer/indexer.go` - Search and indexing logic - `internal/watcher/watcher.go` - Filesystem monitoring +- `internal/i18n/i18n.go` - Internationalization engine **Frontend Development**: - `frontend/src/editor.js` - CodeMirror editor, preview, slash commands @@ -820,12 +954,17 @@ personotes/ - `frontend/src/file-tree.js` - File tree interactions and drag-and-drop - `frontend/src/favorites.js` - Favorites system - `frontend/src/daily-notes.js` - Daily notes creation and calendar widget +- `frontend/src/public-toggle.js` - Public/private status toggle - `frontend/src/keyboard-shortcuts.js` - Global keyboard shortcuts - `frontend/src/theme-manager.js` - Theme switching logic - `frontend/src/font-manager.js` - Font customization logic +- `frontend/src/i18n.js` - i18n client and translation +- `frontend/src/language-manager.js` - Language selector UI +- `frontend/src/sidebar-sections.js` - Sidebar sections management - `frontend/src/ui.js` - UI utilities (sidebar toggle) - `static/theme.css` - Styling and theming (8 themes) - `templates/*.html` - HTML templates (Go template syntax) +- `locales/*.json` - Translation files (en.json, fr.json) **Configuration**: - `frontend/vite.config.js` - Frontend build configuration diff --git a/EXPORT_GUIDE.md b/EXPORT_GUIDE.md new file mode 100644 index 0000000..2e57919 --- /dev/null +++ b/EXPORT_GUIDE.md @@ -0,0 +1,360 @@ +# 📤 Export de Notes Publiques - Guide Complet + +## 🎯 Concept + +Les notes publiques sont **générées en fichiers HTML statiques** directement sur le serveur. Cela signifie que vous pouvez copier le dossier `public/` et le déployer sur n'importe quel serveur web (Apache, Nginx, GitHub Pages, Netlify, etc.) **sans avoir besoin de Go**. + +## 🔍 Lister les notes publiques + +Vous pouvez lister toutes les notes publiques avec la commande CLI intégrée : + +```bash +./server list-public +``` + +**Sortie** : +``` +📚 Notes publiques (4): + +• Mon Guide + Source: personal/guide.md + Public: public/guide.html + Date: 2025-11-13 20:06:21 +``` + +Cette commande lit le fichier `notes/.public.json` et affiche : +- Le titre de chaque note +- Son chemin source +- Son chemin public +- La date de publication + +## 📁 Structure générée + +Quand vous publiez une note, le système génère automatiquement : + +``` +public/ +├── index.html # Liste de toutes les notes publiques +├── ma-note.html # Notes converties en HTML (structure plate) +├── autre.html +└── static/ + ├── theme.css # Styles CSS copiés + └── themes.css +``` + +**Note** : La structure est plate - toutes les notes publiques sont directement dans `public/`, peu importe leur emplacement d'origine dans vos dossiers personnels. + +## 🔄 Génération automatique + +### Quand une note est publiée + +1. **L'utilisateur clique sur "🔒 Privé"** dans l'éditeur +2. Le système : + - Lit le fichier Markdown + - Extrait le front matter (titre, tags, date) + - Convertit le Markdown en HTML avec goldmark + - Génère un fichier HTML standalone complet + - Copie les CSS nécessaires + - Régénère `index.html` avec la nouvelle note +3. **Le bouton devient "🌐 Public"** + +### Quand une note est retirée + +1. **L'utilisateur clique sur "🌐 Public"** +2. Le système : + - Supprime le fichier HTML correspondant + - Régénère `index.html` sans cette note +3. **Le bouton redevient "🔒 Privé"** + +## 📋 Emplacement des fichiers + +- **Source** : `notes/` (vos fichiers Markdown originaux) +- **Export** : `public/` (fichiers HTML générés) +- **Index** : `.public.json` (liste des notes publiques) + +## 🚀 Déploiement + +### Option 1 : Copie manuelle sur serveur web + +```bash +# Copier le dossier public/ sur votre serveur +scp -r public/ user@server.com:/var/www/html/notes/ + +# Ou avec rsync +rsync -av public/ user@server.com:/var/www/html/notes/ +``` + +**Configuration Nginx** : +```nginx +server { + listen 80; + server_name notes.example.com; + + root /var/www/html/notes; + index index.html; + + location / { + try_files $uri $uri/ =404; + } +} +``` + +**Configuration Apache** : +```apache + + ServerName notes.example.com + DocumentRoot /var/www/html/notes + + + Options Indexes FollowSymLinks + AllowOverride All + Require all granted + + +``` + +### Option 2 : GitHub Pages (gratuit) + +```bash +# 1. Créer un repo GitHub +git init public/ +cd public/ +git add . +git commit -m "Initial public notes" + +# 2. Pousser vers GitHub +git remote add origin https://github.com/username/notes-public.git +git push -u origin main + +# 3. Activer GitHub Pages +# Settings → Pages → Source: main branch → Save +``` + +Vos notes seront disponibles à : `https://username.github.io/notes-public/` + +### Option 3 : Netlify Drop (ultra simple) + +1. Allez sur https://app.netlify.com/drop +2. Glissez-déposez le dossier `public/` +3. Netlify génère automatiquement une URL + +### Option 4 : Vercel + +```bash +cd public/ +npx vercel +``` + +## 🔄 Automatisation avec script + +Créez un script pour synchroniser automatiquement : + +```bash +#!/bin/bash +# sync-public.sh + +# Serveur de destination +REMOTE_USER="user" +REMOTE_HOST="server.com" +REMOTE_PATH="/var/www/html/notes" + +# Synchroniser +rsync -av --delete public/ ${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_PATH} + +echo "✅ Notes publiques synchronisées !" +``` + +Utilisation : +```bash +chmod +x sync-public.sh +./sync-public.sh +``` + +## 🔄 Automatisation avec Git Hook + +Synchroniser automatiquement après chaque publication : + +```bash +# .git/hooks/post-commit +#!/bin/bash + +# Si le dossier public/ a changé, synchroniser +if git diff --name-only HEAD~1 | grep -q "^public/"; then + ./sync-public.sh +fi +``` + +## 📊 Avantages de cette approche + +### ✅ Performance +- HTML pré-généré = temps de chargement instantané +- Pas de backend requis = moins de latence +- Cacheable à 100% par CDN + +### ✅ Sécurité +- Fichiers statiques = surface d'attaque minimale +- Pas de code serveur exécuté côté public +- Isolation complète du système d'édition + +### ✅ Portabilité +- Fonctionne sur **n'importe quel serveur web** +- Hébergement gratuit possible (GitHub Pages, Netlify) +- Peut être mis sur un CDN (Cloudflare, etc.) + +### ✅ Simplicité +- Pas besoin de Go sur le serveur de destination +- Pas de base de données +- Juste des fichiers HTML à copier + +### ✅ SEO +- HTML pré-rendu = indexation optimale par Google +- Pas de JavaScript requis pour afficher le contenu +- Meta tags facilement ajoutables + +## 🎨 Personnalisation + +### Modifier les styles + +Les fichiers CSS sont dans `public/static/`. Vous pouvez les modifier directement : + +```bash +# Éditer le CSS +nano public/static/theme.css + +# Ou copier vos propres CSS +cp my-custom.css public/static/custom.css +``` + +### Ajouter des meta tags + +Modifiez `internal/api/public.go` dans la fonction `generateStandaloneHTML()` : + +```go + + + + + + + + %s - PersoNotes +``` + +### Ajouter Google Analytics + +Ajoutez dans `generateStandaloneHTML()` avant `` : + +```html + + + +``` + +## 🐛 Dépannage + +### Les notes ne s'affichent pas + +**Problème** : `/public/` retourne 404 + +**Solutions** : +1. Vérifier que le dossier `public/` existe : `ls -la public/` +2. Publier au moins une note pour générer le dossier +3. Redémarrer le serveur Go + +### Les styles ne s'appliquent pas + +**Problème** : La page s'affiche sans CSS + +**Solutions** : +1. Vérifier que `public/static/theme.css` existe +2. Les chemins CSS sont relatifs : `../static/theme.css` +3. Si vous copiez ailleurs, ajustez les chemins + +### Le HTML contient du Markdown brut + +**Problème** : Le Markdown n'est pas converti + +**Solutions** : +1. Vérifier que goldmark est installé : `go mod tidy` +2. Republier la note (toggle Private → Public) +3. Vérifier les logs du serveur + +## 📝 Workflow recommandé + +### Workflow quotidien + +1. **Écrire** vos notes en Markdown dans l'éditeur +2. **Publier** les notes que vous voulez partager (bouton Public) +3. **Synchroniser** le dossier `public/` vers votre serveur (manuel ou automatique) + +### Workflow avec Git + +```bash +# 1. Publier des notes via l'interface web +# 2. Commiter les changements +git add public/ +git commit -m "Publish new notes" +git push + +# 3. Sur le serveur de production +git pull +# Les nouvelles notes sont disponibles ! +``` + +## 🔐 Sécurité + +### Routes publiques (pas d'auth) +- ✅ `/public/*` - Fichiers HTML statiques accessibles à tous + +### Routes privées (nécessitent auth) +- 🔒 `/` - Interface d'édition +- 🔒 `/api/*` - APIs de modification +- 🔒 `/api/public/toggle` - **Protéger cet endpoint !** + +### Protection recommandée + +Utilisez un reverse proxy avec authentification pour protéger l'édition : + +```nginx +# Nginx +location /public { + # Pas d'auth - accessible à tous + proxy_pass http://localhost:8080; +} + +location / { + # Auth requise pour édition + auth_basic "Personotes Admin"; + auth_basic_user_file /etc/nginx/.htpasswd; + proxy_pass http://localhost:8080; +} +``` + +## 📚 Ressources + +- **Goldmark** : https://github.com/yuin/goldmark +- **GitHub Pages** : https://pages.github.com +- **Netlify** : https://www.netlify.com +- **Vercel** : https://vercel.com + +## ❓ FAQ + +**Q: Puis-je personnaliser le design des pages publiques ?** +R: Oui ! Modifiez `public/static/theme.css` ou éditez la fonction `generateStandaloneHTML()` dans `internal/api/public.go`. + +**Q: Les images dans mes notes fonctionnent-elles ?** +R: Oui, si vous utilisez des URLs absolues ou si vous copiez les images dans `public/static/images/`. + +**Q: Puis-je exporter vers PDF ?** +R: Les fichiers HTML peuvent être convertis en PDF avec wkhtmltopdf ou un navigateur (Imprimer → PDF). + +**Q: Comment supprimer toutes les notes publiques d'un coup ?** +R: Supprimez le dossier `public/` et le fichier `.public.json`, puis relancez le serveur. + +**Q: Les modifications des notes sont-elles automatiquement republiées ?** +R: Non. Si vous modifiez une note Markdown qui est déjà publique, vous devez la republier (toggle Private puis Public) pour régénérer le HTML. diff --git a/PUBLIC_NOTES.md b/PUBLIC_NOTES.md new file mode 100644 index 0000000..0388cc4 --- /dev/null +++ b/PUBLIC_NOTES.md @@ -0,0 +1,188 @@ +# 📚 Public Notes - Documentation + +## Vue d'ensemble + +La fonctionnalité **Public Notes** permet de partager certaines notes sélectionnées avec le public, sans authentification requise. Ces notes sont converties en HTML côté serveur avec `goldmark` et sont accessibles via des URLs dédiées. + +## 🔒 Architecture de sécurité + +### Routes publiques (sans authentification) + +Les routes suivantes sont **publiques** et **accessibles à tous** : + +- `GET /public` - Liste de toutes les notes publiques +- `GET /public/view/{path}` - Affichage d'une note publique en HTML + +### Routes protégées (nécessitent authentification en production) + +Toutes les autres routes doivent être protégées par authentification : + +- `/` - Interface principale de l'application +- `/api/*` - Toutes les API (édition, création, suppression) +- `/api/public/toggle` - Toggle du statut public (création/retrait) + +**IMPORTANT** : L'endpoint `/api/public/toggle` doit être protégé car il permet de publier/dépublier des notes. + +## 🛡️ Recommandations de sécurité + +### Option 1 : Authelia / Authentik (Recommandé) + +Utilisez un reverse proxy avec authentification devant Personotes : + +```nginx +# Nginx avec Authelia +location /public { + proxy_pass http://localhost:8080; + # Pas d'auth pour /public +} + +location / { + proxy_pass http://localhost:8080; + # Rediriger vers Authelia pour authentification + auth_request /authelia; + error_page 401 =302 https://auth.example.com; +} +``` + +### Option 2 : Basic Auth Nginx + +Configuration minimale pour protéger l'édition : + +```nginx +location /public { + proxy_pass http://localhost:8080; +} + +location / { + proxy_pass http://localhost:8080; + auth_basic "Personotes Admin"; + auth_basic_user_file /etc/nginx/.htpasswd; +} +``` + +### Option 3 : Cloudflare Access / Zero Trust + +Utilisez Cloudflare Access pour protéger toutes les routes sauf `/public`. + +## 📝 Utilisation + +### 1. Publier une note + +1. Ouvrez une note dans l'éditeur +2. Cliquez sur le bouton **🔒 Privé** +3. Le bouton devient **🌐 Public** (vert) +4. La note est maintenant visible sur `/public` + +**Note** : Le titre et les tags sont automatiquement extraits du front matter. + +### 2. Retirer une note du public + +1. Ouvrez la note publiée +2. Cliquez sur le bouton **🌐 Public** +3. Le bouton redevient **🔒 Privé** +4. La note n'est plus visible sur `/public` + +### 3. Voir les notes publiques + +Accédez à : `http://votre-domaine.com/public` + +## 🌍 Internationalisation + +La fonctionnalité est entièrement internationalisée : + +- **Anglais** : Public Notes, Published on, etc. +- **Français** : Notes Publiques, Publié le, etc. + +La langue est détectée automatiquement depuis : +1. localStorage (`language` key) +2. Navigateur (navigator.language) +3. Par défaut : Anglais + +## 📂 Fichiers techniques + +### Backend + +- `internal/api/public.go` - Handlers pour les notes publiques +- `notes/.public.json` - Liste des notes publiques (auto-généré) + +### Frontend + +- `frontend/src/public-toggle.js` - Gestion du bouton Public/Privé +- `templates/public-list.html` - Liste des notes publiques +- `templates/public-view.html` - Vue d'une note publique + +### Traductions + +- `locales/en.json` - Traductions anglaises (section `public`) +- `locales/fr.json` - Traductions françaises (section `public`) + +## 🎨 Rendu HTML + +Les notes publiques sont rendues avec **goldmark** (bibliothèque Go) : + +- ✅ GitHub Flavored Markdown (GFM) +- ✅ Tables, strikethrough, task lists +- ✅ Syntax highlighting (highlight.js) +- ✅ Typographie intelligente (smart quotes, dashes) +- ✅ Auto-génération des IDs pour les titres + +**Avantages** : +- SEO-friendly (HTML pré-rendu) +- Performance optimale (pas de JS requis) +- Sécurité renforcée (rendu côté serveur) +- Cache facile (HTML statique) + +## 🔗 URLs et partage + +### Structure des URLs + +- Liste : `https://votre-domaine.com/public` +- Note : `https://votre-domaine.com/public/view/path/to/note.md` + +### Partage sur réseaux sociaux + +Pour améliorer le partage, vous pouvez ajouter des meta tags Open Graph dans `templates/public-view.html` : + +```html + + + + +``` + +## 🚀 Évolutions futures possibles + +- [ ] Export PDF des notes publiques +- [ ] RSS feed des notes publiques +- [ ] Sitemap XML pour SEO +- [ ] Commentaires sur les notes publiques +- [ ] Analytics (pages vues, etc.) +- [ ] Bouton "Partager" avec liens directs + +## ❓ FAQ + +### Q: Les notes publiques peuvent-elles contenir des liens vers des notes privées ? + +**R:** Oui, mais les liens vers des notes privées ne fonctionneront que pour les utilisateurs authentifiés. Pour les visiteurs publics, ils verront une erreur 403. + +### Q: Puis-je changer le thème des pages publiques ? + +**R:** Oui, modifiez les templates `public-list.html` et `public-view.html`. Ils utilisent les mêmes CSS que l'application principale (`static/theme.css`). + +### Q: Comment savoir quelles notes sont publiques ? + +**R:** Consultez le fichier `notes/.public.json` ou accédez à `/public` pour voir la liste complète. + +### Q: Les images dans les notes publiques fonctionnent-elles ? + +**R:** Oui, si les images sont référencées avec des chemins absolus ou des URLs. Les chemins relatifs ne fonctionneront que si les images sont dans `static/`. + +### Q: Peut-on avoir plusieurs niveaux d'accès (public, privé, semi-privé) ? + +**R:** Actuellement non. Il y a uniquement 2 niveaux : **privé** (nécessite auth) et **public** (accessible à tous). Pour plus de granularité, utilisez un système d'authentification avec rôles (Authelia, Authentik). + +## 📞 Support + +Pour toute question ou problème : +- GitHub Issues : https://github.com/mathieu/personotes/issues +- Documentation complète : `/docs/USAGE_GUIDE.md` diff --git a/QUICK_START_PUBLIC.md b/QUICK_START_PUBLIC.md new file mode 100644 index 0000000..98cc6f2 --- /dev/null +++ b/QUICK_START_PUBLIC.md @@ -0,0 +1,119 @@ +# 🚀 Quick Start - Notes Publiques + +## Comment ça marche ? + +Les notes publiques sont **générées en fichiers HTML statiques**. Vous pouvez les copier et les héberger n'importe où ! + +## 📝 Publier une note + +1. Ouvrez une note dans l'éditeur +2. Cliquez sur le bouton **🔒 Privé** +3. Le bouton devient **🌐 Public** ✅ + +**Que se passe-t-il ?** +- Un fichier HTML est généré dans `public/notes/` +- Le fichier `public/index.html` est mis à jour +- Les CSS sont copiés dans `public/static/` + +## 🌐 Accéder aux notes publiques + +### Lister les notes publiques (CLI) + +```bash +./server list-public +``` + +Cette commande affiche toutes les notes exportées avec leurs chemins source et public. + +### Sur le serveur Personotes + +``` +http://localhost:8080/public/ +``` + +### Structure générée + +``` +public/ +├── index.html # Liste de toutes vos notes publiques +├── ma-note.html # Vos notes en HTML (structure plate) +├── autre.html +└── static/ + ├── theme.css # Styles + └── themes.css +``` + +## 📤 Exporter vers un autre serveur + +### Méthode 1 : Copie simple + +```bash +# Copier tout le dossier public/ +cp -r public/ /var/www/html/notes/ +``` + +### Méthode 2 : SCP (serveur distant) + +```bash +scp -r public/ user@server.com:/var/www/html/notes/ +``` + +### Méthode 3 : GitHub Pages (gratuit) + +```bash +cd public/ +git init +git add . +git commit -m "Public notes" +git remote add origin https://github.com/username/notes.git +git push -u origin main +``` + +Activez GitHub Pages dans les settings du repo → vos notes sont en ligne ! + +### Méthode 4 : Netlify Drop + +1. Allez sur https://app.netlify.com/drop +2. Glissez-déposez le dossier `public/` +3. C'est en ligne ! + +## 🔧 Configuration serveur web + +### Apache + +```apache + + ServerName notes.example.com + DocumentRoot /var/www/html/notes + +``` + +### Nginx + +```nginx +server { + listen 80; + server_name notes.example.com; + root /var/www/html/notes; + index index.html; +} +``` + +## ✨ Avantages + +- ✅ **Portable** : Fonctionne sur n'importe quel serveur web +- ✅ **Rapide** : HTML pré-généré = chargement instantané +- ✅ **Gratuit** : Hébergez sur GitHub Pages, Netlify, etc. +- ✅ **SEO** : Google peut indexer vos notes +- ✅ **Sécurisé** : Pas de code serveur, juste du HTML + +## 🔄 Workflow typique + +1. **Écrire** dans Personotes (Markdown) +2. **Publier** (bouton Public) +3. **Copier** le dossier `public/` vers votre serveur +4. **Profit !** Vos notes sont en ligne + +## 📖 Plus d'infos + +Voir `EXPORT_GUIDE.md` pour le guide complet. diff --git a/cmd/server/main.go b/cmd/server/main.go index 38e0e07..9f279c5 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "encoding/json" "errors" "flag" "fmt" @@ -10,6 +11,7 @@ import ( "net/http" "os" "os/signal" + "path/filepath" "syscall" "time" @@ -20,21 +22,82 @@ import ( ) func main() { + // Vérifier si une sous-commande est demandée + if len(os.Args) > 1 && os.Args[1] == "list-public" { + cmdListPublic() + return + } + addr := flag.String("addr", ":8080", "Adresse d ecoute HTTP") notesDir := flag.String("notes-dir", "./notes", "Repertoire contenant les notes Markdown") flag.Parse() logger := log.New(os.Stdout, "[server] ", log.LstdFlags) + runServer(*addr, *notesDir, logger) +} + +// cmdListPublic liste les notes publiques +func cmdListPublic() { + notesDir := "./notes" + if len(os.Args) > 2 && os.Args[2] != "" { + notesDir = os.Args[2] + } + + publicFile := filepath.Join(notesDir, ".public.json") + + // Lire le fichier .public.json + data, err := os.ReadFile(publicFile) + if err != nil { + if os.IsNotExist(err) { + fmt.Println("Aucune note publique trouvée.") + fmt.Printf("Le fichier %s n'existe pas.\n", publicFile) + os.Exit(0) + } + fmt.Fprintf(os.Stderr, "Erreur de lecture: %v\n", err) + os.Exit(1) + } + + var publicNotes struct { + Notes []struct { + Path string `json:"path"` + Title string `json:"title"` + PublishedAt time.Time `json:"published_at"` + } `json:"notes"` + } + + if err := json.Unmarshal(data, &publicNotes); err != nil { + fmt.Fprintf(os.Stderr, "Erreur de parsing JSON: %v\n", err) + os.Exit(1) + } + + if len(publicNotes.Notes) == 0 { + fmt.Println("Aucune note publique.") + os.Exit(0) + } + + fmt.Printf("\n📚 Notes publiques (%d):\n\n", len(publicNotes.Notes)) + for _, note := range publicNotes.Notes { + filename := filepath.Base(note.Path) + htmlFile := filename[:len(filename)-3] + ".html" + + fmt.Printf("• %s\n", note.Title) + fmt.Printf(" Source: %s\n", note.Path) + fmt.Printf(" Public: public/%s\n", htmlFile) + fmt.Printf(" Date: %s\n\n", note.PublishedAt.Format("2006-01-02 15:04:05")) + } +} + +func runServer(addr, notesDir string, logger *log.Logger) { ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() - if err := ensureDir(*notesDir); err != nil { + if err := ensureDir(notesDir); err != nil { logger.Fatalf("repertoire notes invalide: %v", err) } idx := indexer.New() - if err := idx.Load(*notesDir); err != nil { + if err := idx.Load(notesDir); err != nil { logger.Fatalf("echec de l indexation initiale: %v", err) } @@ -45,7 +108,7 @@ func main() { } logger.Printf("traductions chargees: %v", translator.GetAvailableLanguages()) - w, err := watcher.Start(ctx, *notesDir, idx, logger) + w, err := watcher.Start(ctx, notesDir, idx, logger) if err != nil { logger.Fatalf("echec du watcher: %v", err) } @@ -63,6 +126,12 @@ func main() { mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static")))) mux.Handle("/frontend/", http.StripPrefix("/frontend/", http.FileServer(http.Dir("./frontend")))) + // Servir les fichiers HTML publics (notes exportées) + publicDir := filepath.Join(notesDir, "..", "public") + if _, err := os.Stat(publicDir); err == nil { + mux.Handle("/public/", http.StripPrefix("/public/", http.FileServer(http.Dir(publicDir)))) + } + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { http.NotFound(w, r) @@ -77,7 +146,7 @@ func main() { } }) - apiHandler := api.NewHandler(*notesDir, idx, templates, logger, translator) + apiHandler := api.NewHandler(notesDir, idx, templates, logger, translator) mux.Handle("/api/i18n/", apiHandler) // I18n translations mux.Handle("/api/v1/notes", apiHandler) // REST API v1 mux.Handle("/api/v1/notes/", apiHandler) // REST API v1 @@ -93,16 +162,18 @@ func main() { mux.Handle("/api/folder/", apiHandler) // Folder view mux.Handle("/api/notes/", apiHandler) mux.Handle("/api/tree", apiHandler) + mux.Handle("/api/public/list", apiHandler) // List public notes + mux.Handle("/api/public/toggle", apiHandler) // Toggle public status (génère HTML statique) srv := &http.Server{ - Addr: *addr, + Addr: addr, Handler: mux, ReadTimeout: 15 * time.Second, WriteTimeout: 15 * time.Second, IdleTimeout: 60 * time.Second, } - logger.Printf("demarrage du serveur sur %s", *addr) + logger.Printf("demarrage du serveur sur %s", addr) go func() { <-ctx.Done() diff --git a/docs/EXPORT_GUIDE.md b/docs/EXPORT_GUIDE.md new file mode 100644 index 0000000..2e57919 --- /dev/null +++ b/docs/EXPORT_GUIDE.md @@ -0,0 +1,360 @@ +# 📤 Export de Notes Publiques - Guide Complet + +## 🎯 Concept + +Les notes publiques sont **générées en fichiers HTML statiques** directement sur le serveur. Cela signifie que vous pouvez copier le dossier `public/` et le déployer sur n'importe quel serveur web (Apache, Nginx, GitHub Pages, Netlify, etc.) **sans avoir besoin de Go**. + +## 🔍 Lister les notes publiques + +Vous pouvez lister toutes les notes publiques avec la commande CLI intégrée : + +```bash +./server list-public +``` + +**Sortie** : +``` +📚 Notes publiques (4): + +• Mon Guide + Source: personal/guide.md + Public: public/guide.html + Date: 2025-11-13 20:06:21 +``` + +Cette commande lit le fichier `notes/.public.json` et affiche : +- Le titre de chaque note +- Son chemin source +- Son chemin public +- La date de publication + +## 📁 Structure générée + +Quand vous publiez une note, le système génère automatiquement : + +``` +public/ +├── index.html # Liste de toutes les notes publiques +├── ma-note.html # Notes converties en HTML (structure plate) +├── autre.html +└── static/ + ├── theme.css # Styles CSS copiés + └── themes.css +``` + +**Note** : La structure est plate - toutes les notes publiques sont directement dans `public/`, peu importe leur emplacement d'origine dans vos dossiers personnels. + +## 🔄 Génération automatique + +### Quand une note est publiée + +1. **L'utilisateur clique sur "🔒 Privé"** dans l'éditeur +2. Le système : + - Lit le fichier Markdown + - Extrait le front matter (titre, tags, date) + - Convertit le Markdown en HTML avec goldmark + - Génère un fichier HTML standalone complet + - Copie les CSS nécessaires + - Régénère `index.html` avec la nouvelle note +3. **Le bouton devient "🌐 Public"** + +### Quand une note est retirée + +1. **L'utilisateur clique sur "🌐 Public"** +2. Le système : + - Supprime le fichier HTML correspondant + - Régénère `index.html` sans cette note +3. **Le bouton redevient "🔒 Privé"** + +## 📋 Emplacement des fichiers + +- **Source** : `notes/` (vos fichiers Markdown originaux) +- **Export** : `public/` (fichiers HTML générés) +- **Index** : `.public.json` (liste des notes publiques) + +## 🚀 Déploiement + +### Option 1 : Copie manuelle sur serveur web + +```bash +# Copier le dossier public/ sur votre serveur +scp -r public/ user@server.com:/var/www/html/notes/ + +# Ou avec rsync +rsync -av public/ user@server.com:/var/www/html/notes/ +``` + +**Configuration Nginx** : +```nginx +server { + listen 80; + server_name notes.example.com; + + root /var/www/html/notes; + index index.html; + + location / { + try_files $uri $uri/ =404; + } +} +``` + +**Configuration Apache** : +```apache + + ServerName notes.example.com + DocumentRoot /var/www/html/notes + + + Options Indexes FollowSymLinks + AllowOverride All + Require all granted + + +``` + +### Option 2 : GitHub Pages (gratuit) + +```bash +# 1. Créer un repo GitHub +git init public/ +cd public/ +git add . +git commit -m "Initial public notes" + +# 2. Pousser vers GitHub +git remote add origin https://github.com/username/notes-public.git +git push -u origin main + +# 3. Activer GitHub Pages +# Settings → Pages → Source: main branch → Save +``` + +Vos notes seront disponibles à : `https://username.github.io/notes-public/` + +### Option 3 : Netlify Drop (ultra simple) + +1. Allez sur https://app.netlify.com/drop +2. Glissez-déposez le dossier `public/` +3. Netlify génère automatiquement une URL + +### Option 4 : Vercel + +```bash +cd public/ +npx vercel +``` + +## 🔄 Automatisation avec script + +Créez un script pour synchroniser automatiquement : + +```bash +#!/bin/bash +# sync-public.sh + +# Serveur de destination +REMOTE_USER="user" +REMOTE_HOST="server.com" +REMOTE_PATH="/var/www/html/notes" + +# Synchroniser +rsync -av --delete public/ ${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_PATH} + +echo "✅ Notes publiques synchronisées !" +``` + +Utilisation : +```bash +chmod +x sync-public.sh +./sync-public.sh +``` + +## 🔄 Automatisation avec Git Hook + +Synchroniser automatiquement après chaque publication : + +```bash +# .git/hooks/post-commit +#!/bin/bash + +# Si le dossier public/ a changé, synchroniser +if git diff --name-only HEAD~1 | grep -q "^public/"; then + ./sync-public.sh +fi +``` + +## 📊 Avantages de cette approche + +### ✅ Performance +- HTML pré-généré = temps de chargement instantané +- Pas de backend requis = moins de latence +- Cacheable à 100% par CDN + +### ✅ Sécurité +- Fichiers statiques = surface d'attaque minimale +- Pas de code serveur exécuté côté public +- Isolation complète du système d'édition + +### ✅ Portabilité +- Fonctionne sur **n'importe quel serveur web** +- Hébergement gratuit possible (GitHub Pages, Netlify) +- Peut être mis sur un CDN (Cloudflare, etc.) + +### ✅ Simplicité +- Pas besoin de Go sur le serveur de destination +- Pas de base de données +- Juste des fichiers HTML à copier + +### ✅ SEO +- HTML pré-rendu = indexation optimale par Google +- Pas de JavaScript requis pour afficher le contenu +- Meta tags facilement ajoutables + +## 🎨 Personnalisation + +### Modifier les styles + +Les fichiers CSS sont dans `public/static/`. Vous pouvez les modifier directement : + +```bash +# Éditer le CSS +nano public/static/theme.css + +# Ou copier vos propres CSS +cp my-custom.css public/static/custom.css +``` + +### Ajouter des meta tags + +Modifiez `internal/api/public.go` dans la fonction `generateStandaloneHTML()` : + +```go + + + + + + + + %s - PersoNotes +``` + +### Ajouter Google Analytics + +Ajoutez dans `generateStandaloneHTML()` avant `` : + +```html + + + +``` + +## 🐛 Dépannage + +### Les notes ne s'affichent pas + +**Problème** : `/public/` retourne 404 + +**Solutions** : +1. Vérifier que le dossier `public/` existe : `ls -la public/` +2. Publier au moins une note pour générer le dossier +3. Redémarrer le serveur Go + +### Les styles ne s'appliquent pas + +**Problème** : La page s'affiche sans CSS + +**Solutions** : +1. Vérifier que `public/static/theme.css` existe +2. Les chemins CSS sont relatifs : `../static/theme.css` +3. Si vous copiez ailleurs, ajustez les chemins + +### Le HTML contient du Markdown brut + +**Problème** : Le Markdown n'est pas converti + +**Solutions** : +1. Vérifier que goldmark est installé : `go mod tidy` +2. Republier la note (toggle Private → Public) +3. Vérifier les logs du serveur + +## 📝 Workflow recommandé + +### Workflow quotidien + +1. **Écrire** vos notes en Markdown dans l'éditeur +2. **Publier** les notes que vous voulez partager (bouton Public) +3. **Synchroniser** le dossier `public/` vers votre serveur (manuel ou automatique) + +### Workflow avec Git + +```bash +# 1. Publier des notes via l'interface web +# 2. Commiter les changements +git add public/ +git commit -m "Publish new notes" +git push + +# 3. Sur le serveur de production +git pull +# Les nouvelles notes sont disponibles ! +``` + +## 🔐 Sécurité + +### Routes publiques (pas d'auth) +- ✅ `/public/*` - Fichiers HTML statiques accessibles à tous + +### Routes privées (nécessitent auth) +- 🔒 `/` - Interface d'édition +- 🔒 `/api/*` - APIs de modification +- 🔒 `/api/public/toggle` - **Protéger cet endpoint !** + +### Protection recommandée + +Utilisez un reverse proxy avec authentification pour protéger l'édition : + +```nginx +# Nginx +location /public { + # Pas d'auth - accessible à tous + proxy_pass http://localhost:8080; +} + +location / { + # Auth requise pour édition + auth_basic "Personotes Admin"; + auth_basic_user_file /etc/nginx/.htpasswd; + proxy_pass http://localhost:8080; +} +``` + +## 📚 Ressources + +- **Goldmark** : https://github.com/yuin/goldmark +- **GitHub Pages** : https://pages.github.com +- **Netlify** : https://www.netlify.com +- **Vercel** : https://vercel.com + +## ❓ FAQ + +**Q: Puis-je personnaliser le design des pages publiques ?** +R: Oui ! Modifiez `public/static/theme.css` ou éditez la fonction `generateStandaloneHTML()` dans `internal/api/public.go`. + +**Q: Les images dans mes notes fonctionnent-elles ?** +R: Oui, si vous utilisez des URLs absolues ou si vous copiez les images dans `public/static/images/`. + +**Q: Puis-je exporter vers PDF ?** +R: Les fichiers HTML peuvent être convertis en PDF avec wkhtmltopdf ou un navigateur (Imprimer → PDF). + +**Q: Comment supprimer toutes les notes publiques d'un coup ?** +R: Supprimez le dossier `public/` et le fichier `.public.json`, puis relancez le serveur. + +**Q: Les modifications des notes sont-elles automatiquement republiées ?** +R: Non. Si vous modifiez une note Markdown qui est déjà publique, vous devez la republier (toggle Private puis Public) pour régénérer le HTML. diff --git a/frontend/src/favorites.js b/frontend/src/favorites.js index f216c16..ccef874 100644 --- a/frontend/src/favorites.js +++ b/frontend/src/favorites.js @@ -133,13 +133,13 @@ class FavoritesManager { attachFavoriteButtons() { debug('attachFavoriteButtons: Début...'); - // Supprimer tous les boutons favoris existants pour les recréer avec le bon état - document.querySelectorAll('.add-to-favorites').forEach(btn => btn.remove()); - // Ajouter des boutons étoile aux éléments du file tree this.getFavoritesPaths().then(favoritePaths => { debug('Chemins favoris:', favoritePaths); + // Supprimer tous les boutons favoris existants APRÈS avoir récupéré la liste + document.querySelectorAll('.add-to-favorites').forEach(btn => btn.remove()); + // Dossiers const folderHeaders = document.querySelectorAll('.folder-header'); debug('Nombre de folder-header trouvés:', folderHeaders.length); @@ -153,7 +153,7 @@ class FavoritesManager { if (path) { const button = document.createElement('button'); button.className = 'add-to-favorites'; - button.innerHTML = '⭐'; + button.innerHTML = ''; button.title = 'Ajouter aux favoris'; // Extraire le nom avant d'ajouter le bouton @@ -191,11 +191,11 @@ class FavoritesManager { if (path) { const button = document.createElement('button'); button.className = 'add-to-favorites'; - button.innerHTML = '⭐'; + button.innerHTML = ''; button.title = 'Ajouter aux favoris'; // Extraire le nom avant d'ajouter le bouton - const name = fileItem.textContent.trim().replace('📄', '').trim().replace('.md', ''); + const name = fileItem.textContent.trim().replace('.md', ''); button.onclick = (e) => { e.preventDefault(); @@ -220,6 +220,11 @@ class FavoritesManager { }); debug('attachFavoriteButtons: Terminé'); + + // Initialiser les icônes Lucide après création des boutons + if (typeof lucide !== 'undefined') { + lucide.createIcons(); + } }); } } diff --git a/frontend/src/file-tree.js b/frontend/src/file-tree.js index 86b5ff2..ca6992a 100644 --- a/frontend/src/file-tree.js +++ b/frontend/src/file-tree.js @@ -13,6 +13,9 @@ class FileTree { init() { this.setupEventListeners(); + // Restaurer l'état des dossiers au démarrage + setTimeout(() => this.restoreFolderStates(), 500); + debug('FileTree initialized with event delegation'); } @@ -67,17 +70,60 @@ class FileTree { const children = folderItem.querySelector('.folder-children'); const toggle = header.querySelector('.folder-toggle'); const icon = header.querySelector('.folder-icon'); + const folderPath = folderItem.getAttribute('data-path'); if (children.style.display === 'none') { // Ouvrir le dossier children.style.display = 'block'; toggle.classList.add('expanded'); - icon.textContent = '📂'; + icon.innerHTML = ''; + this.saveFolderState(folderPath, true); } else { // Fermer le dossier children.style.display = 'none'; toggle.classList.remove('expanded'); - icon.textContent = '📁'; + icon.innerHTML = ''; + this.saveFolderState(folderPath, false); + } + } + + saveFolderState(folderPath, isExpanded) { + if (!folderPath) return; + const expandedFolders = this.getExpandedFolders(); + if (isExpanded) { + expandedFolders.add(folderPath); + } else { + expandedFolders.delete(folderPath); + } + localStorage.setItem('expanded-folders', JSON.stringify([...expandedFolders])); + } + + getExpandedFolders() { + const saved = localStorage.getItem('expanded-folders'); + return saved ? new Set(JSON.parse(saved)) : new Set(); + } + + restoreFolderStates() { + const expandedFolders = this.getExpandedFolders(); + document.querySelectorAll('.folder-item').forEach(folderItem => { + const folderPath = folderItem.getAttribute('data-path'); + if (folderPath && expandedFolders.has(folderPath)) { + const header = folderItem.querySelector('.folder-header'); + const children = folderItem.querySelector('.folder-children'); + const toggle = header?.querySelector('.folder-toggle'); + const icon = header?.querySelector('.folder-icon'); + + if (children && toggle && icon) { + children.style.display = 'block'; + toggle.classList.add('expanded'); + icon.innerHTML = ''; + } + } + }); + + // Réinitialiser les icônes Lucide + if (typeof lucide !== 'undefined') { + lucide.createIcons(); } } @@ -159,8 +205,9 @@ class FileTree { // Vérifier si le swap concerne le file-tree const target = event.detail?.target; if (target && (target.id === 'file-tree' || target.closest('#file-tree'))) { - debug('FileTree: afterSwap detected, updating attributes...'); + debug('FileTree: afterSwap detected, updating attributes and restoring folder states...'); this.updateDraggableAttributes(); + setTimeout(() => this.restoreFolderStates(), 50); } }); @@ -169,8 +216,9 @@ class FileTree { const target = event.detail?.target; // Ignorer les swaps de statut (auto-save-status, save-status) if (target && target.id === 'file-tree') { - debug('FileTree: oobAfterSwap detected, updating attributes...'); + debug('FileTree: oobAfterSwap detected, updating attributes and restoring folder states...'); this.updateDraggableAttributes(); + setTimeout(() => this.restoreFolderStates(), 50); } }); @@ -181,6 +229,7 @@ class FileTree { setTimeout(() => { this.setupEventListeners(); this.updateDraggableAttributes(); + this.restoreFolderStates(); }, 50); }); } @@ -678,7 +727,7 @@ class SelectionManager { const checkbox = document.querySelector(`.selection-checkbox[data-path="${path}"]`); const isDir = checkbox?.dataset.isDir === 'true'; - li.innerHTML = `${isDir ? '📁' : '📄'} ${path}`; + li.innerHTML = `${isDir ? '' : ''} ${path}`; ul.appendChild(li); }); diff --git a/frontend/src/i18n.js b/frontend/src/i18n.js index acc8999..9a72421 100644 --- a/frontend/src/i18n.js +++ b/frontend/src/i18n.js @@ -231,8 +231,10 @@ class I18n { // Create singleton instance export const i18n = new I18n(); -// Export convenience function +// Export convenience functions export const t = (key, args) => i18n.t(key, args); +export const loadTranslations = () => i18n.loadTranslations(); +export const translatePage = () => i18n.translatePage(); // Initialize on import i18n.init().then(() => { diff --git a/frontend/src/language-manager.js b/frontend/src/language-manager.js index 2d12843..ee1a913 100644 --- a/frontend/src/language-manager.js +++ b/frontend/src/language-manager.js @@ -181,12 +181,12 @@ class LanguageManager { // Header buttons const homeButton = document.querySelector('button[hx-get="/api/home"]'); if (homeButton && !homeButton.hasAttribute('data-i18n')) { - homeButton.innerHTML = `🏠 ${t('menu.home')}`; + homeButton.innerHTML = ` ${t('menu.home')}`; } const newNoteButton = document.querySelector('header button[onclick="showNewNoteModal()"]'); if (newNoteButton && !newNoteButton.hasAttribute('data-i18n')) { - newNoteButton.innerHTML = `✨ ${t('menu.newNote')}`; + newNoteButton.innerHTML = ` ${t('menu.newNote')}`; } // Search placeholder @@ -199,7 +199,7 @@ class LanguageManager { const newNoteModal = document.getElementById('new-note-modal'); if (newNoteModal) { const title = newNoteModal.querySelector('h2'); - if (title) title.textContent = `📝 ${t('newNoteModal.title')}`; + if (title) title.innerHTML = ` ${t('newNoteModal.title')}`; const label = newNoteModal.querySelector('label[for="note-name"]'); if (label) label.textContent = t('newNoteModal.label'); @@ -218,7 +218,7 @@ class LanguageManager { const newFolderModal = document.getElementById('new-folder-modal'); if (newFolderModal) { const title = newFolderModal.querySelector('h2'); - if (title) title.textContent = `📁 ${t('newFolderModal.title')}`; + if (title) title.innerHTML = ` ${t('newFolderModal.title')}`; const label = newFolderModal.querySelector('label[for="folder-name"]'); if (label) label.textContent = t('newFolderModal.label'); @@ -256,16 +256,16 @@ class LanguageManager { // Theme modal const modalTitle = document.querySelector('.theme-modal-content h2'); if (modalTitle) { - modalTitle.textContent = `⚙️ ${t('settings.title')}`; + modalTitle.innerHTML = ` ${t('settings.title')}`; } // Translate tabs const tabs = document.querySelectorAll('.settings-tab'); if (tabs.length >= 4) { - tabs[0].innerHTML = `🎨 ${t('tabs.themes')}`; - tabs[1].innerHTML = `🔤 ${t('tabs.fonts')}`; - tabs[2].innerHTML = `⌨️ ${t('tabs.shortcuts')}`; - tabs[3].innerHTML = `⚙️ ${t('tabs.other')}`; + tabs[0].innerHTML = ` ${t('tabs.themes')}`; + tabs[1].innerHTML = ` ${t('tabs.fonts')}`; + tabs[2].innerHTML = ` ${t('tabs.shortcuts')}`; + tabs[3].innerHTML = ` ${t('tabs.other')}`; } // Translate close button in settings @@ -281,20 +281,20 @@ class LanguageManager { if (langSection) { const heading = langSection.querySelector('h3'); if (heading) { - heading.textContent = `🌍 ${t('languages.title')}`; + heading.innerHTML = ` ${t('languages.title')}`; } } // Sidebar sections const searchSectionTitle = document.querySelector('.sidebar-section-title'); - if (searchSectionTitle && searchSectionTitle.textContent.includes('🔍')) { - searchSectionTitle.textContent = `🔍 ${t('search.title') || 'Recherche'}`; + if (searchSectionTitle && (searchSectionTitle.textContent.includes('🔍') || searchSectionTitle.querySelector('[data-lucide="search"]'))) { + searchSectionTitle.innerHTML = ` ${t('search.title') || 'Recherche'}`; } // Sidebar "Nouveau dossier" button const newFolderBtn = document.querySelector('.folder-create-btn'); if (newFolderBtn && !newFolderBtn.hasAttribute('data-i18n')) { - newFolderBtn.innerHTML = `📁 ${t('fileTree.newFolder')}`; + newFolderBtn.innerHTML = ` ${t('fileTree.newFolder')}`; } // Sidebar "Paramètres" button span diff --git a/frontend/src/link-inserter.js b/frontend/src/link-inserter.js index 0d236e5..90bf4d2 100644 --- a/frontend/src/link-inserter.js +++ b/frontend/src/link-inserter.js @@ -309,7 +309,7 @@ class LinkInserter { this.results = []; this.resultsContainer.innerHTML = ` `; @@ -318,7 +318,7 @@ class LinkInserter { showError() { this.resultsContainer.innerHTML = ` `; diff --git a/frontend/src/main.js b/frontend/src/main.js index cadc6d5..128a624 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -12,3 +12,7 @@ import './vim-mode-manager.js'; import './favorites.js'; import './sidebar-sections.js'; import './keyboard-shortcuts.js'; +import { initPublicToggle } from './public-toggle.js'; + +// Initialiser le toggle public +initPublicToggle(); diff --git a/frontend/src/public-toggle.js b/frontend/src/public-toggle.js new file mode 100644 index 0000000..83229a8 --- /dev/null +++ b/frontend/src/public-toggle.js @@ -0,0 +1,145 @@ +// public-toggle.js - Gestion du statut public/privé des notes +import { t } from './i18n.js'; + +/** + * Initialise le gestionnaire de toggle public + */ +export function initPublicToggle() { + // Event delegation sur le body pour gérer les boutons dynamiques + document.body.addEventListener('click', (e) => { + const btn = e.target.closest('#toggle-public-btn'); + if (!btn) return; + + e.preventDefault(); + togglePublicStatus(btn); + }); +} + +/** + * Bascule le statut public d'une note + * @param {HTMLElement} btn - Le bouton cliqué + */ +async function togglePublicStatus(btn) { + const path = btn.dataset.path; + const isCurrentlyPublic = btn.dataset.isPublic === 'true'; + + if (!path) { + console.error('Pas de chemin de note trouvé'); + return; + } + + // Désactiver le bouton pendant la requête + btn.disabled = true; + const originalText = btn.textContent; + btn.textContent = '⏳ ' + t('public.loading'); + + try { + const response = await fetch('/api/public/toggle', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ path }), + }); + + if (!response.ok) { + throw new Error(`HTTP Error: ${response.status}`); + } + + const data = await response.json(); + const isNowPublic = data.status === 'public'; + + // Mettre à jour le bouton + btn.dataset.isPublic = isNowPublic; + + // Mettre à jour le texte et le titre avec i18n + const textKey = isNowPublic ? 'public.buttonPublic' : 'public.buttonPrivate'; + const titleKey = isNowPublic ? 'public.titlePublic' : 'public.titlePrivate'; + + btn.innerHTML = `${isNowPublic ? '🌐 ' + t('public.buttonPublic') : '🔒 ' + t('public.buttonPrivate')}`; + btn.title = t(titleKey); + btn.setAttribute('data-i18n-title', titleKey); + + // Ajouter/retirer la classe CSS + if (isNowPublic) { + btn.classList.add('public-active'); + } else { + btn.classList.remove('public-active'); + } + + // Afficher une notification de succès + const messageKey = isNowPublic ? 'public.notificationPublished' : 'public.notificationUnpublished'; + showNotification(t(messageKey)); + + } catch (error) { + console.error('Error toggling public status:', error); + btn.textContent = originalText; + showNotification(t('public.notificationError'), 'error'); + } finally { + btn.disabled = false; + } +} + +/** + * Affiche une notification temporaire + * @param {string} message - Le message à afficher + * @param {string} type - Type de notification (success, error) + */ +function showNotification(message, type = 'success') { + // Créer l'élément de notification + const notification = document.createElement('div'); + notification.className = `public-notification ${type}`; + notification.textContent = message; + notification.style.cssText = ` + position: fixed; + bottom: 2rem; + right: 2rem; + background: ${type === 'error' ? 'var(--error-red, #e74c3c)' : 'var(--success-green, #2ecc71)'}; + color: white; + padding: 1rem 1.5rem; + border-radius: 6px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + z-index: 10000; + animation: slideIn 0.3s ease-out; + font-weight: 500; + `; + + // Ajouter l'animation + const style = document.createElement('style'); + style.textContent = ` + @keyframes slideIn { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } + } + @keyframes slideOut { + from { + transform: translateX(0); + opacity: 1; + } + to { + transform: translateX(100%); + opacity: 0; + } + } + `; + if (!document.querySelector('style[data-public-notification]')) { + style.setAttribute('data-public-notification', 'true'); + document.head.appendChild(style); + } + + document.body.appendChild(notification); + + // Retirer après 3 secondes + setTimeout(() => { + notification.style.animation = 'slideOut 0.3s ease-out'; + setTimeout(() => { + notification.remove(); + }, 300); + }, 3000); +} diff --git a/frontend/src/search.js b/frontend/src/search.js index 965cc73..3277966 100644 --- a/frontend/src/search.js +++ b/frontend/src/search.js @@ -51,7 +51,7 @@ class SearchModal {
-
💡 Recherche avancée
+
Recherche avancée
tag:projet @@ -289,7 +289,7 @@ class SearchModal { this.results = []; this.resultsContainer.innerHTML = `
-
💡 Recherche avancée
+
Recherche avancée
tag:projet @@ -325,7 +325,7 @@ class SearchModal { this.results = []; this.resultsContainer.innerHTML = `
-
🔍
+

Aucun résultat pour « ${this.escapeHtml(query)} »

Essayez d'autres mots-clés ou utilisez les filtres

@@ -335,7 +335,7 @@ class SearchModal { showError() { this.resultsContainer.innerHTML = `
-
⚠️
+

Une erreur s'est produite lors de la recherche

`; diff --git a/frontend/src/theme-manager.js b/frontend/src/theme-manager.js index 86a6f87..f27af17 100644 --- a/frontend/src/theme-manager.js +++ b/frontend/src/theme-manager.js @@ -9,49 +9,49 @@ class ThemeManager { { id: 'material-dark', name: 'Material Dark', - icon: '🌙', + icon: 'moon', description: 'Thème professionnel inspiré de Material Design' }, { id: 'monokai-dark', name: 'Monokai Dark', - icon: '🎨', + icon: 'palette', description: 'Palette Monokai classique pour les développeurs' }, { id: 'dracula', name: 'Dracula', - icon: '🧛', + icon: 'moon-star', description: 'Thème sombre élégant avec des accents violets et cyan' }, { id: 'one-dark', name: 'One Dark', - icon: '⚡', + icon: 'zap', description: 'Thème populaire d\'Atom avec des couleurs douces' }, { id: 'solarized-dark', name: 'Solarized Dark', - icon: '☀️', + icon: 'sun', description: 'Palette scientifiquement optimisée pour réduire la fatigue oculaire' }, { id: 'nord', name: 'Nord', - icon: '❄️', + icon: 'snowflake', description: 'Palette arctique apaisante avec des tons bleus froids' }, { id: 'catppuccin', name: 'Catppuccin', - icon: '🌸', + icon: 'flower-2', description: 'Thème pastel doux et chaleureux avec des accents roses et bleus' }, { id: 'everforest', name: 'Everforest', - icon: '🌲', + icon: 'tree-pine', description: 'Palette naturelle inspirée de la forêt avec des tons verts et beiges' } ]; diff --git a/go.mod b/go.mod index 76935eb..bb27ddc 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.22 require ( github.com/fsnotify/fsnotify v1.7.0 + github.com/yuin/goldmark v1.7.13 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index 37fe95d..d3c03bc 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA= +github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/internal/api/handler.go b/internal/api/handler.go index c39d179..ba5bee8 100644 --- a/internal/api/handler.go +++ b/internal/api/handler.go @@ -71,6 +71,16 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } + // Public endpoints + if path == "/api/public/list" { + h.handlePublicList(w, r) + return + } + if path == "/api/public/toggle" { + h.handlePublicToggle(w, r) + return + } + // Legacy/HTML endpoints if strings.HasPrefix(path, "/api/search") { h.handleSearch(w, r) @@ -295,12 +305,14 @@ func (h *Handler) handleHome(w http.ResponseWriter, r *http.Request) { Filename string Content string IsHome bool + IsPublic bool Backlinks []BacklinkInfo Breadcrumb template.HTML }{ Filename: "🏠 Accueil - Index", Content: content, IsHome: true, + IsPublic: false, Backlinks: nil, // Pas de backlinks pour la page d'accueil Breadcrumb: h.generateBreadcrumb(""), } @@ -351,7 +363,10 @@ func (h *Handler) generateHomeMarkdown(r *http.Request) string { // Section des favoris (après les tags) h.generateFavoritesSection(&sb, r) - // Section des notes récemment modifiées (après les favoris) + // Section des notes publiques (après les favoris) + h.generatePublicNotesSection(&sb, r) + + // Section des notes récemment modifiées (après les notes publiques) h.generateRecentNotesSection(&sb, r) // Section de toutes les notes avec accordéon @@ -380,7 +395,7 @@ func (h *Handler) generateTagsSection(sb *strings.Builder) { sb.WriteString("
\n") sb.WriteString("
\n") - sb.WriteString("

🏷️ Tags

\n") + sb.WriteString("

Tags

\n") sb.WriteString("
\n") sb.WriteString("
\n") sb.WriteString("
\n") @@ -408,7 +423,7 @@ func (h *Handler) generateFavoritesSection(sb *strings.Builder, r *http.Request) sb.WriteString("
\n") sb.WriteString("
\n") - sb.WriteString("

⭐ " + h.t(r, "favorites.title") + "

\n") + sb.WriteString("

" + h.t(r, "favorites.title") + "

\n") sb.WriteString("
\n") sb.WriteString("
\n") sb.WriteString("
\n") @@ -420,7 +435,7 @@ func (h *Handler) generateFavoritesSection(sb *strings.Builder, r *http.Request) // Dossier - avec accordéon sb.WriteString(fmt.Sprintf("
\n")) sb.WriteString(fmt.Sprintf("
\n", safeID)) - sb.WriteString(fmt.Sprintf(" 📁\n", safeID)) + sb.WriteString(fmt.Sprintf(" \n", safeID)) sb.WriteString(fmt.Sprintf(" %s\n", fav.Title)) sb.WriteString(fmt.Sprintf("
\n")) sb.WriteString(fmt.Sprintf("
\n", safeID)) @@ -445,6 +460,44 @@ func (h *Handler) generateFavoritesSection(sb *strings.Builder, r *http.Request) sb.WriteString("
\n\n") } +// generatePublicNotesSection génère la section des notes publiques +func (h *Handler) generatePublicNotesSection(sb *strings.Builder, r *http.Request) { + publicNotes, err := h.loadPublicNotes() + if err != nil || len(publicNotes.Notes) == 0 { + return + } + + sb.WriteString("
\n") + sb.WriteString("
\n") + sb.WriteString(fmt.Sprintf("

%s (%d)

\n", h.t(r, "publicNotes.title"), len(publicNotes.Notes))) + sb.WriteString("
\n") + sb.WriteString("
\n") + sb.WriteString("
\n") + + for _, note := range publicNotes.Notes { + filename := filepath.Base(note.Path) + htmlFile := filename[:len(filename)-3] + ".html" + publicURL := fmt.Sprintf("/public/%s", htmlFile) + + sb.WriteString("
\n") + sb.WriteString(fmt.Sprintf("
\n")) + sb.WriteString(fmt.Sprintf(" ", note.Path, h.t(r, "publicNotes.editNote"))) + sb.WriteString(fmt.Sprintf(" %s", note.Title)) + sb.WriteString(" \n") + sb.WriteString(fmt.Sprintf(" 🌐\n", publicURL, h.t(r, "publicNotes.viewPublic"))) + sb.WriteString("
\n") + sb.WriteString(fmt.Sprintf("
\n")) + sb.WriteString(fmt.Sprintf(" 📄 %s\n", note.Path)) + sb.WriteString(fmt.Sprintf(" %s\n", note.PublishedAt.Format("02/01/2006"))) + sb.WriteString("
\n") + sb.WriteString("
\n") + } + + sb.WriteString("
\n") + sb.WriteString("
\n") + sb.WriteString("
\n\n") +} + // generateRecentNotesSection génère la section des notes récemment modifiées func (h *Handler) generateRecentNotesSection(sb *strings.Builder, r *http.Request) { recentDocs := h.idx.GetRecentDocuments(5) @@ -477,7 +530,7 @@ func (h *Handler) generateRecentNotesSection(sb *strings.Builder, r *http.Reques sb.WriteString(fmt.Sprintf(" \n", doc.Path)) sb.WriteString(fmt.Sprintf("
%s
\n", doc.Title)) sb.WriteString(fmt.Sprintf("
\n")) - sb.WriteString(fmt.Sprintf(" 📅 %s\n", dateStr)) + sb.WriteString(fmt.Sprintf(" %s\n", dateStr)) if len(doc.Tags) > 0 { sb.WriteString(fmt.Sprintf(" ")) for i, tag := range doc.Tags { @@ -523,7 +576,7 @@ func (h *Handler) generateFavoriteFolderContent(sb *strings.Builder, folderPath // Sous-dossier sb.WriteString(fmt.Sprintf("%s
\n", indent, indentClass)) sb.WriteString(fmt.Sprintf("%s
\n", indent, safeID)) - sb.WriteString(fmt.Sprintf("%s 📁\n", indent, safeID)) + sb.WriteString(fmt.Sprintf("%s \n", indent, safeID)) sb.WriteString(fmt.Sprintf("%s %s\n", indent, name)) sb.WriteString(fmt.Sprintf("%s
\n", indent)) sb.WriteString(fmt.Sprintf("%s
\n", indent, safeID)) @@ -579,7 +632,7 @@ func (h *Handler) generateMarkdownTree(sb *strings.Builder, node *TreeNode, dept indentClass := fmt.Sprintf("indent-level-%d", depth) sb.WriteString(fmt.Sprintf("%s
\n", indent, indentClass)) sb.WriteString(fmt.Sprintf("%s
\n", indent, safeID)) - sb.WriteString(fmt.Sprintf("%s 📁\n", indent, safeID)) + sb.WriteString(fmt.Sprintf("%s \n", indent, safeID)) sb.WriteString(fmt.Sprintf("%s %s\n", indent, node.Name)) sb.WriteString(fmt.Sprintf("%s
\n", indent)) sb.WriteString(fmt.Sprintf("%s
\n", indent, safeID)) @@ -688,12 +741,14 @@ func (h *Handler) createAndRenderNote(w http.ResponseWriter, r *http.Request, fi Filename string Content string IsHome bool + IsPublic bool Backlinks []BacklinkInfo }{ Filename: filename, Content: initialContent, IsHome: false, - Backlinks: nil, // Pas de backlinks pour une nouvelle note + IsPublic: false, // Nouvelle note, pas publique par défaut + Backlinks: nil, // Pas de backlinks pour une nouvelle note } err = h.templates.ExecuteTemplate(w, "editor.html", data) @@ -832,12 +887,14 @@ func (h *Handler) handleGetNote(w http.ResponseWriter, r *http.Request, filename Filename string Content string IsHome bool + IsPublic bool Backlinks []BacklinkInfo Breadcrumb template.HTML }{ Filename: filename, Content: string(content), IsHome: false, + IsPublic: h.isPublic(filename), Backlinks: backlinkData, Breadcrumb: h.generateBreadcrumb(filename), } @@ -1347,15 +1404,17 @@ func (h *Handler) handleFolderView(w http.ResponseWriter, r *http.Request) { // Utiliser le template editor.html data := struct { - Filename string - Content string - IsHome bool - Backlinks []BacklinkInfo + Filename string + Content string + IsHome bool + IsPublic bool + Backlinks []BacklinkInfo Breadcrumb template.HTML }{ Filename: cleanPath, Content: content, IsHome: true, // Pas d'édition pour une vue de dossier + IsPublic: false, Backlinks: nil, Breadcrumb: h.generateBreadcrumb(cleanPath), } @@ -1370,7 +1429,7 @@ func (h *Handler) handleFolderView(w http.ResponseWriter, r *http.Request) { // generateBreadcrumb génère un fil d'Ariane HTML cliquable func (h *Handler) generateBreadcrumb(path string) template.HTML { if path == "" { - return template.HTML(`📁 Racine`) + return template.HTML(` Racine`) } parts := strings.Split(filepath.ToSlash(path), "/") @@ -1379,7 +1438,7 @@ func (h *Handler) generateBreadcrumb(path string) template.HTML { sb.WriteString(``) // Lien racine - sb.WriteString(`📁 Racine`) + sb.WriteString(` Racine`) // Construire les liens pour chaque partie currentPath := "" diff --git a/internal/api/public.go b/internal/api/public.go new file mode 100644 index 0000000..39dcb58 --- /dev/null +++ b/internal/api/public.go @@ -0,0 +1,607 @@ +package api + +import ( + "bytes" + "encoding/json" + "fmt" + "html/template" + "io" + "net/http" + "os" + "path/filepath" + "sort" + "strings" + "time" + + "github.com/mathieu/personotes/internal/indexer" + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/extension" + "github.com/yuin/goldmark/parser" + "github.com/yuin/goldmark/renderer/html" +) + +// PublicNote représente une note partagée publiquement +type PublicNote struct { + Path string `json:"path"` + Title string `json:"title"` + PublishedAt time.Time `json:"published_at"` +} + +// PublicNotesData contient la liste des notes publiques +type PublicNotesData struct { + Notes []PublicNote `json:"notes"` +} + +// getPublicDirPath retourne le chemin du dossier public HTML +func (h *Handler) getPublicDirPath() string { + return filepath.Join(h.notesDir, "..", "public") +} + +// getPublicNotesFilePath retourne le chemin du fichier .public.json +func (h *Handler) getPublicNotesFilePath() string { + return filepath.Join(h.notesDir, ".public.json") +} + +// loadPublicNotes charge les notes publiques depuis le fichier JSON +func (h *Handler) loadPublicNotes() (*PublicNotesData, error) { + path := h.getPublicNotesFilePath() + + // Si le fichier n'existe pas, retourner une liste vide + if _, err := os.Stat(path); os.IsNotExist(err) { + return &PublicNotesData{Notes: []PublicNote{}}, nil + } + + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + var publicNotes PublicNotesData + if err := json.Unmarshal(data, &publicNotes); err != nil { + return nil, err + } + + // Trier par date de publication (plus récent d'abord) + sort.Slice(publicNotes.Notes, func(i, j int) bool { + return publicNotes.Notes[i].PublishedAt.After(publicNotes.Notes[j].PublishedAt) + }) + + return &publicNotes, nil +} + +// savePublicNotes sauvegarde les notes publiques dans le fichier JSON +func (h *Handler) savePublicNotes(publicNotes *PublicNotesData) error { + path := h.getPublicNotesFilePath() + + data, err := json.MarshalIndent(publicNotes, "", " ") + if err != nil { + return err + } + + return os.WriteFile(path, data, 0644) +} + +// isPublic vérifie si une note est publique +func (h *Handler) isPublic(notePath string) bool { + publicNotes, err := h.loadPublicNotes() + if err != nil { + return false + } + + for _, note := range publicNotes.Notes { + if note.Path == notePath { + return true + } + } + return false +} + +// ensurePublicDir crée le dossier public s'il n'existe pas +func (h *Handler) ensurePublicDir() error { + publicDir := h.getPublicDirPath() + + // Créer public/ + if err := os.MkdirAll(publicDir, 0755); err != nil { + return err + } + + // Créer public/static/ + staticDir := filepath.Join(publicDir, "static") + if err := os.MkdirAll(staticDir, 0755); err != nil { + return err + } + + return nil +} + +// copyStaticAssets copie les fichiers CSS/fonts nécessaires +func (h *Handler) copyStaticAssets() error { + publicStaticDir := filepath.Join(h.getPublicDirPath(), "static") + + // Fichiers à copier + filesToCopy := []string{ + "static/theme.css", + "static/themes.css", + } + + for _, file := range filesToCopy { + src := file + dst := filepath.Join(publicStaticDir, filepath.Base(file)) + + if err := copyFile(src, dst); err != nil { + h.logger.Printf("Avertissement: impossible de copier %s: %v", file, err) + // Continuer même si la copie échoue + } + } + + return nil +} + +// copyFile copie un fichier +func copyFile(src, dst string) error { + sourceFile, err := os.Open(src) + if err != nil { + return err + } + defer sourceFile.Close() + + destFile, err := os.Create(dst) + if err != nil { + return err + } + defer destFile.Close() + + _, err = io.Copy(destFile, sourceFile) + return err +} + +// generateNoteHTML génère le fichier HTML pour une note publique +func (h *Handler) generateNoteHTML(notePath string) error { + // Lire le fichier Markdown + absPath := filepath.Join(h.notesDir, notePath) + content, err := os.ReadFile(absPath) + if err != nil { + return fmt.Errorf("lecture fichier: %w", err) + } + + // Extraire le front matter et le contenu + fm, body, err := indexer.ExtractFrontMatterAndBodyFromReader(bytes.NewReader(content)) + if err != nil { + // Continuer avec le contenu complet si erreur + body = string(content) + } + + // Déterminer le titre + title := filepath.Base(notePath) + if fm.Title != "" { + title = fm.Title + } else { + title = strings.TrimSuffix(title, ".md") + } + + // Convertir Markdown en HTML avec goldmark + md := goldmark.New( + goldmark.WithExtensions( + extension.GFM, + extension.Typographer, + ), + goldmark.WithParserOptions( + parser.WithAutoHeadingID(), + ), + goldmark.WithRendererOptions( + html.WithHardWraps(), + html.WithXHTML(), + ), + ) + + var buf bytes.Buffer + if err := md.Convert([]byte(body), &buf); err != nil { + return fmt.Errorf("conversion markdown: %w", err) + } + + // Générer le HTML complet standalone + htmlContent := h.generateStandaloneHTML(title, buf.String(), fm.Tags, fm.Date) + + // Écrire le fichier HTML - structure plate avec seulement le nom du fichier + filename := filepath.Base(notePath) + outputPath := filepath.Join(h.getPublicDirPath(), strings.TrimSuffix(filename, ".md")+".html") + + if err := os.WriteFile(outputPath, []byte(htmlContent), 0644); err != nil { + return fmt.Errorf("écriture fichier: %w", err) + } + + h.logger.Printf("HTML généré: %s", outputPath) + return nil +} + +// deleteNoteHTML supprime le fichier HTML d'une note +func (h *Handler) deleteNoteHTML(notePath string) error { + filename := filepath.Base(notePath) + outputPath := filepath.Join(h.getPublicDirPath(), strings.TrimSuffix(filename, ".md")+".html") + + if err := os.Remove(outputPath); err != nil && !os.IsNotExist(err) { + return err + } + + h.logger.Printf("HTML supprimé: %s", outputPath) + return nil +} + +// generatePublicIndex génère le fichier index.html avec la liste des notes +func (h *Handler) generatePublicIndex() error { + publicNotes, err := h.loadPublicNotes() + if err != nil { + return fmt.Errorf("chargement notes publiques: %w", err) + } + + // Enrichir avec les métadonnées + enrichedNotes := []map[string]interface{}{} + for _, note := range publicNotes.Notes { + filename := filepath.Base(note.Path) + item := map[string]interface{}{ + "Path": strings.TrimSuffix(filename, ".md") + ".html", + "Title": note.Title, + "PublishedAt": note.PublishedAt.Format("02/01/2006"), + "Tags": []string{}, + } + + // Essayer de lire le fichier pour obtenir les métadonnées + absPath := filepath.Join(h.notesDir, note.Path) + if content, err := os.ReadFile(absPath); err == nil { + if fm, _, err := indexer.ExtractFrontMatterAndBodyFromReader(bytes.NewReader(content)); err == nil { + if fm.Title != "" { + item["Title"] = fm.Title + } + item["Tags"] = fm.Tags + } + } + + enrichedNotes = append(enrichedNotes, item) + } + + // Générer le HTML de l'index + htmlContent := h.generateIndexHTML(enrichedNotes) + + // Écrire index.html + indexPath := filepath.Join(h.getPublicDirPath(), "index.html") + if err := os.WriteFile(indexPath, []byte(htmlContent), 0644); err != nil { + return fmt.Errorf("écriture index.html: %w", err) + } + + h.logger.Printf("Index généré: %s", indexPath) + return nil +} + +// handlePublicList retourne la liste des notes publiques +func (h *Handler) handlePublicList(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Méthode non autorisée", http.StatusMethodNotAllowed) + return + } + + publicNotes, err := h.loadPublicNotes() + if err != nil { + h.logger.Printf("Erreur chargement notes publiques: %v", err) + http.Error(w, "Erreur de chargement", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(publicNotes) +} + +// handlePublicToggle bascule le statut public d'une note + génère/supprime HTML +func (h *Handler) handlePublicToggle(w http.ResponseWriter, r *http.Request) { + h.logger.Printf("handlePublicToggle appelé") + + if r.Method != http.MethodPost { + h.logger.Printf("Méthode non autorisée: %s", r.Method) + http.Error(w, "Méthode non autorisée", http.StatusMethodNotAllowed) + return + } + + if err := r.ParseForm(); err != nil { + h.logger.Printf("Erreur ParseForm: %v", err) + http.Error(w, "Formulaire invalide", http.StatusBadRequest) + return + } + + path := r.FormValue("path") + h.logger.Printf("Chemin reçu: %s", path) + if path == "" { + h.logger.Printf("Chemin vide") + http.Error(w, "Chemin requis", http.StatusBadRequest) + return + } + + // Valider que le fichier existe + absPath := filepath.Join(h.notesDir, path) + if _, err := os.Stat(absPath); os.IsNotExist(err) { + http.Error(w, "Fichier introuvable", http.StatusNotFound) + return + } + + publicNotes, err := h.loadPublicNotes() + if err != nil { + h.logger.Printf("Erreur chargement notes publiques: %v", err) + http.Error(w, "Erreur de chargement", http.StatusInternalServerError) + return + } + + // Vérifier si la note est déjà publique + isCurrentlyPublic := false + newNotes := []PublicNote{} + for _, note := range publicNotes.Notes { + if note.Path == path { + isCurrentlyPublic = true + } else { + newNotes = append(newNotes, note) + } + } + + if isCurrentlyPublic { + // Retirer du public - supprimer le HTML + publicNotes.Notes = newNotes + + if err := h.deleteNoteHTML(path); err != nil { + h.logger.Printf("Erreur suppression HTML: %v", err) + } + } else { + // Ajouter au public - générer le HTML + title := filepath.Base(path) + title = strings.TrimSuffix(title, ".md") + + // Essayer de lire le titre depuis le fichier + if content, err := os.ReadFile(absPath); err == nil { + if fm, _, err := indexer.ExtractFrontMatterAndBodyFromReader(bytes.NewReader(content)); err == nil && fm.Title != "" { + title = fm.Title + } + } + + newNote := PublicNote{ + Path: path, + Title: title, + PublishedAt: time.Now(), + } + publicNotes.Notes = append(publicNotes.Notes, newNote) + + // Créer les dossiers nécessaires + if err := h.ensurePublicDir(); err != nil { + h.logger.Printf("Erreur création dossiers: %v", err) + http.Error(w, "Erreur de création des dossiers", http.StatusInternalServerError) + return + } + + // Copier les assets statiques + if err := h.copyStaticAssets(); err != nil { + h.logger.Printf("Avertissement copie assets: %v", err) + } + + // Générer le HTML de la note + if err := h.generateNoteHTML(path); err != nil { + h.logger.Printf("Erreur génération HTML: %v", err) + http.Error(w, "Erreur de génération HTML", http.StatusInternalServerError) + return + } + } + + // Sauvegarder le fichier .public.json + if err := h.savePublicNotes(publicNotes); err != nil { + h.logger.Printf("Erreur sauvegarde notes publiques: %v", err) + http.Error(w, "Erreur de sauvegarde", http.StatusInternalServerError) + return + } + + // Régénérer l'index + if err := h.generatePublicIndex(); err != nil { + h.logger.Printf("Erreur génération index: %v", err) + } + + // Retourner le nouveau statut + status := "private" + if !isCurrentlyPublic { + status = "public" + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{ + "status": status, + "path": path, + }) +} + +// generateStandaloneHTML génère un fichier HTML standalone complet +func (h *Handler) generateStandaloneHTML(title, content string, tags []string, date string) string { + tagsHTML := "" + if len(tags) > 0 { + tagsHTML = `
` + for _, tag := range tags { + tagsHTML += fmt.Sprintf(`#%s`, template.HTMLEscapeString(tag)) + } + tagsHTML += `
` + } + + dateHTML := "" + if date != "" { + dateHTML = fmt.Sprintf(` %s`, template.HTMLEscapeString(date)) + } + + return fmt.Sprintf(` + + + + + %s - PersoNotes + + + + + + + + + + + + +
+ +
+
+

%s

+
%s
+ %s +
+
+ %s +
+
+
+ + + +`, template.HTMLEscapeString(title), template.HTMLEscapeString(title), dateHTML, tagsHTML, content) +} + +// generateIndexHTML génère le HTML de la page d'index +func (h *Handler) generateIndexHTML(notes []map[string]interface{}) string { + notesHTML := "" + + if len(notes) > 0 { + notesHTML = `
    ` + for _, note := range notes { + path, _ := note["Path"].(string) + title, _ := note["Title"].(string) + publishedAt, _ := note["PublishedAt"].(string) + tags := []string{} + if tagsInterface, ok := note["Tags"].([]string); ok { + tags = tagsInterface + } + + tagsHTML := "" + if len(tags) > 0 { + tagsHTML = `
    ` + for _, tag := range tags { + tagsHTML += fmt.Sprintf(`#%s`, template.HTMLEscapeString(tag)) + } + tagsHTML += `
    ` + } + + notesHTML += fmt.Sprintf(` +
  • + +

    %s

    +
    + Published on %s +
    + %s +
    +
  • `, path, template.HTMLEscapeString(title), publishedAt, tagsHTML) + } + notesHTML += `
` + } else { + notesHTML = ` +
+ + + + +

No public notes yet

+
` + } + + return fmt.Sprintf(` + + + + + Public Notes - PersoNotes + + + + + + + + + + +
+
+

Public Notes

+

Discover my shared notes

+
+ %s +
+ + +`, notesHTML) +} diff --git a/locales/en.json b/locales/en.json index 3721666..9c892e8 100644 --- a/locales/en.json +++ b/locales/en.json @@ -260,5 +260,25 @@ "nextMonth": "Next month", "noNote": "No note", "noteOf": "Note of" + }, + "public": { + "buttonPrivate": "Private", + "buttonPublic": "Public", + "titlePrivate": "This note is private - Click to make it public", + "titlePublic": "This note is public - Click to make it private", + "listTitle": "Public Notes", + "listSubtitle": "Discover my shared notes", + "noPublicNotes": "No public notes yet", + "publishedOn": "Published on", + "backToList": "Back to public notes", + "loading": "Loading...", + "notificationPublished": "✅ Note published! It is now visible at /public", + "notificationUnpublished": "✅ Note removed from public space", + "notificationError": "❌ Error changing status" + }, + "publicNotes": { + "title": "Public Notes", + "editNote": "Edit note", + "viewPublic": "View public page" } } diff --git a/locales/fr.json b/locales/fr.json index 3dc7fcd..37bb488 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -260,5 +260,25 @@ "nextMonth": "Mois suivant", "noNote": "Pas de note", "noteOf": "Note du" + }, + "public": { + "buttonPrivate": "Privé", + "buttonPublic": "Public", + "titlePrivate": "Cette note est privée - Cliquer pour la rendre publique", + "titlePublic": "Cette note est publique - Cliquer pour la rendre privée", + "listTitle": "Notes Publiques", + "listSubtitle": "Découvrez mes notes partagées", + "noPublicNotes": "Aucune note publique pour le moment", + "publishedOn": "Publié le", + "backToList": "Retour aux notes publiques", + "loading": "Chargement...", + "notificationPublished": "✅ Note publiée ! Elle est maintenant visible sur /public", + "notificationUnpublished": "✅ Note retirée de l'espace public", + "notificationError": "❌ Erreur lors de la modification du statut" + }, + "publicNotes": { + "title": "Notes Publiques", + "editNote": "Éditer la note", + "viewPublic": "Voir la page publique" } } diff --git a/notes/.favorites.json b/notes/.favorites.json index 838b57e..f13dda9 100644 --- a/notes/.favorites.json +++ b/notes/.favorites.json @@ -7,40 +7,68 @@ "added_at": "2025-11-11T13:55:49.371541279+01:00", "order": 0 }, - { - "path": "research/design/ui-inspiration.md", - "is_dir": false, - "title": "ui-inspiration", - "added_at": "2025-11-11T14:20:49.985321698+01:00", - "order": 1 - }, { "path": "ideas/client-feedback.md", "is_dir": false, "title": "client-feedback", "added_at": "2025-11-11T14:22:16.497953232+01:00", + "order": 1 + }, + { + "path": "personal/learning-goals.md", + "is_dir": false, + "title": "learning-goals", + "added_at": "2025-12-24T13:17:21.123080299+01:00", "order": 2 }, { - "path": "ideas/collaboration.md", - "is_dir": false, - "title": "collaboration", - "added_at": "2025-11-11T14:22:18.012032002+01:00", + "path": "archive", + "is_dir": true, + "title": "archive", + "added_at": "2025-12-24T15:48:42.323990909+01:00", "order": 3 }, { - "path": "ideas/mobile-app.md", + "path": "archive/ai-assistant.md", "is_dir": false, - "title": "mobile-app", - "added_at": "2025-11-11T14:22:19.048311608+01:00", + "title": "ai-assistant", + "added_at": "2025-12-24T15:49:08.265811752+01:00", "order": 4 }, { - "path": "documentation/guides", - "is_dir": true, - "title": "guides", - "added_at": "2025-11-12T18:18:20.53353467+01:00", + "path": "meetings/2025/sprint-planning.md", + "is_dir": false, + "title": "sprint-planning", + "added_at": "2025-12-24T15:55:04.58786532+01:00", "order": 5 + }, + { + "path": "meetings", + "is_dir": true, + "title": "meetings", + "added_at": "2025-12-24T15:56:40.332077313+01:00", + "order": 6 + }, + { + "path": "Myfolder.txt", + "is_dir": true, + "title": "Myfolder.txt", + "added_at": "2025-12-24T15:57:09.512148418+01:00", + "order": 7 + }, + { + "path": "projets", + "is_dir": true, + "title": "projets", + "added_at": "2025-12-24T15:59:24.938636283+01:00", + "order": 8 + }, + { + "path": "documentation/bienvenue.md", + "is_dir": false, + "title": "bienvenue", + "added_at": "2025-12-24T16:30:46.322365652+01:00", + "order": 9 } ] } \ No newline at end of file diff --git a/notes/.public.json b/notes/.public.json new file mode 100644 index 0000000..2f3c546 --- /dev/null +++ b/notes/.public.json @@ -0,0 +1,39 @@ +{ + "notes": [ + { + "path": "documentation/bienvenue.md", + "title": "Bienvenue dans PersoNotes", + "published_at": "2025-12-24T16:28:26.173656053+01:00" + }, + { + "path": "documentation/authentication.md", + "title": "Authentication Guide", + "published_at": "2025-12-24T16:26:46.9731494+01:00" + }, + { + "path": "ideas/collaboration.md", + "title": "Real-time Collaboration", + "published_at": "2025-12-24T12:40:15.778895438+01:00" + }, + { + "path": "daily/2025/11/11.md", + "title": "Daily Note - 2025-11-11", + "published_at": "2025-12-24T12:38:12.77519956+01:00" + }, + { + "path": "ideas/mobile-app.md", + "title": "Native Mobile App", + "published_at": "2025-12-24T12:37:20.649319402+01:00" + }, + { + "path": "tasks/backlog.md", + "title": "Product Backlog", + "published_at": "2025-11-13T20:13:05.36334514+01:00" + }, + { + "path": "documentation/api/endpoints.md", + "title": "API Endpoints Reference", + "published_at": "2025-11-13T19:36:57.522466752+01:00" + } + ] +} \ No newline at end of file diff --git a/notes/projets/backend/api-design.md b/notes/Myfolder.txt/api-design.md similarity index 100% rename from notes/projets/backend/api-design.md rename to notes/Myfolder.txt/api-design.md diff --git a/notes/daily/2025/12/24.md b/notes/daily/2025/12/24.md new file mode 100644 index 0000000..3fe4554 --- /dev/null +++ b/notes/daily/2025/12/24.md @@ -0,0 +1,25 @@ +--- +title: Daily Note - 2025-12-24 +date: 24-12-2025 +last_modified: 24-12-2025:12:36 +tags: + - daily +--- + +# 📅 Mercredi 24 Décembre 2025 + +## 🎯 Objectifs du jour +- + + +## 📝 Notes +- + +## ✅ Accompli +- + +## 💭 Réflexions +- + +## 🔗 Liens +- diff --git a/notes/documentation/bienvenue.md b/notes/documentation/bienvenue.md index 470395a..1bc16ff 100644 --- a/notes/documentation/bienvenue.md +++ b/notes/documentation/bienvenue.md @@ -1,7 +1,7 @@ --- title: Bienvenue dans PersoNotes date: 08-11-2025 -last_modified: 09-11-2025:01:13 +last_modified: 24-12-2025:16:28 tags: - aide - documentation diff --git a/notes/documentation/client-feedback.md b/notes/documentation/client-feedback.md index 54a0f55..098773c 100644 --- a/notes/documentation/client-feedback.md +++ b/notes/documentation/client-feedback.md @@ -1,7 +1,7 @@ --- title: Client Feedback Session date: 10-11-2025 -last_modified: 11-11-2025:11:12 +last_modified: 24-12-2025:16:45 tags: - meeting - client @@ -28,3 +28,7 @@ Focus sur l'export PDF pour la v1.1 # DERNIER EDIT + +[Progressive Web App](projets/mobile/pwa.md) + +`This is a ` \ No newline at end of file diff --git a/notes/ideas/collaboration.md b/notes/documentation/collaboration.md similarity index 100% rename from notes/ideas/collaboration.md rename to notes/documentation/collaboration.md diff --git a/notes/documentation/api/endpoints.md b/notes/ideas/endpoints.md similarity index 100% rename from notes/documentation/api/endpoints.md rename to notes/ideas/endpoints.md diff --git a/notes/ideas/mobile-app.md b/notes/meetings/2025/mobile-app.md similarity index 100% rename from notes/ideas/mobile-app.md rename to notes/meetings/2025/mobile-app.md diff --git a/notes/projets/frontend/codemirror-integration.md b/notes/projets/backend/codemirror-integration.md similarity index 94% rename from notes/projets/frontend/codemirror-integration.md rename to notes/projets/backend/codemirror-integration.md index 69949f2..a1aa735 100644 --- a/notes/projets/frontend/codemirror-integration.md +++ b/notes/projets/backend/codemirror-integration.md @@ -1,7 +1,7 @@ --- title: CodeMirror Integration date: 10-11-2025 -last_modified: 12-11-2025:09:37 +last_modified: 24-12-2025:16:46 tags: - projet - frontend diff --git a/notes/projets/frontend/vite-build.md b/notes/projets/frontend/vite-build.md index 6bd698f..94764c3 100644 --- a/notes/projets/frontend/vite-build.md +++ b/notes/projets/frontend/vite-build.md @@ -1,8 +1,11 @@ --- -title: "Vite Build Process" -date: "10-11-2025" -last_modified: "10-11-2025:19:21" -tags: ["projet", "frontend", "build"] +title: Vite Build Process +date: 10-11-2025 +last_modified: 24-12-2025:16:41 +tags: + - projet + - frontend + - build --- # Vite Build Process diff --git a/notes/tasks/backlog.md b/notes/projets/mobile/backlog.md similarity index 100% rename from notes/tasks/backlog.md rename to notes/projets/mobile/backlog.md diff --git a/public/authentication.html b/public/authentication.html new file mode 100644 index 0000000..b0351fa --- /dev/null +++ b/public/authentication.html @@ -0,0 +1,108 @@ + + + + + + Authentication Guide - PersoNotes + + + + + + + + + + + + +
+ +
+
+

Authentication Guide

+
10-11-2025
+
#documentation#api#security
+
+
+

Authentication

+

Current Status

+

⚠️ No authentication currently implemented.

+

Future Implementation

+

JWT Tokens

+
POST /api/auth/login
+{
+  "username": "user",
+  "password": "pass"
+}
+
+Response:
+{
+  "token": "eyJhbGc..."
+}
+
+

Bearer Token

+
Authorization: Bearer eyJhbGc...
+
+

Security

+
    +
  • HTTPS only in production
  • +
  • Reverse proxy with nginx
  • +
  • Rate limiting
  • +
+

Test Delete 1

+ +
+
+
+ + + + \ No newline at end of file diff --git a/public/backlog.html b/public/backlog.html new file mode 100644 index 0000000..9b4a5b7 --- /dev/null +++ b/public/backlog.html @@ -0,0 +1,95 @@ + + + + + + Product Backlog - PersoNotes + + + + + + + + + + +
+ +
+
+

Product Backlog

+
📅 10-11-2025
+
#task#planning
+
+
+

Product Backlog

+

High Priority

+
    +
  • Export notes to PDF
  • +
  • Bulk operations (delete, move)
  • +
  • Tags management page
  • +
  • Keyboard shortcuts documentation
  • +
+

Medium Priority

+
    +
  • Note templates
  • +
  • Trash/Recycle bin
  • +
  • Note history/versions
  • +
  • Full-text search improvements
  • +
+

Low Priority

+
    +
  • Themes customization
  • +
  • Plugin system
  • +
  • Graph view of notes links
  • +
+ +
+
+
+ + + \ No newline at end of file diff --git a/public/bienvenue.html b/public/bienvenue.html new file mode 100644 index 0000000..8de7139 --- /dev/null +++ b/public/bienvenue.html @@ -0,0 +1,312 @@ + + + + + + Bienvenue dans PersoNotes - PersoNotes + + + + + + + + + + + + +
+ +
+
+

Bienvenue dans PersoNotes

+
08-11-2025
+
#aide#documentation#tutorial
+
+
+

08/11/2025 -

+

C’est mon application de prise de note

+

J’espére qu’elle va bien marcher

+

Bienvenue dans PersoNotes

+

Bienvenue dans votre application de prise de notes en Markdown ! Cette page vous explique comment utiliser l’application et le format front matter.

+

Qu’est-ce que le Front Matter ?

+

Le front matter est un bloc de métadonnées en YAML placé au début de chaque note, entre deux lignes ---. Il permet d’ajouter des informations structurées à vos notes.

+

Format du Front Matter

+
---
+title: Titre de votre note
+date: 08-11-2025
+last_modified: 08-11-2025:14:10
+tags: [projet, urgent, backend]
+---
+
+

Champs disponibles

+
    +
  • title : Le titre de votre note (généré automatiquement depuis le nom du fichier)
  • +
  • date : Date de création (format: JJ-MM-AAAA)
  • +
  • last_modified : Dernière modification (format: JJ-MM-AAAA:HH:MM) - mis à jour automatiquement
  • +
  • tags : Liste de tags pour organiser et rechercher vos notes
  • +
+

Exemples de tags

+

Vous pouvez écrire vos tags de deux façons :

+
# Format inline
+tags: [projet, urgent, backend, api]
+
+# Format liste
+tags:
+  - projet
+  - urgent
+  - backend
+  - api
+
+

Les tags sont indexés et permettent de rechercher vos notes via la barre de recherche.

+

Guide Markdown

+

Titres

+
# Titre niveau 1
+## Titre niveau 2
+### Titre niveau 3
+
+

Emphase

+
*italique* ou _italique_
+**gras** ou __gras__
+***gras et italique***
+~~barré~~
+
+

Rendu : italique, gras, gras et italique

+

Listes

+

Liste non ordonnée

+
- Élément 1
+- Élément 2
+  - Sous-élément 2.1
+  - Sous-élément 2.2
+- Élément 3
+
+

Rendu :

+
    +
  • Élément 1
  • +
  • Élément 2 +
      +
    • Sous-élément 2.1
    • +
    • Sous-élément 2.2
    • +
    +
  • +
  • Élément 3
  • +
+

Liste ordonnée

+
1. Premier élément
+2. Deuxième élément
+3. Troisième élément
+
+

Rendu :

+
    +
  1. Premier élément
  2. +
  3. Deuxième élément
  4. +
  5. Troisième élément
  6. +
+

Liens et Images

+
[Texte du lien](https://example.com)
+![Texte alternatif](url-de-image.jpg)
+
+

Exemple : Documentation Markdown

+

Code

+

Code inline

+

Utilisez des backticks : code inline

+

Bloc de code

+
​```javascript
+function hello() {
+  console.log("Hello World!");
+}
+​```
+
+

Rendu :

+
function hello() {
+  console.log("Hello World!");
+}
+
+

Citations

+
> Ceci est une citation
+> sur plusieurs lignes
+
+

Rendu :

+
+

Ceci est une citation
+sur plusieurs lignes

+
+

Tableaux

+
| Colonne 1 | Colonne 2 | Colonne 3 |
+|-----------|-----------|-----------|
+| Ligne 1   | Données   | Données   |
+| Ligne 2   | Données   | Données   |
+
+

Rendu :

+ + + + + + + + + + + + + + + + + + + + +
Colonne 1Colonne 2Colonne 3
Ligne 1DonnéesDonnées
Ligne 2DonnéesDonnées
+

Séparateurs

+
---
+
+

Rendu :

+
+

Commandes Slash

+

Utilisez le caractère / au début d’une ligne pour accéder aux commandes rapides :

+
    +
  • /h1, /h2, /h3 - Titres
  • +
  • /list - Liste à puces
  • +
  • /date - Insérer la date du jour
  • +
  • /link - Créer un lien
  • +
  • /bold - Texte en gras
  • +
  • /italic - Texte en italique
  • +
  • /code - Code inline
  • +
  • /codeblock - Bloc de code
  • +
  • /quote - Citation
  • +
  • /hr - Ligne de séparation
  • +
  • /table - Créer un tableau
  • +
+

Navigation : Utilisez les flèches ↑↓ pour naviguer, Entrée ou Tab pour insérer, Échap pour annuler.

+

Raccourcis et Astuces

+

Créer une note

+

Cliquez sur le bouton ✨ Nouvelle note dans l’en-tête. Si la note existe déjà, elle sera ouverte, sinon elle sera créée.

+

Rechercher des notes

+

Utilisez la barre de recherche en haut pour filtrer vos notes par tags. La recherche est mise à jour en temps réel.

+

Sauvegarder

+

Cliquez sur le bouton 💾 Enregistrer pour sauvegarder vos modifications. Le champ last_modified du front matter sera automatiquement mis à jour.

+

Supprimer une note

+

Cliquez sur l’icône 🗑️ à côté du nom de la note dans la sidebar.

+

Organisation avec les tags

+

Les tags sont un excellent moyen d’organiser vos notes. Voici quelques suggestions :

+
    +
  • Par projet : projet-notes, projet-api, projet-frontend
  • +
  • Par priorité : urgent, important, backlog
  • +
  • Par type : documentation, tutorial, meeting, todo
  • +
  • Par technologie : javascript, go, python, docker
  • +
  • Par statut : en-cours, terminé, archive
  • +
+

Exemple complet

+

Voici un exemple de note complète :

+
---
+title: Réunion API Backend
+date: 08-11-2025
+last_modified: 08-11-2025:15:30
+tags: [meeting, backend, api, urgent]
+---
+
+# Réunion API Backend
+
+## Participants
+
+- Alice (Lead Dev)
+- Bob (Backend)
+- Charlie (Frontend)
+
+## Points discutés
+
+### 1. Architecture de l'API
+
+Nous avons décidé d'utiliser une architecture REST avec les endpoints suivants :
+
+- `GET /api/notes` - Liste toutes les notes
+- `POST /api/notes` - Créer une note
+- `PUT /api/notes/:id` - Modifier une note
+- `DELETE /api/notes/:id` - Supprimer une note
+
+### 2. Authentification
+
+> Utilisation de JWT pour l'authentification
+
+Code d'exemple :
+
+​```go
+func generateToken(userID string) (string, error) {
+    // Implementation
+}
+​```
+
+### 3. Prochaines étapes
+
+- [ ] Implémenter les endpoints
+- [ ] Écrire les tests
+- [ ] Documentation API
+
+## Actions
+
+| Qui     | Action               | Deadline   |
+|---------|---------------------|------------|
+| Bob     | Endpoints API       | 15-11-2025 |
+| Charlie | Interface Frontend  | 20-11-2025 |
+| Alice   | Review & Deploy     | 25-11-2025 |
+
+
+

Bonne prise de notes ! 📝

+ +
+
+
+ + + + \ No newline at end of file diff --git a/public/collaboration.html b/public/collaboration.html new file mode 100644 index 0000000..8c2dac9 --- /dev/null +++ b/public/collaboration.html @@ -0,0 +1,89 @@ + + + + + + Real-time Collaboration - PersoNotes + + + + + + + + + + +
+ +
+
+

Real-time Collaboration

+
📅 10-11-2025
+
#idea#collaboration
+
+
+

Real-time Collaboration

+

Goal

+

Plusieurs utilisateurs éditent la même note simultanément.

+

Technology

+
    +
  • WebSockets
  • +
  • Operational Transforms ou CRDT
  • +
  • Presence indicators
  • +
+

Challenges

+
    +
  • Conflict resolution
  • +
  • Performance at scale
  • +
  • User permissions
  • +
+ +
+
+
+ + + \ No newline at end of file diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..81b4a77 --- /dev/null +++ b/public/index.html @@ -0,0 +1,110 @@ + + + + + + Public Notes - PersoNotes + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/mobile-app.html b/public/mobile-app.html new file mode 100644 index 0000000..213eda1 --- /dev/null +++ b/public/mobile-app.html @@ -0,0 +1,93 @@ + + + + + + Native Mobile App - PersoNotes + + + + + + + + + + +
+ +
+
+

Native Mobile App

+
📅 10-11-2025
+
#idea#mobile
+
+
+

Native Mobile App Idea

+

Concept

+

Créer une app native iOS/Android pour l’édition de notes.

+

Tech Stack

+
    +
  • React Native ou Flutter
  • +
  • Sync avec l’API REST
  • +
  • Offline-first architecture
  • +
+

Features

+
    +
  • Push notifications
  • +
  • Widget home screen
  • +
  • Voice notes
  • +
  • Photo attachments
  • +
+

Timeline

+

Q2 2025 - Prototype
+Q3 2025 - Beta testing

+ +
+
+
+ + + \ No newline at end of file diff --git a/public/notes/personal/learning-goals.html b/public/notes/personal/learning-goals.html new file mode 100644 index 0000000..c1a63db --- /dev/null +++ b/public/notes/personal/learning-goals.html @@ -0,0 +1,94 @@ + + + + + + 2025 Learning Goals - PersoNotes + + + + + + + + + + +
+ +
+
+

2025 Learning Goals

+
📅 10-11-2025
+
#personal#learning
+
+
+

Learning Goals 2025

+

Technical

+
    +
  • Master Go concurrency patterns
  • +
  • Learn Rust basics
  • +
  • Deep dive into databases
  • +
  • System design courses
  • +
+

Soft Skills

+
    +
  • Technical writing
  • +
  • Public speaking
  • +
  • Mentoring
  • +
+

Books to Read

+
    +
  1. Designing Data-Intensive Applications
  2. +
  3. The Pragmatic Programmer
  4. +
  5. Clean Architecture
  6. +
+ +
+
+
+ + + \ No newline at end of file diff --git a/public/static/theme.css b/public/static/theme.css new file mode 100644 index 0000000..aa0e521 --- /dev/null +++ b/public/static/theme.css @@ -0,0 +1,3663 @@ +/* + * PersoNotes - Material Darker Theme + * Inspired by Material Design with dark palette + */ + +:root { + /* Colors - Material Dark Theme (Professional) */ + --bg-primary: #1e1e1e; + --bg-secondary: #252525; + --bg-tertiary: #2d2d2d; + --bg-elevated: #323232; + + --border-primary: #3e3e3e; + --border-secondary: #2a2a2a; + + --text-primary: #e0e0e0; + --text-secondary: #b0b0b0; + --text-muted: #6e6e6e; + + /* Accent color - Single blue accent for consistency */ + --accent-primary: #42a5f5; + --accent-hover: #64b5f6; + + /* Semantic colors */ + --success: #66bb6a; + --warning: #ffa726; + --error: #ef5350; + + /* Spacing - 8px base unit system */ + --spacing-xs: 0.5rem; /* 8px - 1 unit */ + --spacing-sm: 1rem; /* 16px - 2 units */ + --spacing-md: 1.5rem; /* 24px - 3 units */ + --spacing-lg: 2rem; /* 32px - 4 units */ + --spacing-xl: 3rem; /* 48px - 6 units */ + + /* Sidebar spacing - aligned to 8px grid */ + --sidebar-item-gap: 0.125rem; /* 2px - minimal spacing between items */ + --sidebar-padding-v: 0.25rem; /* 4px - compact vertical padding */ + --sidebar-padding-h: 1rem; /* 16px */ + --sidebar-indent: 1.5rem; /* 24px */ + + /* Shadows - reduced opacity for minimal look */ + --shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.1); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.08); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.12), 0 4px 6px -4px rgba(0, 0, 0, 0.1); + --shadow-glow: 0 0 20px rgba(66, 165, 245, 0.1); + + /* Border radius */ + --radius-sm: 4px; + --radius-md: 6px; + --radius-lg: 8px; + + /* Transitions */ + --transition-fast: 150ms ease; + --transition-normal: 250ms ease; + + /* Typography scale - consistent font sizes */ + --text-xs: 0.75rem; /* 12px */ + --text-sm: 0.875rem; /* 14px */ + --text-base: 1rem; /* 16px - default */ + --text-md: 1.125rem; /* 18px */ + --text-lg: 1.25rem; /* 20px */ + --text-xl: 1.5rem; /* 24px */ + --text-2xl: 2rem; /* 32px */ + --text-3xl: 3rem; /* 48px */ + + /* Touch targets - minimum sizes for accessibility */ + --touch-sm: 2rem; /* 32px - compact */ + --touch-md: 2.75rem; /* 44px - standard mobile minimum */ + --touch-lg: 3rem; /* 48px - comfortable */ + + /* Lucide Icons - Professional SVG icons */ + --icon-xs: 14px; + --icon-sm: 16px; + --icon-md: 20px; + --icon-lg: 24px; + --icon-xl: 32px; +} + +/* Lucide Icons Styling */ +.lucide { + display: inline-block; + vertical-align: middle; + stroke-width: 2; + stroke: currentColor; + fill: none; +} + +/* Icon size variants */ +.icon-xs { width: var(--icon-xs); height: var(--icon-xs); } +.icon-sm { width: var(--icon-sm); height: var(--icon-sm); } +.icon-md { width: var(--icon-md); height: var(--icon-md); } +.icon-lg { width: var(--icon-lg); height: var(--icon-lg); } +.icon-xl { width: var(--icon-xl); height: var(--icon-xl); } + +/* Icon with text alignment */ +.icon-text { + display: inline-flex; + align-items: center; + gap: 0.5rem; +} + +/* Icon color variants */ +.icon-primary { color: var(--text-primary); } +.icon-secondary { color: var(--text-secondary); } +.icon-muted { color: var(--text-muted); } +.icon-accent { color: var(--accent-primary); } + +/* Base styles */ +html { + font-size: 16px; + background: var(--bg-primary); +} + +body { + background: var(--bg-primary); + color: var(--text-primary); + margin: 0; + font-family: 'JetBrainsMono Nerd Font', 'JetBrains Mono', 'Fira Code', 'Monaco', 'Consolas', monospace; + line-height: 1.6; +} + +/* Header */ +header { + background: var(--bg-secondary); + border-bottom: 1px solid var(--border-primary); + padding: var(--spacing-md) var(--spacing-lg); + display: flex; + align-items: center; + gap: var(--spacing-md); + box-shadow: var(--shadow-sm); + position: sticky; + top: 0; + z-index: 100; +} + +header h1 { + margin: 0; + font-size: 1.25rem; + font-weight: 600; + color: var(--text-primary); + background: var(--accent-primary); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +/* Search input */ +input[type="search"] { + background: var(--bg-tertiary); + border: 1px solid var(--border-primary); + color: var(--text-primary); + border-radius: var(--radius-md); + padding: var(--spacing-sm) var(--spacing-md); + font-size: 0.9rem; + transition: all var(--transition-fast); +} + +input[type="search"]:focus { + outline: none; + border-color: var(--accent-primary); + box-shadow: 0 0 0 3px rgba(88, 166, 255, 0.1); + background: var(--bg-elevated); +} + +input[type="search"]::placeholder { + color: var(--text-muted); +} + +/* Main layout */ +.main-layout { + position: relative; + height: calc(100vh - 65px); + overflow-x: hidden; +} + +/* Sidebar */ +aside { + position: absolute; + top: 0; + left: 0; + bottom: 0; + width: 300px; + min-width: 200px; + max-width: 600px; + transform: translateX(0); + transition: transform 0.25s ease; + z-index: 10; + background: var(--bg-secondary); + border-right: 1px solid var(--border-primary); + padding: var(--spacing-md); + overflow-y: auto; + display: flex; + flex-direction: column; + gap: var(--spacing-md); +} + +/* Resize handle for sidebar */ +.sidebar-resize-handle { + position: absolute; + top: 0; + right: -6px; /* Overlap for easier grab */ + bottom: 0; + width: 16px; /* Even wider for Firefox */ + cursor: col-resize; + background: rgba(66, 165, 245, 0.05); + border-left: 2px solid transparent; + border-right: 2px solid transparent; + transition: all 0.2s ease; + z-index: 11; + display: flex; + align-items: center; + justify-content: center; +} + +.sidebar-resize-handle:hover { + background: rgba(66, 165, 245, 0.15); + border-left-color: var(--accent-primary); + border-right-color: var(--accent-primary); +} + +.sidebar-resize-handle.resizing { + background: rgba(66, 165, 245, 0.25); + border-left: 3px solid var(--accent-primary); + border-right: 3px solid var(--accent-primary); +} + +/* Visual indicator - always visible */ +.sidebar-resize-handle::before { + content: '⋮'; + font-size: 18px; + color: var(--accent-primary); + opacity: 0.5; + transition: all 0.2s ease; + pointer-events: none; +} + +.sidebar-resize-handle:hover::before { + opacity: 1; + text-shadow: 0 0 4px var(--accent-primary); +} + +aside::-webkit-scrollbar { + width: 8px; +} + +aside::-webkit-scrollbar-track { + background: var(--bg-secondary); +} + +aside::-webkit-scrollbar-thumb { + background: var(--border-primary); + border-radius: 4px; +} + +aside::-webkit-scrollbar-thumb:hover { + background: var(--text-muted); +} + +/* Titres de section de la sidebar */ +aside h2, +.sidebar-section-title { + font-size: 0.95rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-secondary); + margin: 0 0 var(--spacing-sm) 0; +} + +/* Sections rétractables de la sidebar */ +.sidebar-section-header { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 0; + user-select: none; + transition: opacity var(--transition-fast); +} + +.sidebar-section-header:hover { + opacity: 0.8; +} + +.section-toggle { + font-size: 0.75rem; + color: var(--text-secondary); + display: inline-block; + transition: transform var(--transition-fast); + width: 1rem; + text-align: center; +} + +.section-toggle.expanded { + transform: rotate(90deg); +} + +.sidebar-section-content { + overflow: hidden; + transition: opacity var(--transition-fast); +} + +aside hr { + border: none; + border-top: 1px solid var(--border-primary); + margin: var(--spacing-sm) 0; +} + +/* File tree and search results - styles now handled by .file-item class */ + +/* Search results header */ +.search-results-header { + padding: var(--spacing-sm) 0; + margin-bottom: var(--spacing-sm); + border-bottom: 1px solid var(--border-secondary); +} + +.search-results-count { + font-size: 0.85rem; + color: var(--text-secondary); + font-weight: 500; +} + +/* Search results list */ +.search-results-list { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: var(--spacing-sm); +} + +.search-result-item { + margin: 0; +} + +.search-result-link { + display: flex; + align-items: flex-start; + gap: var(--spacing-md); + background: var(--bg-tertiary); + border: 1px solid transparent; + border-radius: var(--radius-md); + padding: var(--spacing-md); + text-decoration: none; + color: var(--text-primary); + transition: transform var(--transition-fast), border-color var(--transition-fast), box-shadow var(--transition-fast); +} + +.search-result-link:hover { + transform: translateY(-1px); + border-color: var(--accent-primary); + box-shadow: var(--shadow-sm); +} + +.search-result-icon { + font-size: 1.5rem; + flex-shrink: 0; + opacity: 0.7; + line-height: 1; +} + +.search-result-content { + flex: 1; + min-width: 0; +} + +.search-result-header { + margin-bottom: var(--spacing-xs); +} + +.search-result-title { + font-weight: 600; + font-size: 0.95rem; + color: var(--text-primary); +} + +.search-result-path { + font-size: 0.7rem; + color: var(--text-muted); + font-family: 'Fira Code', 'Cascadia Code', 'Consolas', monospace; + margin-bottom: var(--spacing-xs); +} + +.search-result-snippet { + margin: var(--spacing-xs) 0; + font-size: 0.85rem; + color: var(--text-secondary); + line-height: 1.5; +} + +.search-result-footer { + display: flex; + justify-content: space-between; + align-items: center; + gap: var(--spacing-sm); + margin-top: var(--spacing-sm); + flex-wrap: wrap; +} + +.search-result-tags { + display: flex; + flex-wrap: wrap; + gap: var(--spacing-xs); +} + +.search-result-date { + font-size: 0.7rem; + color: var(--text-muted); + white-space: nowrap; +} + +.tag-pill { + background: rgba(130, 170, 255, 0.15); + color: var(--accent-primary); + border: 1px solid rgba(130, 170, 255, 0.3); + border-radius: 999px; + padding: 0 var(--spacing-sm); + font-size: 0.7rem; + line-height: 1.6; + font-family: 'Fira Code', 'Cascadia Code', 'Consolas', monospace; + font-weight: 500; +} + +/* No results message */ +.search-no-results { + text-align: center; + padding: var(--spacing-xl) var(--spacing-md); + color: var(--text-secondary); +} + +.search-no-results-icon { + font-size: 3rem; + margin-bottom: var(--spacing-md); + opacity: 0.5; +} + +.search-no-results-text { + font-size: 0.95rem; + margin-bottom: var(--spacing-sm); +} + +.search-no-results-text strong { + color: var(--accent-primary); +} + +.search-no-results-hint { + font-size: 0.85rem; + color: var(--text-muted); +} + +/* Search help */ +.search-help { + padding: var(--spacing-md); +} + +.search-help-title { + font-size: 0.95rem; + font-weight: 600; + color: var(--accent-primary); + margin-bottom: var(--spacing-xs); +} + +.search-help-text { + font-size: 0.85rem; + color: var(--text-secondary); + margin-bottom: var(--spacing-md); +} + +.search-help-examples { + display: flex; + flex-direction: column; + gap: var(--spacing-sm); +} + +.search-help-example { + display: flex; + align-items: center; + gap: var(--spacing-sm); + padding: var(--spacing-sm); + background: var(--bg-tertiary); + border-radius: var(--radius-sm); + border: 1px solid var(--border-secondary); +} + +.search-help-example code { + background: var(--bg-primary); + color: var(--accent-primary); + padding: 0.2rem 0.4rem; + border-radius: var(--radius-sm); + font-size: 0.8rem; + font-family: 'Fira Code', 'Cascadia Code', 'Consolas', monospace; + font-weight: 500; + min-width: 110px; +} + +.search-help-example span { + font-size: 0.8rem; + color: var(--text-secondary); +} + +/* Main content */ +main { + margin-left: 300px; + transition: margin-left 0.25s ease; + background: var(--bg-primary); + padding: var(--spacing-xl); + overflow-y: auto; + height: 100%; +} + +main::-webkit-scrollbar { + width: 10px; +} + +main::-webkit-scrollbar-track { + background: var(--bg-primary); +} + +main::-webkit-scrollbar-thumb { + background: var(--border-primary); + border-radius: 5px; +} + +main::-webkit-scrollbar-thumb:hover { + background: var(--text-muted); +} + +/* Editor */ +.editor-form { + display: flex; + flex-direction: column; + gap: var(--spacing-lg); +} + +.editor-header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.editor-header label { + font-size: 0.95rem; + color: var(--text-secondary); +} + +.toggle-preview-btn { + background: var(--bg-tertiary); + border: 1px solid var(--border-primary); + color: var(--text-primary); + padding: var(--spacing-sm) var(--spacing-md); + border-radius: var(--radius-sm); + font-size: 0.85rem; + cursor: pointer; + transition: all var(--transition-fast); + white-space: nowrap; +} + +.toggle-preview-btn:hover { + background: var(--bg-elevated); + border-color: var(--accent-primary); + color: var(--accent-primary); + transform: translateY(-1px); + box-shadow: var(--shadow-sm); +} + +.editor-grid { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + gap: var(--spacing-lg); + min-height: 70vh; + transition: grid-template-columns var(--transition-normal); +} + +/* Mode: Éditeur seul */ +.editor-grid.preview-hidden { + grid-template-columns: 1fr; +} + +.editor-grid.preview-hidden #preview { + display: none !important; +} + +/* Mode: Preview seule */ +.editor-grid.editor-hidden { + grid-template-columns: 1fr; +} + +.editor-grid.editor-hidden .editor-panel { + display: none !important; +} + +/* Mode: Split (défaut) */ +.editor-grid.split-view { + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); +} + +.editor-grid.split-view .editor-panel, +.editor-grid.split-view #preview { + display: block !important; +} + +.editor-panel { + position: relative; +} + +#editor { + background: var(--bg-secondary); + border: 1px solid var(--border-primary); + border-radius: var(--radius-md); + color: var(--text-primary); + padding: var(--spacing-md); + font-family: 'Fira Code', 'Cascadia Code', 'Consolas', 'Monaco', monospace; + font-size: 0.95rem; + line-height: 1.6; + min-height: 75vh; + width: 100%; + resize: vertical; + transition: border-color var(--transition-fast); +} + +#editor:focus { + outline: none; + border-color: var(--accent-primary); + box-shadow: 0 0 0 3px rgba(88, 166, 255, 0.1); +} + +/* CodeMirror integration */ +.CodeMirror { + background: var(--bg-secondary); + border: 1px solid var(--border-primary); + border-radius: var(--radius-md); + color: var(--text-primary); + font-family: 'Fira Code', 'Cascadia Code', 'Consolas', 'Monaco', monospace; + font-size: 0.95rem; + line-height: 1.6; + height: calc(100vh - 180px); + transition: border-color var(--transition-fast); +} + +/* Cacher le textarea original quand CodeMirror est actif */ +.editor-panel textarea#editor { + display: none !important; + position: absolute !important; + visibility: hidden !important; + opacity: 0 !important; + pointer-events: none !important; + height: 0 !important; + width: 0 !important; +} + +.editor-panel .CodeMirror ~ textarea { + display: none !important; + position: absolute !important; + visibility: hidden !important; + opacity: 0 !important; + pointer-events: none !important; + height: 0 !important; + width: 0 !important; +} + +.CodeMirror-focused { + outline: none; + border-color: var(--accent-primary); + box-shadow: 0 0 0 3px rgba(88, 166, 255, 0.1); +} + +.CodeMirror-gutters { + background: var(--bg-tertiary); + border-right: 1px solid var(--border-primary); +} + +.CodeMirror-linenumber { + color: var(--text-muted); +} + +.CodeMirror-cursor { + border-left: 2px solid var(--accent-primary); +} + +.CodeMirror-selected { + background: rgba(88, 166, 255, 0.2); +} + +.CodeMirror-line::selection, +.CodeMirror-line > span::selection, +.CodeMirror-line > span > span::selection { + background: rgba(88, 166, 255, 0.2); +} + +.editor-actions { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--spacing-md); +} + +.editor-actions-primary { + display: flex; + flex-wrap: wrap; + gap: var(--spacing-sm); +} + +.save-status { + color: var(--text-secondary); + min-height: 1.25rem; + display: inline-flex; + align-items: center; +} + +/* Preview */ +.preview { + background: var(--bg-secondary); + border: 1px solid var(--border-primary); + border-radius: var(--radius-md); + padding: 2rem 3rem; + overflow-y: auto; + max-height: calc(100vh - 180px); + height: calc(100vh - 180px); +} + +.markdown-preview { + min-height: 75vh; + box-shadow: inset 0 0 0 1px rgba(88, 166, 255, 0.05); +} + +.markdown-preview::-webkit-scrollbar { + width: 8px; +} + +.markdown-preview::-webkit-scrollbar-thumb { + background: var(--border-primary); + border-radius: 4px; +} + +.preview h1, .preview h2, .preview h3, .preview h4, .preview h5, .preview h6 { + font-weight: 600; + margin-top: 0; + margin-bottom: 0.8em; + line-height: 1.4; +} + +.preview h1 { + font-size: 2em; + color: var(--accent-primary); + border-bottom: 1px solid var(--border-primary); + padding-bottom: 0.3em; + margin-bottom: 1em; +} + +.preview h2 { + font-size: 1.5em; + color: var(--accent-primary); + margin-bottom: 0.9em; +} + +.preview h3 { + font-size: 1.25em; + color: var(--text-primary); +} + +.preview h4 { + font-size: 1.1em; + color: var(--text-secondary); +} + +.preview h5 { + font-size: 1em; + color: var(--text-secondary); +} + +.preview h6 { + font-size: 1em; + color: var(--text-muted); +} + +.preview p { + margin-top: 0; + margin-bottom: 1em; + line-height: 1.6; +} + +.preview p:last-child { + margin-bottom: 0; +} + +.preview ul, .preview ol { + margin-top: 0; + margin-bottom: 1em; + padding-left: 2em; +} + +.preview li { + margin-top: 0.3em; + margin-bottom: 0.3em; + line-height: 1.5; +} + +.preview ul > li::marker { + color: var(--accent-primary); +} + +.preview ol > li::marker { + color: var(--accent-primary); + font-weight: 600; +} + +.preview blockquote { + margin: 0 0 1em 0; + padding-left: 1.5em; + border-left: 4px solid var(--accent-primary); + color: var(--text-secondary); + font-style: italic; +} + +.preview hr { + margin: 1.5em 0; + border: none; + border-top: 1px solid var(--border-primary); +} + +.preview a { + color: var(--accent-primary); + text-decoration: none; + font-weight: 500; +} + +.preview a:hover { + text-decoration: underline; + color: var(--accent-hover); +} + +.preview strong, .preview b { + color: var(--text-primary); + font-weight: 600; +} + +.preview em, .preview i { + color: var(--text-secondary); + font-style: italic; +} + +.preview code { + background: var(--bg-tertiary); + padding: 0.2em 0.4em; + border-radius: var(--radius-sm); + font-size: 0.85em; + color: var(--accent-primary); + font-weight: 500; +} + +.preview pre { + background: var(--bg-tertiary); + border: 1px solid var(--border-primary); + border-radius: var(--radius-md); + padding: var(--spacing-md); + overflow-x: auto; + margin: 0 0 1em 0; + line-height: 1.5; +} + +.preview pre code { + background: none; + padding: 0; + color: var(--text-primary); +} + +.preview blockquote { + border-left: 3px solid var(--accent-primary); + padding-left: var(--spacing-md); + margin-left: 0; + color: var(--text-secondary); + font-style: italic; +} + +.preview table { + width: 100%; + border-collapse: collapse; + margin: 1em 0; + background: var(--bg-tertiary); + border-radius: var(--radius-md); + overflow: hidden; +} + +.preview table thead { + background: var(--accent-primary); +} + +.preview table thead th { + color: white; + font-weight: 600; + text-align: left; + padding: var(--spacing-md); + border: none; +} + +.preview table tbody tr { + border-bottom: 1px solid var(--border-primary); + transition: background var(--transition-fast); +} + +.preview table tbody tr:last-child { + border-bottom: none; +} + +.preview table tbody tr:hover { + background: var(--bg-secondary); +} + +.preview table td { + padding: var(--spacing-md); + border: none; + color: var(--text-primary); +} + +.preview table tbody td { + border-right: 1px solid var(--border-primary); +} + +.preview table tbody td:last-child { + border-right: none; +} + +/* Buttons */ +button, +[type="submit"], +[type="button"] { + background: var(--accent-primary); + color: white; + border: none; + border-radius: var(--radius-md); + padding: var(--spacing-sm) var(--spacing-lg); + font-size: 0.9rem; + font-weight: 500; + cursor: pointer; + transition: all var(--transition-fast); + box-shadow: var(--shadow-sm); +} + +button:hover, +[type="submit"]:hover, +[type="button"]:hover { + transform: translateY(-1px); + box-shadow: var(--shadow-md), var(--shadow-glow); +} + +button:active, +[type="submit"]:active, +[type="button"]:active { + transform: translateY(0); +} + +button.secondary { + background: var(--bg-tertiary); + color: var(--text-primary); + border: 1px solid var(--border-primary); +} + +button.secondary:hover { + background: var(--bg-elevated); + border-color: var(--accent-primary); + box-shadow: var(--shadow-sm); +} + +/* Slash command palette */ +#slash-commands-palette { + background: var(--bg-secondary); + border: 1px solid var(--accent-primary); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-lg), var(--shadow-glow); + padding: var(--spacing-xs); + min-width: 220px; + max-height: 320px; + overflow-y: auto; +} + +#slash-commands-palette::-webkit-scrollbar { + width: 6px; +} + +#slash-commands-palette::-webkit-scrollbar-track { + background: transparent; +} + +#slash-commands-palette::-webkit-scrollbar-thumb { + background: var(--border-primary); + border-radius: 3px; +} + +#slash-commands-palette li { + padding: var(--spacing-sm) var(--spacing-md); + cursor: pointer; + color: var(--text-primary); + transition: all var(--transition-fast); + border-radius: var(--radius-sm); + margin: var(--spacing-xs) 0; + font-size: 0.9rem; + font-family: 'Fira Code', 'Cascadia Code', 'Consolas', monospace; + display: flex; + align-items: center; + gap: var(--spacing-sm); +} + +#slash-commands-palette li::before { + content: '⌘'; + color: var(--text-muted); + font-size: 0.85em; +} + +#slash-commands-palette li:hover { + background: var(--bg-tertiary); + color: var(--accent-primary); + transform: translateX(2px); +} + +#slash-commands-palette li[style*="background-color"] { + background: var(--accent-primary) !important; + color: white !important; + font-weight: 500; + transform: translateX(2px); +} + +#slash-commands-palette li[style*="background-color"]::before { + color: rgba(255, 255, 255, 0.8); +} + +/* Labels */ +label { + color: var(--text-secondary); + font-size: 0.9rem; + font-weight: 500; + margin-bottom: var(--spacing-sm); + display: block; +} + +/* Progress indicator */ +progress { + background: var(--bg-tertiary); + border: 1px solid var(--border-primary); + border-radius: var(--radius-sm); +} + +progress::-webkit-progress-bar { + background: var(--bg-tertiary); + border-radius: var(--radius-sm); +} + +progress::-webkit-progress-value { + background: var(--accent-primary); + border-radius: var(--radius-sm); +} + +/* CSS Reset */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +/* Form elements base styles */ +input, textarea, select { + font-family: inherit; + font-size: inherit; +} + +/* Remove default form margins */ +form { + margin: 0; +} + +/* Ensure flex containers work properly */ +body, html { + margin: 0; + padding: 0; + height: 100%; + width: 100%; +} + +/* Modal styles */ +#new-note-modal, +#new-folder-modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 9999; + display: flex; + align-items: center; + justify-content: center; +} + +.modal-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.7); + backdrop-filter: blur(4px); + animation: fadeIn 0.2s ease; +} + +.modal-content { + position: relative; + background: var(--bg-secondary); + border: 1px solid var(--accent-primary); + border-radius: var(--radius-lg); + padding: var(--spacing-lg); + min-width: 350px; + max-width: 90%; + box-shadow: var(--shadow-lg), var(--shadow-glow); + animation: slideUp 0.3s ease; + z-index: 1; +} + +.modal-content h2 { + margin: 0 0 var(--spacing-md) 0; + color: var(--text-primary); + font-size: 1.2rem; +} + +.modal-content label { + display: block; + margin-bottom: var(--spacing-xs); + color: var(--text-secondary); + font-weight: 500; + font-size: 0.9rem; +} + +.modal-content input[type="text"] { + width: 100%; + background: var(--bg-tertiary); + border: 1px solid var(--border-primary); + color: var(--text-primary); + border-radius: var(--radius-md); + padding: var(--spacing-sm) var(--spacing-md); + font-size: 0.95rem; + transition: all var(--transition-fast); +} + +.modal-content input[type="text"]:focus { + outline: none; + border-color: var(--accent-primary); + box-shadow: 0 0 0 3px rgba(88, 166, 255, 0.1); +} + +.modal-content p { + font-size: 0.8rem; + margin-top: 0.25rem; + margin-bottom: 0; +} + +.modal-content form > div { + margin-top: 1rem !important; +} + +.modal-content button { + padding: var(--spacing-sm) var(--spacing-md); + font-size: 0.9rem; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* File tree hierarchical styles */ +#file-tree { + font-size: 0.9rem; +} + +.folder-item { + margin: var(--sidebar-item-gap) 0; +} + +.folder-header { + display: flex; + align-items: center; + gap: var(--spacing-xs); + padding: var(--sidebar-padding-v) var(--sidebar-padding-h); + cursor: pointer; + border-radius: var(--radius-sm); + transition: all var(--transition-fast); + user-select: none; + font-size: 0.875rem; +} + +.folder-header:hover { + background: var(--bg-tertiary); + color: var(--accent-primary); +} + +.folder-toggle { + display: inline-block; + font-size: 0.65rem; + transition: transform var(--transition-fast); + min-width: 10px; +} + +.folder-toggle.expanded { + transform: rotate(90deg); +} + +.folder-icon { + font-size: 0.95rem; +} + +.folder-name { + flex: 1; + font-weight: 500; +} + +.folder-children { + padding-left: var(--sidebar-indent); + overflow: hidden; + transition: all var(--transition-fast); +} + +.file-item { + display: flex; + align-items: center; + gap: var(--spacing-xs); + padding: var(--sidebar-padding-v) var(--sidebar-padding-h); + color: var(--text-primary); + text-decoration: none; + border-radius: var(--radius-sm); + transition: all var(--transition-fast); + font-size: 0.85rem; + margin: var(--sidebar-item-gap) 0; + cursor: pointer; +} + +.file-item:hover { + background: var(--bg-tertiary); + color: var(--accent-primary); + transform: translateX(2px); +} + +/* Drag and drop styles */ +.file-item.dragging { + opacity: 0.4; + background: var(--bg-tertiary); + cursor: grabbing; +} + +.folder-header.dragging { + opacity: 0.4; + background: var(--bg-tertiary); + cursor: grabbing; +} + +.folder-item.drag-over .folder-header { + background: var(--accent-primary); + color: white; + box-shadow: var(--shadow-glow); + border: 2px solid var(--accent-primary); + border-radius: var(--radius-md); +} + +.file-item.drag-over { + background: var(--accent-primary); + color: white; + box-shadow: var(--shadow-glow); +} + +/* Style pour la racine en drag-over */ +.sidebar-section-header.drag-over { + background: var(--accent-primary) !important; + color: white !important; + box-shadow: var(--shadow-glow); + border: 2px solid var(--accent-primary); + border-radius: var(--radius-md); +} + +.sidebar-section-header.drag-over .folder-name, +.sidebar-section-header.drag-over .root-hint, +.sidebar-section-header.drag-over .folder-icon { + color: white !important; +} + +/* Indicateur de destination pendant le drag */ +.drag-destination-indicator { + position: fixed; + bottom: 20px; + left: 50%; + transform: translateX(-50%); + background: var(--bg-secondary); + border: 2px solid var(--accent-primary); + border-radius: var(--radius-lg); + padding: var(--spacing-md) var(--spacing-lg); + box-shadow: var(--shadow-lg), var(--shadow-glow); + z-index: 10000; + display: none; + align-items: center; + gap: var(--spacing-md); + min-width: 300px; + animation: slideUp 0.3s ease; +} + +.drag-destination-indicator .indicator-icon { + font-size: 1.5rem; + flex-shrink: 0; +} + +.drag-destination-indicator .indicator-text { + color: var(--text-primary); + font-size: 0.95rem; + flex: 1; +} + +.drag-destination-indicator .indicator-text strong { + color: var(--accent-primary); + font-weight: 600; +} + +.drag-destination-indicator .indicator-path { + font-size: 0.75rem; + color: var(--text-muted); + font-family: 'Fira Code', 'Cascadia Code', monospace; + padding: 2px 6px; + background: var(--bg-tertiary); + border-radius: var(--radius-sm); + border: 1px solid var(--border-primary); +} + +/* Curseur pendant le drag */ +.folder-header[draggable="true"] { + cursor: grab; +} + +.folder-header[draggable="true"]:active { + cursor: grabbing; +} + +/* Zone de drop racine */ +/* Indicateur de racine (non cliquable) */ +.root-indicator { + background: var(--bg-tertiary); + border: 1px solid var(--border-primary); + border-radius: var(--radius-md); + padding: var(--spacing-sm) var(--spacing-md); + display: flex; + align-items: center; + gap: 0.5rem; + cursor: default; + user-select: none; + margin-bottom: 0.5rem; +} + +.root-indicator .folder-icon { + font-size: 1.1rem; + opacity: 0.8; +} + +.root-indicator .folder-name { + font-weight: 500; + color: var(--text-secondary); + font-size: 0.85rem; +} + +.root-indicator .root-hint { + font-size: 0.75rem; + color: var(--text-muted); + font-family: 'Fira Code', 'Cascadia Code', monospace; + margin-left: auto; +} + +/* Styles pour root-drop-zone conservés pour la compatibilité drag & drop */ +.root-drop-zone { + margin-bottom: 0.5rem; +} + +.root-folder-header { + background: var(--bg-tertiary); + border: 1px solid var(--border-primary); + border-radius: var(--radius-md); + padding: var(--spacing-sm) var(--spacing-md) !important; + display: flex; + align-items: center; + gap: 0.5rem; + cursor: default !important; /* Pas draggable */ + transition: all var(--transition-fast); + user-select: none; +} + +.root-folder-header .folder-icon { + font-size: 1.1rem; +} + +.root-folder-header .folder-name { + font-weight: 600; + color: var(--text-primary); + font-size: 0.9rem; +} + +.root-folder-header .root-hint { + font-size: 0.75rem; + color: var(--text-muted); + font-family: 'Fira Code', 'Cascadia Code', monospace; + margin-left: auto; +} + +/* Quand on drag au-dessus de la racine */ +.root-drop-zone.drag-over .root-folder-header { + background: var(--accent-primary); + color: white; + border-color: var(--accent-primary); + box-shadow: var(--shadow-glow); +} + +.root-drop-zone.drag-over .root-folder-header .folder-name, +.root-drop-zone.drag-over .root-folder-header .root-hint { + color: white; +} + +/* Sidebar toggle styles */ +.main-layout.sidebar-hidden aside { + transform: translateX(-100%); +} + +.main-layout.sidebar-hidden main { + margin-left: 0; +} + +.auto-save-status { + margin-left: 1rem; + font-size: 0.8rem; + color: var(--text-muted); + font-style: italic; +} + +/* Utility class to hide elements */ +.hidden { + display: none !important; +} + +/* Bouton de fermeture sidebar - caché par défaut (desktop) */ +.sidebar-close-btn { + display: none; +} + +/* ======================================== + STYLES POUR L'ARBORESCENCE DE NOTES + ======================================== */ + +.note-tree { + width: 100%; + max-width: 100%; + box-sizing: border-box; +} + +.folder, .file { + width: 100%; + max-width: 100%; + box-sizing: border-box; + overflow: hidden; +} + +.folder-header { + cursor: pointer; + user-select: none; + padding: 0.4rem 0; + display: flex; + align-items: center; + gap: 0.5rem; + transition: all var(--transition-fast); +} + +.folder-header:hover { + color: var(--accent-primary); +} + +.folder-icon { + flex-shrink: 0; +} + +.folder-content { + display: block; +} + +.file { + padding: 0.3rem 0; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.file-icon { + flex-shrink: 0; + opacity: 0.6; + transition: opacity var(--transition-fast); +} + +.file a { + color: var(--text-primary); + text-decoration: none; + cursor: pointer; + word-break: break-word; + flex: 1; + font-size: 0.95rem; + transition: color var(--transition-fast); +} + +.file a:hover { + color: var(--accent-primary); +} + +.file:hover .file-icon { + opacity: 1; +} + +/* Indentation levels - Desktop */ +.indent-level-1 { padding-left: 0; } +.indent-level-2 { padding-left: 0.5rem; } +.indent-level-3 { padding-left: 1rem; } +.indent-level-4 { padding-left: 1.5rem; } +.indent-level-5 { padding-left: 2rem; } +.indent-level-6 { padding-left: 2.5rem; } +.indent-level-7 { padding-left: 3rem; } +.indent-level-8 { padding-left: 3.5rem; } + +/* ======================================== + RESPONSIVE DESIGN - Mobile & Tablet + ======================================== */ + +/* Tablettes et petits écrans (max 768px) */ +@media screen and (max-width: 768px) { + /* Empêcher tout débordement horizontal */ + html, body { + overflow-x: hidden; + max-width: 100vw; + } + + body { + position: relative; + } + + /* Tous les éléments doivent respecter la largeur */ + * { + max-width: 100%; + } + + /* Header responsive */ + header { + flex-wrap: wrap; + padding: 0.75rem; + gap: 0.5rem; + width: 100%; + box-sizing: border-box; + } + + header h1 { + font-size: 1.2rem; + margin: 0; + } + + header input[type="search"] { + max-width: 100%; + flex: 1 1 100%; + order: 10; + } + + header button { + padding: 0.4rem 0.8rem; + font-size: 0.85rem; + } + + /* Main layout en colonne pour mobile */ + .main-layout { + flex-direction: column; + width: 100%; + max-width: 100vw; + overflow-x: hidden; + } + + /* Sidebar masquée par défaut sur mobile */ + aside { + position: fixed; + top: 0; + left: -280px; + width: 280px !important; /* Force width on mobile, ignore resize */ + min-width: 280px; + max-width: 280px; + height: 100vh; + z-index: 1000; + transition: left 0.3s cubic-bezier(0.4, 0, 0.2, 1); + overflow-y: auto; + overflow-x: hidden; + box-shadow: var(--shadow-lg); + background: var(--bg-primary); + } + + /* Hide resize handle on mobile */ + .sidebar-resize-handle { + display: none; + } + + aside.sidebar-visible { + left: 0; + } + + /* Overlay pour fermer la sidebar */ + .sidebar-overlay { + display: none; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.6); + z-index: 999; + cursor: pointer; + -webkit-tap-highlight-color: transparent; + } + + .sidebar-overlay.active { + display: block; + animation: fadeIn 0.3s ease; + } + + @keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + + /* Main content prend toute la largeur */ + main { + margin-left: 0 !important; + width: 100%; + max-width: 100vw; + padding: 1rem; + box-sizing: border-box; + overflow-x: hidden; + } + + #editor-container, + #main-content { + width: 100%; + max-width: 100%; + box-sizing: border-box; + overflow-x: hidden; + } + + /* Editor grid en colonne sur mobile */ + .editor-grid { + grid-template-columns: 1fr !important; + gap: 0; + width: 100%; + max-width: 100%; + box-sizing: border-box; + } + + .editor-panel, + .editor-form { + width: 100%; + max-width: 100%; + box-sizing: border-box; + overflow-x: hidden; + } + + /* Forcer la preview seule visible par défaut sur mobile */ + .editor-grid.mobile-preview-only .editor-panel { + display: none !important; + } + + /* Ajuster les tailles de police */ + body { + font-size: 14px; + } + + /* Modales en plein écran sur mobile */ + .modal-content { + width: 95%; + max-width: 95%; + margin: 1rem; + } + + /* Boutons d'action en colonne */ + .editor-actions-primary { + flex-direction: column; + gap: 0.5rem; + } + + .editor-actions-primary button { + width: 100%; + } + + /* Toggle preview button plus visible */ + .toggle-preview-btn { + min-width: auto; + } + + /* File tree compact */ + #file-tree { + font-size: 0.9rem; + } + + /* Réduire le padding des sections sidebar */ + aside section { + padding: 0.75rem; + } + + /* Preview prend tout l'espace et est centré */ + .preview { + min-height: 60vh; + padding: 0.75rem; + max-width: 100%; + width: 100%; + margin: 0 auto; + box-sizing: border-box; + overflow-x: auto; + word-wrap: break-word; + overflow-wrap: break-word; + } + + /* S'assurer que le contenu du preview ne déborde pas */ + .preview * { + max-width: 100%; + box-sizing: border-box; + } + + .preview pre { + overflow-x: auto; + max-width: 100%; + } + + .preview table { + display: block; + overflow-x: auto; + max-width: 100%; + } + + .preview img { + max-width: 100%; + height: auto; + } + + .preview code { + word-break: break-all; + } + + /* Centrer le contenu de la page d'accueil sur mobile */ + .note-tree { + max-width: 100%; + width: 100%; + padding: 0.5rem; + box-sizing: border-box; + overflow-x: hidden; + } + + .note-tree * { + max-width: 100%; + box-sizing: border-box; + } + + /* Indentation réduite sur mobile pour éviter les débordements */ + .indent-level-1 { padding-left: 0 !important; } + .indent-level-2 { padding-left: 0.75rem !important; } + .indent-level-3 { padding-left: 1.5rem !important; } + .indent-level-4 { padding-left: 2.25rem !important; } + .indent-level-5 { padding-left: 3rem !important; } + .indent-level-6 { padding-left: 3.5rem !important; } + .indent-level-7 { padding-left: 4rem !important; } + .indent-level-8 { padding-left: 4.5rem !important; } + + /* Les dossiers et fichiers */ + .folder, + .file { + max-width: 100%; + overflow: hidden; + padding-top: 0.3rem !important; + padding-bottom: 0.3rem !important; + } + + .folder-header { + max-width: 100%; + overflow: hidden; + padding: 0.3rem 0 !important; + font-size: 0.9rem; + } + + .file { + font-size: 0.9rem; + } + + .file-icon, + .folder-icon { + font-size: 1rem; + } + + /* Améliorer la sidebar sur mobile */ + aside { + padding-top: 1rem; + } + + /* Bouton de fermeture dans le menu mobile */ + .sidebar-close-btn { + display: block; + position: sticky; + top: 0; + right: 0; + background: var(--bg-tertiary); + border: none; + color: var(--text-primary); + font-size: 1.5rem; + padding: 0.5rem; + cursor: pointer; + width: 100%; + text-align: right; + z-index: 10; + margin-bottom: 1rem; + } + + /* CodeMirror ajusté pour mobile */ + .CodeMirror, + .cm-editor { + font-size: 14px; + max-width: 100%; + width: 100%; + box-sizing: border-box; + } + + .cm-scroller { + overflow-x: auto; + max-width: 100%; + } + + /* Accordéon de dossiers plus compact */ + .folder-header { + padding: 0.4rem 0 !important; + font-size: 0.9rem; + } + + .file { + padding: 0.3rem 0 !important; + font-size: 0.9rem; + } +} + +/* Smartphones en mode portrait (max 480px) */ +@media screen and (max-width: 480px) { + /* Encore plus strict sur les débordements */ + html, body { + overflow-x: hidden; + max-width: 100vw; + width: 100vw; + } + + header h1 { + font-size: 1rem; + } + + header button { + padding: 0.3rem 0.6rem; + font-size: 0.8rem; + } + + aside { + width: 100%; + left: -100%; + } + + .modal-content { + width: 100%; + height: 100vh; + margin: 0; + border-radius: 0; + } + + body { + font-size: 13px; + } + + /* Réduire encore plus les marges */ + main { + padding: 0.25rem; + } + + .preview { + padding: 0.5rem; + } + + /* Centrage parfait pour très petits écrans */ + #editor-container { + padding: 0; + } + + .note-tree { + padding: 0.25rem; + } + + /* Texte plus petit si nécessaire */ + .folder-header, + .file { + font-size: 0.85rem; + } + + /* Adapter la modale de recherche sur mobile */ + .search-modal-container { + width: 100%; + max-width: 100%; + margin: 0; + border-radius: 0; + max-height: 100vh; + } + + .search-modal-input { + font-size: 14px; + } + + .search-modal-results { + max-height: calc(100vh - 200px); + } +} + +/* ========================================================================== + Search Modal Styles + ========================================================================== */ + +.search-modal { + display: none; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 10000; + align-items: flex-start; + justify-content: center; + padding-top: 10vh; + opacity: 0; + transition: opacity 200ms ease; +} + +.search-modal.active { + opacity: 1; +} + +.search-modal-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.7); + backdrop-filter: blur(4px); +} + +.search-modal-container { + position: relative; + width: 90%; + max-width: 680px; + background: var(--bg-secondary); + border: 1px solid var(--border-primary); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-lg), var(--shadow-glow); + display: flex; + flex-direction: column; + max-height: 70vh; + transform: translateY(-20px); + transition: transform 200ms ease; +} + +.search-modal.active .search-modal-container { + transform: translateY(0); +} + +/* Header */ +.search-modal-header { + padding: var(--spacing-md); + border-bottom: 1px solid var(--border-primary); +} + +.search-modal-input-wrapper { + display: flex; + align-items: center; + gap: var(--spacing-sm); +} + +.search-modal-icon { + color: var(--text-muted); + flex-shrink: 0; +} + +.search-modal-input { + flex: 1; + background: transparent; + border: none; + color: var(--text-primary); + font-size: 16px; + outline: none; + padding: var(--spacing-sm) 0; +} + +.search-modal-input::placeholder { + color: var(--text-muted); +} + +.search-modal-kbd { + background: var(--bg-tertiary); + border: 1px solid var(--border-primary); + border-radius: var(--radius-sm); + padding: 2px 6px; + font-size: 11px; + font-family: monospace; + color: var(--text-muted); + flex-shrink: 0; +} + +/* Body */ +.search-modal-body { + flex: 1; + overflow: hidden; + display: flex; + flex-direction: column; +} + +.search-modal-results { + flex: 1; + overflow-y: auto; + padding: var(--spacing-sm); +} + +/* Results Header */ +.search-modal-results-header { + padding: var(--spacing-sm) var(--spacing-md); + color: var(--text-muted); + font-size: 0.85rem; +} + +.search-modal-results-count { + font-weight: 500; +} + +/* Result Item */ +.search-modal-result-item { + display: flex; + gap: var(--spacing-md); + padding: var(--spacing-md); + margin: var(--spacing-xs) 0; + border-radius: var(--radius-md); + cursor: pointer; + transition: all var(--transition-fast); + border: 1px solid transparent; +} + +.search-modal-result-item:hover { + background: var(--bg-tertiary); +} + +.search-modal-result-item.selected { + background: var(--bg-secondary); + border-color: var(--accent-primary); +} + +.search-modal-result-icon { + font-size: 1.5rem; + flex-shrink: 0; +} + +.search-modal-result-content { + flex: 1; + min-width: 0; +} + +.search-modal-result-title { + font-weight: 500; + color: var(--text-primary); + margin-bottom: 4px; +} + +.search-modal-result-title mark { + background: var(--accent-primary); + color: white; + padding: 2px 4px; + border-radius: 3px; + font-weight: 600; +} + +.search-modal-result-path { + font-size: 0.8rem; + color: var(--text-muted); + margin-bottom: 6px; +} + +.search-modal-result-snippet { + font-size: 0.85rem; + color: var(--text-secondary); + margin-bottom: 8px; + line-height: 1.5; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.search-modal-result-snippet mark { + background: rgba(130, 170, 255, 0.3); + color: var(--accent-primary); + padding: 2px 4px; + border-radius: 3px; + font-weight: 500; +} + +.search-modal-result-footer { + display: flex; + align-items: center; + gap: var(--spacing-md); + flex-wrap: wrap; +} + +.search-modal-result-tags { + display: flex; + gap: var(--spacing-xs); + flex-wrap: wrap; +} + +.search-modal-result-tags .tag-pill { + background: var(--bg-tertiary); + color: var(--accent-primary); + border: 1px solid var(--border-primary); + padding: 2px 8px; + border-radius: 12px; + font-size: 0.75rem; + font-weight: 500; +} + +.search-modal-result-date { + font-size: 0.75rem; + color: var(--text-muted); +} + +/* Help */ +.search-modal-help { + padding: var(--spacing-lg); + text-align: center; +} + +.search-modal-help-title { + font-size: 1.1rem; + font-weight: 500; + color: var(--text-primary); + margin-bottom: var(--spacing-lg); +} + +.search-modal-help-items { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: var(--spacing-md); +} + +.search-modal-help-item { + display: flex; + flex-direction: column; + gap: var(--spacing-xs); + padding: var(--spacing-md); + background: var(--bg-tertiary); + border: 1px solid var(--border-primary); + border-radius: var(--radius-md); + transition: all var(--transition-fast); +} + +.search-modal-help-item:hover { + border-color: var(--accent-primary); + transform: translateY(-2px); +} + +.search-modal-help-item code { + background: var(--bg-primary); + color: var(--accent-primary); + padding: 4px 8px; + border-radius: var(--radius-sm); + font-size: 0.9rem; + font-family: 'Fira Code', 'Cascadia Code', monospace; + border: 1px solid var(--border-primary); +} + +.search-modal-help-item span { + font-size: 0.85rem; + color: var(--text-muted); +} + +/* Loading */ +.search-modal-loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: var(--spacing-xl); + gap: var(--spacing-md); +} + +.search-modal-spinner { + width: 40px; + height: 40px; + border: 3px solid var(--border-primary); + border-top-color: var(--accent-primary); + border-radius: 50%; +} + +.search-modal-loading p { + color: var(--text-muted); + margin: 0; +} + +/* No Results */ +.search-modal-no-results { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: var(--spacing-xl); + text-align: center; +} + +.search-modal-no-results-icon { + font-size: 3rem; + margin-bottom: var(--spacing-md); + opacity: 0.5; +} + +.search-modal-no-results-text { + color: var(--text-primary); + margin: 0 0 var(--spacing-sm) 0; +} + +.search-modal-no-results-hint { + color: var(--text-muted); + font-size: 0.9rem; + margin: 0; +} + +/* Error */ +.search-modal-error { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: var(--spacing-xl); + text-align: center; +} + +.search-modal-error-icon { + font-size: 3rem; + margin-bottom: var(--spacing-md); +} + +.search-modal-error p { + color: var(--error); + margin: 0; +} + +/* Footer */ +.search-modal-footer { + border-top: 1px solid var(--border-primary); + padding: var(--spacing-sm) var(--spacing-md); + background: var(--bg-tertiary); +} + +.search-modal-footer-hint { + display: flex; + align-items: center; + gap: var(--spacing-sm); + font-size: 0.8rem; + color: var(--text-muted); +} + +.search-modal-footer-hint kbd { + background: var(--bg-primary); + border: 1px solid var(--border-primary); + border-radius: var(--radius-sm); + padding: 2px 6px; + font-size: 11px; + font-family: monospace; + color: var(--text-secondary); +} + +/* Scrollbar pour les résultats */ +.search-modal-results::-webkit-scrollbar { + width: 8px; +} + +.search-modal-results::-webkit-scrollbar-track { + background: var(--bg-secondary); +} + +.search-modal-results::-webkit-scrollbar-thumb { + background: var(--border-primary); + border-radius: 4px; +} + +.search-modal-results::-webkit-scrollbar-thumb:hover { + background: var(--text-muted); +} + +/* ========================================================================== + Tags Cloud (Home Page) + ========================================================================== */ + +.tags-cloud { + display: flex; + flex-wrap: wrap; + gap: 6px; + padding: 12px 0; + margin: 12px 0 24px 0; + max-height: 150px; + overflow-y: auto; + line-height: 1.4; + border-bottom: 1px solid var(--border-primary); +} + +.tag-item { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 3px 8px; + background: transparent; + border: 1px solid var(--border-primary); + border-radius: 4px; + text-decoration: none; + font-size: 0.8rem; + transition: all var(--transition-fast); + cursor: pointer; +} + +.tag-item:hover { + background: var(--bg-tertiary); + border-color: var(--accent-primary); +} + +.tag-item:active { + transform: scale(0.98); +} + +/* Badge style pour le tag (kbd) - version discrète */ +.tag-badge { + background: transparent; + color: var(--text-secondary); + padding: 0; + font-family: inherit; + font-size: 1em; + font-weight: 500; + border: none; + box-shadow: none; +} + +.tag-item:hover .tag-badge { + color: var(--accent-primary); +} + +/* Badge count style (mark) - version discrète */ +.tag-count { + background: transparent; + color: var(--text-muted); + padding: 0; + border-radius: 0; + font-size: 0.9em; + font-weight: 400; + border: none; + min-width: auto; +} + +.tag-item:hover .tag-count { + color: var(--text-secondary); +} + + +/* Scrollbar pour le nuage de tags */ +.tags-cloud::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +.tags-cloud::-webkit-scrollbar-track { + background: transparent; +} + +.tags-cloud::-webkit-scrollbar-thumb { + background: var(--border-primary); + border-radius: 3px; +} + +.tags-cloud::-webkit-scrollbar-thumb:hover { + background: var(--text-muted); +} + +/* Responsive */ +@media (max-width: 768px) { + .tags-cloud { + gap: 4px; + max-height: 120px; + } + + .tag-item { + font-size: 0.75rem; + padding: 2px 6px; + } +} + +/* ========================================================================== + SECTION FAVORIS SUR LA PAGE D'ACCUEIL + ========================================================================== */ + +/* Les favoris utilisent les mêmes styles que note-tree */ +.favorites-tree { + /* Hérite des styles de .note-tree */ + margin-bottom: 24px; + border-bottom: 1px solid var(--border-primary); + padding-bottom: 12px; + max-height: 400px; + overflow-y: auto; + overflow-x: hidden; +} + +/* Scrollbar pour la liste des favoris */ +.favorites-tree::-webkit-scrollbar { + width: 8px; +} + +.favorites-tree::-webkit-scrollbar-track { + background: var(--bg-tertiary); + border-radius: 4px; +} + +.favorites-tree::-webkit-scrollbar-thumb { + background: var(--border-primary); + border-radius: 4px; +} + +.favorites-tree::-webkit-scrollbar-thumb:hover { + background: var(--text-muted); +} + +/* Responsive */ +@media (max-width: 768px) { + .favorites-tree { + font-size: 0.9rem; + max-height: 300px; + } +} + +/* ========================================================================== + Daily Notes Calendar & Recent + ========================================================================== */ + +/* Calendar Container */ +.daily-calendar { + background: var(--bg-secondary); + border: 1px solid var(--border-primary); + border-radius: var(--radius-md); + padding: var(--spacing-md); + margin-bottom: var(--spacing-md); +} + +/* Calendar Header (Navigation) */ +.daily-calendar-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: var(--spacing-md); + padding-bottom: var(--spacing-sm); + border-bottom: 1px solid var(--border-primary); +} + +.calendar-month-year { + font-weight: 600; + font-size: 0.95rem; + color: var(--text-primary); +} + +.calendar-nav-btn { + background: var(--bg-tertiary); + border: 1px solid var(--border-primary); + color: var(--text-primary); + padding: 0.25rem 0.5rem; + border-radius: var(--radius-sm); + cursor: pointer; + font-size: 1.2rem; + transition: all var(--transition-fast); + line-height: 1; +} + +.calendar-nav-btn:hover { + background: var(--bg-elevated); + border-color: var(--accent-primary); + color: var(--accent-primary); + transform: translateY(-1px); +} + +/* Calendar Grid */ +.calendar-grid { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 2px; +} + +/* Weekday Headers */ +.calendar-weekday-header { + text-align: center; + font-size: 0.75rem; + font-weight: 600; + color: var(--text-secondary); + padding: 0.5rem 0; + text-transform: uppercase; +} + +/* Calendar Day Cells */ +.calendar-day { + aspect-ratio: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background: var(--bg-tertiary); + border-radius: var(--radius-sm); + cursor: default; + transition: all var(--transition-fast); + position: relative; + border: 2px solid transparent; + font-size: 0.85rem; +} + +/* Clickable days (with notes) */ +.calendar-day-clickable { + cursor: pointer; +} + +.calendar-day-clickable:hover { + background: var(--bg-elevated); + border-color: var(--accent-primary); + transform: scale(1.05); +} + +/* Days without notes */ +.calendar-day-no-note { + opacity: 0.5; + cursor: not-allowed; +} + +.calendar-day-no-note .calendar-day-number { + color: var(--text-muted); +} + +/* Day Number */ +.calendar-day-number { + font-weight: 500; + color: var(--text-primary); +} + +/* Indicator for notes */ +.calendar-day-indicator { + position: absolute; + bottom: 2px; + font-size: 0.5rem; + color: var(--accent-primary); +} + +/* Today */ +.calendar-day-today { + border-color: var(--accent-primary); + background: var(--bg-secondary); +} + +.calendar-day-today .calendar-day-number { + color: var(--accent-primary); + font-weight: 700; +} + +/* Has Note */ +.calendar-day-has-note { + font-weight: 600; +} + +.calendar-day-has-note .calendar-day-number { + color: var(--accent-primary); +} + +/* Other Month Days (grayed out) */ +.calendar-day-other-month { + opacity: 0.3; + cursor: default; + pointer-events: none; +} + +/* Today Button */ +.daily-today-btn { + width: 100%; + margin-top: var(--spacing-sm); + padding: var(--spacing-sm); + background: var(--accent-primary); + color: white; + border: none; + border-radius: var(--radius-md); + font-size: 0.85rem; + font-weight: 500; + cursor: pointer; + transition: all var(--transition-fast); +} + +.daily-today-btn:hover { + transform: translateY(-1px); + box-shadow: var(--shadow-md), var(--shadow-glow); +} + +/* Recent Notes List */ +.daily-recent { + display: flex; + flex-direction: column; + gap: 0.15rem; +} + +.daily-recent-item { + display: flex; + align-items: center; + gap: var(--spacing-sm); + padding: 0.4rem 0.6rem; + background: var(--bg-tertiary); + border: 1px solid var(--border-primary); + border-radius: var(--radius-sm); + text-decoration: none; + color: var(--text-primary); + transition: all var(--transition-fast); + cursor: pointer; +} + +.daily-recent-item:hover { + background: var(--bg-elevated); + border-color: var(--accent-primary); + transform: translateX(2px); +} + +.daily-recent-icon { + font-size: 0.9rem; + flex-shrink: 0; +} + +.daily-recent-content { + display: flex; + flex-direction: column; + gap: 0.05rem; + flex: 1; +} + +.daily-recent-weekday { + font-size: 0.65rem; + color: var(--text-muted); + text-transform: uppercase; + font-weight: 600; + letter-spacing: 0.02em; +} + +.daily-recent-title { + font-size: 0.8rem; + color: var(--text-primary); + font-weight: 500; +} + +/* Selection Mode - Suppression en masse */ + +/* Checkboxes de sélection */ +.selection-checkbox { + margin-right: var(--spacing-sm); + width: 18px; + height: 18px; + cursor: pointer; + accent-color: var(--accent-primary); + flex-shrink: 0; +} + +/* Wrapper pour les fichiers avec checkbox */ +.file-item-wrapper { + display: flex; + align-items: center; + padding: 0; + margin: 0; /* No margin on wrapper - use same spacing as folders */ +} + +.file-item-wrapper .file-item { + flex: 1; + margin: 0 !important; /* Force remove margin to avoid double spacing */ +} + +/* Bouton de mode sélection */ +.icon-button { + background: transparent; + border: 1px solid transparent; + color: var(--text-muted); + cursor: pointer; + padding: var(--spacing-xs); + border-radius: var(--radius-sm); + display: flex; + align-items: center; + justify-content: center; + transition: all var(--transition-fast); +} + +.icon-button:hover { + background: var(--bg-tertiary); + border-color: var(--border-primary); + color: var(--text-primary); +} + +.icon-button.active { + background: var(--accent-primary); + border-color: var(--accent-primary); + color: var(--bg-primary); +} + +.icon-button.active:hover { + background: var(--accent-hover); + border-color: var(--accent-hover); +} + +/* Toolbar de sélection flottante */ +.selection-toolbar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background: var(--bg-elevated); + border-top: 2px solid var(--accent-primary); + padding: var(--spacing-md) var(--spacing-lg); + box-shadow: var(--shadow-lg); + z-index: 1000; + animation: slideUp 0.3s ease; +} + +@keyframes slideUp { + from { + transform: translateY(100%); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +.toolbar-content { + display: flex; + justify-content: space-between; + align-items: center; + max-width: 1200px; + margin: 0 auto; +} + +.selection-count { + font-size: 1rem; + font-weight: 600; + color: var(--text-primary); +} + +.toolbar-actions { + display: flex; + gap: var(--spacing-md); +} + +/* Bouton danger */ +.danger-button { + background: var(--error); + border: 1px solid var(--error); + color: var(--bg-primary); + padding: var(--spacing-sm) var(--spacing-md); + border-radius: var(--radius-md); + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + transition: all var(--transition-fast); + display: flex; + align-items: center; + gap: var(--spacing-sm); + white-space: nowrap; +} + +.danger-button:hover { + background: #ff5370; + border-color: #ff5370; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(240, 113, 120, 0.15); +} + +.danger-button:active { + transform: translateY(0); +} + +/* Modale de confirmation de suppression */ +#delete-confirmation-modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 9999; + display: flex; + align-items: center; + justify-content: center; +} + +#delete-confirmation-modal h2 { + display: flex; + align-items: center; + gap: var(--spacing-sm); +} + +#delete-items-list code { + background: var(--bg-secondary); + padding: 0.2rem 0.5rem; + border-radius: var(--radius-sm); + font-size: 0.85rem; + color: var(--text-secondary); +} + +/* Responsive */ +@media (max-width: 768px) { + .daily-calendar { + padding: var(--spacing-sm); + } + + .calendar-day { + font-size: 0.75rem; + } + + .calendar-day-indicator { + font-size: 0.4rem; + } + + .daily-recent-item { + padding: var(--spacing-xs) var(--spacing-sm); + } + + /* Toolbar responsive */ + .selection-toolbar { + padding: var(--spacing-sm) var(--spacing-md); + } + + .toolbar-content { + flex-direction: column; + gap: var(--spacing-sm); + } + + .toolbar-actions { + width: 100%; + justify-content: space-between; + } + + .selection-count { + font-size: 0.9rem; + } +} + +/* =========================== + FAVORIS + =========================== */ + +/* Container des favoris avec scrolling */ +#favorites-list { + max-height: 300px; + overflow-y: auto; + overflow-x: hidden; + margin-bottom: var(--spacing-xs); + display: flex; + flex-direction: column; + gap: 0.1rem; +} + +/* Scrollbar pour la liste des favoris */ +#favorites-list::-webkit-scrollbar { + width: 6px; +} + +#favorites-list::-webkit-scrollbar-track { + background: transparent; +} + +#favorites-list::-webkit-scrollbar-thumb { + background: var(--border-primary); + border-radius: 3px; +} + +#favorites-list::-webkit-scrollbar-thumb:hover { + background: var(--text-muted); +} + +.favorite-item { + position: relative; +} + +.favorite-folder, +.favorite-file { + display: flex; + align-items: center; + gap: var(--spacing-xs); + padding: var(--sidebar-padding-v) var(--sidebar-padding-h); + border-radius: var(--radius-sm); + transition: all var(--transition-fast); + font-size: 0.85rem; + cursor: pointer; + text-decoration: none; + color: var(--text-primary); + position: relative; +} + +.favorite-folder:hover, +.favorite-file:hover { + background: var(--bg-tertiary); + color: var(--accent-primary); + transform: translateX(2px); +} + +.favorite-icon { + font-size: 0.9rem; + color: var(--warning); + flex-shrink: 0; +} + +.favorite-folder-icon, +.favorite-file-icon { + font-size: 0.9rem; + flex-shrink: 0; +} + +.favorite-name { + flex: 1; + font-weight: 500; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.favorite-remove { + opacity: 0; + background: var(--error); + color: white; + border: none; + border-radius: 50%; + width: 18px; + height: 18px; + font-size: 1rem; + line-height: 1; + cursor: pointer; + transition: all var(--transition-fast); + display: flex; + align-items: center; + justify-content: center; + padding: 0; + flex-shrink: 0; +} + +.favorite-folder:hover .favorite-remove, +.favorite-file:hover .favorite-remove { + opacity: 1; +} + +.favorite-remove:hover { + background: #d32f2f; + transform: scale(1.1); +} + +.favorites-empty { + text-align: center; + color: var(--text-muted); + font-size: 0.85rem; + padding: var(--spacing-md); + line-height: 1.5; +} + +/* Bouton d'ajout aux favoris (dans le file tree) */ +.add-to-favorites { + opacity: 0.4; /* Always slightly visible */ + background: transparent; + border: none; + color: var(--text-muted); + cursor: pointer; + font-size: 1rem; + padding: 0.25rem; + transition: all var(--transition-fast); + margin-left: auto; + border-radius: var(--radius-sm); +} + +.folder-header:hover .add-to-favorites, +.file-item:hover .add-to-favorites { + opacity: 1; + background: var(--bg-tertiary); +} + +.add-to-favorites:hover { + color: var(--warning); + transform: scale(1.15); + background: rgba(255, 193, 7, 0.1); /* Subtle yellow background */ +} + +.add-to-favorites.is-favorite { + opacity: 1 !important; /* Always fully visible when favorited */ + color: var(--warning); + background: rgba(255, 193, 7, 0.15); +} + +.add-to-favorites.is-favorite:hover { + background: rgba(255, 193, 7, 0.2); +} + +/* Responsive - Hauteurs adaptatives pour #favorites-list */ +@media screen and (max-height: 800px) { + #favorites-list { + max-height: 200px; + } +} + +@media screen and (min-height: 801px) and (max-height: 1000px) { + #favorites-list { + max-height: 300px; + } +} + +@media screen and (min-height: 1001px) { + #favorites-list { + max-height: 400px; + } +} + +/* Mobile - hauteur encore plus réduite */ +@media screen and (max-width: 768px) { + #favorites-list { + max-height: 180px; + } +} + +/* ========================================================================== + Link Inserter Modal Styles + ========================================================================== */ + +.link-inserter-modal { + display: none; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 10000; + align-items: flex-start; + justify-content: center; + padding-top: 15vh; + opacity: 0; + transition: opacity 200ms ease; +} + +.link-inserter-modal.active { + opacity: 1; +} + +.link-inserter-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(3px); +} + +.link-inserter-container { + position: relative; + width: 90%; + max-width: 560px; + background: var(--bg-secondary); + border: 1px solid var(--border-primary); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-lg), var(--shadow-glow); + display: flex; + flex-direction: column; + max-height: 60vh; + transform: translateY(-20px); + transition: transform 200ms ease; +} + +.link-inserter-modal.active .link-inserter-container { + transform: translateY(0); +} + +/* Header */ +.link-inserter-header { + padding: var(--spacing-md); + border-bottom: 1px solid var(--border-primary); +} + +.link-inserter-input-wrapper { + display: flex; + align-items: center; + gap: var(--spacing-sm); +} + +.link-inserter-icon { + color: var(--accent-primary); + flex-shrink: 0; +} + +.link-inserter-input { + flex: 1; + background: transparent; + border: none; + color: var(--text-primary); + font-size: 15px; + outline: none; + padding: var(--spacing-sm) 0; +} + +.link-inserter-input::placeholder { + color: var(--text-muted); +} + +.link-inserter-kbd { + background: var(--bg-tertiary); + border: 1px solid var(--border-primary); + border-radius: var(--radius-sm); + padding: 2px 6px; + font-size: 11px; + font-family: monospace; + color: var(--text-muted); + flex-shrink: 0; +} + +/* Body */ +.link-inserter-body { + flex: 1; + overflow: hidden; + display: flex; + flex-direction: column; +} + +.link-inserter-results { + flex: 1; + overflow-y: auto; + padding: var(--spacing-sm); +} + +/* Results Header */ +.link-inserter-results-header { + padding: var(--spacing-sm) var(--spacing-md); + color: var(--text-muted); + font-size: 0.8rem; +} + +.link-inserter-results-count { + font-weight: 500; +} + +/* Result Item */ +.link-inserter-result-item { + display: flex; + gap: var(--spacing-sm); + padding: var(--spacing-sm) var(--spacing-md); + margin: var(--spacing-xs) 0; + border-radius: var(--radius-md); + cursor: pointer; + transition: all var(--transition-fast); + border: 1px solid transparent; +} + +.link-inserter-result-item:hover { + background: var(--bg-tertiary); +} + +.link-inserter-result-item.selected { + background: var(--bg-secondary); + border-color: var(--accent-primary); +} + +.link-inserter-result-icon { + font-size: 1.2rem; + flex-shrink: 0; +} + +.link-inserter-result-content { + flex: 1; + min-width: 0; +} + +.link-inserter-result-title { + font-weight: 500; + color: var(--text-primary); + margin-bottom: 3px; + font-size: 0.95rem; +} + +.link-inserter-result-title mark { + background: var(--accent-primary); + color: white; + padding: 2px 4px; + border-radius: 3px; + font-weight: 600; +} + +.link-inserter-result-path { + font-size: 0.75rem; + color: var(--text-muted); + margin-bottom: 4px; +} + +.link-inserter-result-tags { + display: flex; + gap: 4px; + flex-wrap: wrap; + margin-top: 4px; +} + +.tag-pill-small { + background: var(--bg-tertiary); + color: var(--accent-primary); + border: 1px solid var(--border-primary); + padding: 1px 6px; + border-radius: 10px; + font-size: 0.7rem; + font-weight: 500; +} + +/* Help */ +.link-inserter-help { + padding: var(--spacing-xl); + text-align: center; +} + +.link-inserter-help-text { + font-size: 0.95rem; + color: var(--text-muted); +} + +/* Loading */ +.link-inserter-loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: var(--spacing-xl); + gap: var(--spacing-md); +} + +.link-inserter-spinner { + width: 32px; + height: 32px; + border: 3px solid var(--bg-tertiary); + border-top-color: var(--accent-primary); + border-radius: 50%; +} + +.link-inserter-loading p { + color: var(--text-muted); + font-size: 0.9rem; +} + +/* No Results */ +.link-inserter-no-results { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: var(--spacing-xl); + gap: var(--spacing-sm); +} + +.link-inserter-no-results-icon { + font-size: 2.5rem; + opacity: 0.6; +} + +.link-inserter-no-results p { + color: var(--text-secondary); + font-size: 0.9rem; + text-align: center; +} + +/* Error */ +.link-inserter-error { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: var(--spacing-xl); + gap: var(--spacing-sm); +} + +.link-inserter-error-icon { + font-size: 2.5rem; +} + +.link-inserter-error p { + color: var(--text-secondary); + font-size: 0.9rem; +} + +/* Footer */ +.link-inserter-footer { + padding: var(--spacing-sm) var(--spacing-md); + border-top: 1px solid var(--border-primary); + background: var(--bg-tertiary); +} + +.link-inserter-footer-hint { + display: flex; + align-items: center; + gap: var(--spacing-sm); + font-size: 0.75rem; + color: var(--text-muted); +} + +.link-inserter-footer-hint kbd { + background: var(--bg-secondary); + border: 1px solid var(--border-primary); + border-radius: var(--radius-sm); + padding: 2px 5px; + font-family: monospace; + font-size: 0.7rem; +} + +/* ============================================ + BACKLINKS SECTION + ============================================ */ + +/* Preview wrapper to contain both preview and backlinks */ +.preview-wrapper { + display: flex; + flex-direction: column; + gap: var(--spacing-md); + min-height: 0; + height: 100%; +} + +/* Backlinks section styling */ +.backlinks-section { + background: var(--bg-secondary); + border: 1px solid var(--border-primary); + border-radius: var(--radius-md); + padding: var(--spacing-md); + margin-top: var(--spacing-md); +} + +.backlinks-title { + font-size: 1rem; + font-weight: 600; + color: var(--text-primary); + margin: 0 0 var(--spacing-sm) 0; + padding-bottom: var(--spacing-sm); + border-bottom: 1px solid var(--border-primary); + display: flex; + align-items: center; + gap: var(--spacing-xs); +} + +.backlinks-list { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: var(--spacing-xs); +} + +.backlink-item { + margin: 0; + padding: 0; +} + +.backlink-link { + display: block; + padding: var(--spacing-sm) var(--spacing-md); + background: var(--bg-tertiary); + border: 1px solid var(--border-secondary); + border-radius: var(--radius-sm); + color: var(--text-primary); + text-decoration: none; + transition: all var(--transition-fast); + font-size: 0.9rem; + font-weight: 500; +} + +.backlink-link:hover { + background: var(--bg-primary); + border-color: var(--accent-primary); + color: var(--accent-primary); + transform: translateX(4px); + box-shadow: var(--shadow-sm); +} + +.backlink-link:active { + transform: translateX(2px); +} + +/* Mobile Adaptation */ +@media screen and (max-width: 768px) { + .link-inserter-container { + width: 100%; + max-width: 100%; + margin: 0; + border-radius: 0; + max-height: 100vh; + } + + .link-inserter-input { + font-size: 14px; + } + + .link-inserter-results { + max-height: calc(100vh - 200px); + } +} + +/* ======================================== + Recent Notes Section + ======================================== */ +.recent-notes-container { + display: flex; + flex-direction: column; + gap: var(--spacing-md); + max-height: 500px; + overflow-y: auto; + padding: var(--spacing-sm); + margin-bottom: var(--spacing-lg); + /* Masquer la scrollbar mais garder le scroll */ + scrollbar-width: none; /* Firefox */ + -ms-overflow-style: none; /* IE et Edge */ +} + +/* Masquer la scrollbar pour Chrome, Safari et Opera */ +.recent-notes-container::-webkit-scrollbar { + display: none; +} + +.recent-note-card { + background: var(--bg-secondary); + border: 1px solid var(--border-primary); + border-radius: var(--radius-md); + padding: var(--spacing-md); + transition: all var(--transition-fast); + box-shadow: var(--shadow-sm); +} + +.recent-note-card:hover { + border-color: var(--accent-primary); + box-shadow: var(--shadow-md), var(--shadow-glow); + transform: translateY(-2px); +} + +.recent-note-link { + text-decoration: none; + color: inherit; + display: block; +} + +.recent-note-title { + font-size: 1.1rem; + font-weight: 600; + color: var(--text-primary); + margin-bottom: var(--spacing-sm); + line-height: 1.4; +} + +.recent-note-meta { + display: flex; + flex-wrap: wrap; + gap: var(--spacing-md); + align-items: center; + margin-bottom: var(--spacing-sm); + font-size: 0.85rem; +} + +.recent-note-date { + color: var(--text-secondary); + display: flex; + align-items: center; + gap: 0.25rem; +} + +.recent-note-tags { + color: var(--accent-primary); + font-size: 0.8rem; + font-weight: 500; +} + +.recent-note-preview { + color: var(--text-muted); + font-size: 0.9rem; + line-height: 1.5; + margin-top: var(--spacing-sm); + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; +} + +/* ======================================== + Home Page Sections with Accordions + ======================================== */ +.home-section { + margin-bottom: var(--spacing-lg); + border: 1px solid var(--border-primary); + border-radius: var(--radius-lg); + background: var(--bg-secondary); + overflow: hidden; + transition: all var(--transition-fast); +} + +.home-section:hover { + border-color: var(--border-secondary); + box-shadow: var(--shadow-sm); +} + +.home-section-header { + display: flex; + align-items: center; + gap: var(--spacing-sm); + padding: var(--spacing-md) var(--spacing-lg); + cursor: pointer; + background: var(--bg-tertiary); + border-bottom: 1px solid var(--border-primary); + transition: all var(--transition-fast); + user-select: none; +} + +.home-section-header:hover { + background: var(--bg-secondary); +} + +.home-section-header:active { + background: var(--bg-primary); +} + +.home-section-title { + margin: 0; + font-size: 1.3rem; + font-weight: 600; + color: var(--text-primary); + flex: 1; +} + +.home-section-content { + padding: var(--spacing-lg); + overflow-y: auto; + transition: all var(--transition-medium); + max-height: 600px; + /* Masquer la scrollbar mais garder le scroll */ + scrollbar-width: none; /* Firefox */ + -ms-overflow-style: none; /* IE et Edge */ +} + +/* Masquer la scrollbar pour Chrome, Safari et Opera */ +.home-section-content::-webkit-scrollbar { + display: none; +} + +/* Adjust nested containers for accordion layout */ +.home-section .recent-notes-container { + padding: 0; + margin-bottom: 0; +} + +/* ======================================== + Breadcrumb Navigation + ======================================== */ +.breadcrumb { + display: inline-flex; + align-items: center; + flex-wrap: wrap; + gap: 0.25rem; + font-size: 0.95rem; +} + +.breadcrumb-link { + color: var(--accent-primary); + text-decoration: none; + padding: 0.25rem 0.5rem; + border-radius: var(--radius-sm); + transition: all var(--transition-fast); +} + +.breadcrumb-link:hover { + background: var(--bg-tertiary); + color: var(--accent-primary); +} + +.breadcrumb-separator { + color: var(--text-muted); + font-size: 0.9rem; +} + +/* ======================================== + Folder and File Lists (Folder View) + ======================================== */ +/* Styles spécifiques pour la vue de dossier dans l'éditeur */ +#editor-content .folder-list, +#editor-content .file-list { + display: flex; + flex-direction: column; + gap: var(--spacing-sm); + margin-bottom: var(--spacing-lg); +} + +#editor-content .folder-list .folder-item, +#editor-content .file-list .file-item { + padding: var(--spacing-md); + background: var(--bg-secondary); + border: 1px solid var(--border-primary); + border-radius: var(--radius-md); + transition: all var(--transition-fast); +} + +#editor-content .folder-list .folder-item:hover, +#editor-content .file-list .file-item:hover { + border-color: var(--accent-primary); + box-shadow: var(--shadow-sm); + transform: translateX(4px); +} + +#editor-content .folder-list .folder-item a, +#editor-content .file-list .file-item a { + color: var(--text-primary); + text-decoration: none; + display: block; + font-weight: 500; +} + +#editor-content .folder-list .folder-item:hover a, +#editor-content .file-list .file-item:hover a { + color: var(--accent-primary); +} + + + +/* Public toggle button */ +#toggle-public-btn { + transition: all 0.3s ease; +} + +#toggle-public-btn.public-active { + background: var(--accent-green, #2ecc71); + color: white; + border-color: var(--accent-green, #2ecc71); +} + +#toggle-public-btn.public-active:hover { + background: var(--accent-green-dark, #27ae60); + border-color: var(--accent-green-dark, #27ae60); +} diff --git a/public/static/themes.css b/public/static/themes.css new file mode 100644 index 0000000..1b64352 --- /dev/null +++ b/public/static/themes.css @@ -0,0 +1,679 @@ +/* + * PersoNotes - Multi-Theme System + * Supports: Material Dark (default), Monokai Dark, Dracula, One Dark, Solarized Dark, Nord + */ + +/* =========================== + THEME: MATERIAL DARK (défaut) + =========================== */ +:root, +[data-theme="material-dark"] { + --bg-primary: #1e1e1e; + --bg-secondary: #252525; + --bg-tertiary: #2d2d2d; + --bg-elevated: #323232; + + --border-primary: #3e3e3e; + --border-secondary: #2a2a2a; + + --text-primary: #e0e0e0; + --text-secondary: #b0b0b0; + --text-muted: #6e6e6e; + + --accent-primary: #42a5f5; + --accent-hover: #5ab3f7; + + --success: #66bb6a; + --warning: #ffa726; + --error: #ef5350; +} + +/* =========================== + THEME: MONOKAI DARK + =========================== */ +[data-theme="monokai-dark"] { + --bg-primary: #272822; + --bg-secondary: #2d2e27; + --bg-tertiary: #3e3d32; + --bg-elevated: #49483e; + + --border-primary: #49483e; + --border-secondary: #3e3d32; + + --text-primary: #f8f8f2; + --text-secondary: #cfcfc2; + --text-muted: #75715e; + + --accent-primary: #66d9ef; + --accent-hover: #7ee5f7; + + --success: #88c070; + --warning: #e6db74; + --error: #f92672; +} + +/* =========================== + THEME: DRACULA + =========================== */ +[data-theme="dracula"] { + --bg-primary: #282a36; + --bg-secondary: #2f3241; + --bg-tertiary: #373844; + --bg-elevated: #44475a; + + --border-primary: #44475a; + --border-secondary: #373844; + + --text-primary: #f8f8f2; + --text-secondary: #d6d6d6; + --text-muted: #6272a4; + + --accent-primary: #8be9fd; + --accent-hover: #9ff3ff; + + --success: #50fa7b; + --warning: #f1fa8c; + --error: #ff5555; +} + +/* =========================== + THEME: ONE DARK + =========================== */ +[data-theme="one-dark"] { + --bg-primary: #282c34; + --bg-secondary: #2c313a; + --bg-tertiary: #333842; + --bg-elevated: #3e4451; + + --border-primary: #3e4451; + --border-secondary: #333842; + + --text-primary: #abb2bf; + --text-secondary: #9ca3af; + --text-muted: #5c6370; + + --accent-primary: #61afef; + --accent-hover: #75bdf5; + + --success: #98c379; + --warning: #e5c07b; + --error: #e06c75; +} + +/* =========================== + THEME: SOLARIZED DARK + =========================== */ +[data-theme="solarized-dark"] { + --bg-primary: #002b36; + --bg-secondary: #073642; + --bg-tertiary: #094454; + --bg-elevated: #0e5261; + + --border-primary: #0e5261; + --border-secondary: #094454; + + --text-primary: #839496; + --text-secondary: #93a1a1; + --text-muted: #586e75; + + --accent-primary: #268bd2; + --accent-hover: #4098d9; + + --success: #859900; + --warning: #b58900; + --error: #dc322f; +} + +/* =========================== + THEME: NORD + =========================== */ +[data-theme="nord"] { + --bg-primary: #2e3440; + --bg-secondary: #3b4252; + --bg-tertiary: #434c5e; + --bg-elevated: #4c566a; + + --border-primary: #4c566a; + --border-secondary: #434c5e; + + --text-primary: #eceff4; + --text-secondary: #d8dee9; + --text-muted: #616e88; + + --accent-primary: #88c0d0; + --accent-hover: #9dcadb; + + --success: #a3be8c; + --warning: #ebcb8b; + --error: #bf616a; +} + +/* =========================== + THEME: CATPPUCCIN MOCHA + =========================== */ +[data-theme="catppuccin"] { + --bg-primary: #1e1e2e; + --bg-secondary: #181825; + --bg-tertiary: #313244; + --bg-elevated: #45475a; + + --border-primary: #45475a; + --border-secondary: #313244; + + --text-primary: #cdd6f4; + --text-secondary: #bac2de; + --text-muted: #6c7086; + + --accent-primary: #89b4fa; + --accent-hover: #a6c8ff; + + --success: #a6e3a1; + --warning: #f9e2af; + --error: #f38ba8; +} + +/* =========================== + THEME: EVERFOREST DARK + =========================== */ +[data-theme="everforest"] { + --bg-primary: #2d353b; + --bg-secondary: #272e33; + --bg-tertiary: #343f44; + --bg-elevated: #3d484d; + + --border-primary: #3d484d; + --border-secondary: #343f44; + + --text-primary: #d3c6aa; + --text-secondary: #b4a990; + --text-muted: #7a8478; + + --accent-primary: #7fbbb3; + --accent-hover: #93c9c1; + + --success: #a7c080; + --warning: #dbbc7f; + --error: #e67e80; +} + +/* =========================== + BOUTONS D'ACTION DE LA SIDEBAR + =========================== */ +.sidebar-action-btn { + width: 100%; + background: var(--bg-tertiary); + border: 1px solid var(--border-primary); + border-radius: var(--radius-md); + color: var(--text-primary); + padding: var(--spacing-md); + cursor: pointer; + transition: all var(--transition-fast); + display: flex; + align-items: center; + justify-content: center; + gap: var(--spacing-sm); + font-size: 0.9rem; + font-weight: 500; + margin-top: var(--spacing-sm); +} + +.sidebar-action-btn:hover { + background: var(--bg-elevated); + border-color: var(--accent-primary); + color: var(--accent-primary); + transform: translateY(-1px); + box-shadow: var(--shadow-sm); +} + +/* Style spécifique pour le bouton paramètres (avec animation) */ +.theme-settings-btn { + width: 100%; + background: var(--bg-tertiary); + border: 1px solid var(--border-primary); + border-radius: var(--radius-md); + color: var(--text-primary); + padding: var(--spacing-md); + cursor: pointer; + transition: all var(--transition-fast); + display: flex; + align-items: center; + justify-content: center; + gap: var(--spacing-sm); + font-size: 0.9rem; + font-weight: 500; + margin-top: var(--spacing-sm); +} + +.theme-settings-btn:hover { + background: var(--bg-elevated); + border-color: var(--accent-primary); + color: var(--accent-primary); + transform: translateY(-1px); + box-shadow: var(--shadow-sm); +} + +.theme-settings-btn svg { + animation: rotate 2s linear infinite; + animation-play-state: paused; +} + +.theme-settings-btn:hover svg { + animation-play-state: running; +} + +@keyframes rotate { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +/* =========================== + MODALE DE SÉLECTION DE THÈME + =========================== */ +#theme-modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 9999; + display: flex; + align-items: center; + justify-content: center; +} + +.theme-modal-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.7); + backdrop-filter: blur(4px); +} + +.theme-modal-content { + position: relative; + background: var(--bg-secondary); + border: 1px solid var(--accent-primary); + border-radius: var(--radius-lg); + padding: var(--spacing-xl); + max-width: 600px; + width: 90%; + max-height: 80vh; + overflow-y: auto; + box-shadow: var(--shadow-lg), var(--shadow-glow); + animation: slideUp 0.3s ease; + z-index: 1; +} + +.theme-modal-content h2 { + margin: 0 0 var(--spacing-lg) 0; + color: var(--text-primary); + font-size: 1.4rem; + display: flex; + align-items: center; + gap: var(--spacing-sm); +} + +.theme-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: var(--spacing-md); + margin-bottom: var(--spacing-lg); +} + +.theme-card { + background: var(--bg-tertiary); + border: 2px solid var(--border-primary); + border-radius: var(--radius-md); + padding: var(--spacing-md); + cursor: pointer; + transition: all var(--transition-fast); + position: relative; +} + +.theme-card:hover { + border-color: var(--accent-primary); + transform: translateY(-2px); + box-shadow: var(--shadow-md); +} + +.theme-card.active { + border-color: var(--accent-primary); + box-shadow: 0 0 0 3px rgba(66, 165, 245, 0.2); +} + +.theme-card.active::before { + content: '✓'; + position: absolute; + top: var(--spacing-xs); + right: var(--spacing-xs); + background: var(--accent-primary); + color: white; + width: 24px; + height: 24px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + font-size: 0.8rem; +} + +.theme-card-header { + display: flex; + align-items: center; + gap: var(--spacing-sm); + margin-bottom: var(--spacing-md); +} + +.theme-card-icon { + font-size: 1.5rem; +} + +.theme-card-name { + font-weight: 600; + color: var(--text-primary); + font-size: 1rem; +} + +.theme-preview { + display: flex; + gap: var(--spacing-xs); + margin-bottom: var(--spacing-sm); + height: 40px; + border-radius: var(--radius-sm); + overflow: hidden; +} + +.theme-preview-color { + flex: 1; + transition: all var(--transition-fast); +} + +.theme-card:hover .theme-preview-color { + transform: scaleY(1.1); +} + +.theme-description { + font-size: 0.85rem; + color: var(--text-secondary); + line-height: 1.4; +} + +/* Couleurs de prévisualisation pour chaque thème */ +.theme-card[data-theme="material-dark"] .theme-preview-color:nth-child(1) { background: #1e1e1e; } +.theme-card[data-theme="material-dark"] .theme-preview-color:nth-child(2) { background: #42a5f5; } +.theme-card[data-theme="material-dark"] .theme-preview-color:nth-child(3) { background: #29b6f6; } +.theme-card[data-theme="material-dark"] .theme-preview-color:nth-child(4) { background: #e0e0e0; } + +.theme-card[data-theme="monokai-dark"] .theme-preview-color:nth-child(1) { background: #272822; } +.theme-card[data-theme="monokai-dark"] .theme-preview-color:nth-child(2) { background: #66d9ef; } +.theme-card[data-theme="monokai-dark"] .theme-preview-color:nth-child(3) { background: #88c070; } +.theme-card[data-theme="monokai-dark"] .theme-preview-color:nth-child(4) { background: #f8f8f2; } + +.theme-card[data-theme="dracula"] .theme-preview-color:nth-child(1) { background: #282a36; } +.theme-card[data-theme="dracula"] .theme-preview-color:nth-child(2) { background: #8be9fd; } +.theme-card[data-theme="dracula"] .theme-preview-color:nth-child(3) { background: #bd93f9; } +.theme-card[data-theme="dracula"] .theme-preview-color:nth-child(4) { background: #f8f8f2; } + +.theme-card[data-theme="one-dark"] .theme-preview-color:nth-child(1) { background: #282c34; } +.theme-card[data-theme="one-dark"] .theme-preview-color:nth-child(2) { background: #61afef; } +.theme-card[data-theme="one-dark"] .theme-preview-color:nth-child(3) { background: #c678dd; } +.theme-card[data-theme="one-dark"] .theme-preview-color:nth-child(4) { background: #abb2bf; } + +.theme-card[data-theme="solarized-dark"] .theme-preview-color:nth-child(1) { background: #002b36; } +.theme-card[data-theme="solarized-dark"] .theme-preview-color:nth-child(2) { background: #268bd2; } +.theme-card[data-theme="solarized-dark"] .theme-preview-color:nth-child(3) { background: #2aa198; } +.theme-card[data-theme="solarized-dark"] .theme-preview-color:nth-child(4) { background: #839496; } + +.theme-card[data-theme="nord"] .theme-preview-color:nth-child(1) { background: #2e3440; } +.theme-card[data-theme="nord"] .theme-preview-color:nth-child(2) { background: #88c0d0; } +.theme-card[data-theme="nord"] .theme-preview-color:nth-child(3) { background: #81a1c1; } +.theme-card[data-theme="nord"] .theme-preview-color:nth-child(4) { background: #eceff4; } + +.theme-card[data-theme="catppuccin"] .theme-preview-color:nth-child(1) { background: #1e1e2e; } +.theme-card[data-theme="catppuccin"] .theme-preview-color:nth-child(2) { background: #89b4fa; } +.theme-card[data-theme="catppuccin"] .theme-preview-color:nth-child(3) { background: #f5c2e7; } +.theme-card[data-theme="catppuccin"] .theme-preview-color:nth-child(4) { background: #cdd6f4; } + +.theme-card[data-theme="everforest"] .theme-preview-color:nth-child(1) { background: #2d353b; } +.theme-card[data-theme="everforest"] .theme-preview-color:nth-child(2) { background: #7fbbb3; } +.theme-card[data-theme="everforest"] .theme-preview-color:nth-child(3) { background: #a7c080; } +.theme-card[data-theme="everforest"] .theme-preview-color:nth-child(4) { background: #d3c6aa; } + +.theme-modal-footer { + display: flex; + justify-content: flex-end; + gap: var(--spacing-sm); + margin-top: var(--spacing-lg); + padding-top: var(--spacing-md); + border-top: 1px solid var(--border-primary); +} + +/* Onglets de paramètres */ +.settings-tabs { + display: flex; + gap: var(--spacing-sm); + margin-bottom: var(--spacing-lg); + border-bottom: 2px solid var(--border-primary); +} + +.settings-tab { + background: transparent; + border: none; + padding: var(--spacing-sm) var(--spacing-md); + color: var(--text-secondary); + font-size: 0.95rem; + font-weight: 500; + cursor: pointer; + transition: all var(--transition-fast); + border-bottom: 2px solid transparent; + margin-bottom: -2px; +} + +.settings-tab:hover { + color: var(--text-primary); + background: var(--bg-tertiary); +} + +.settings-tab.active { + color: var(--accent-primary); + border-bottom-color: var(--accent-primary); +} + +.settings-section { + animation: fadeIn 0.3s ease; +} + +/* Grille de polices */ +.font-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: var(--spacing-md); + margin-bottom: var(--spacing-lg); +} + +.font-card { + background: var(--bg-tertiary); + border: 2px solid var(--border-primary); + border-radius: var(--radius-md); + padding: var(--spacing-md); + cursor: pointer; + transition: all var(--transition-fast); + position: relative; +} + +.font-card:hover { + border-color: var(--accent-primary); + transform: translateY(-2px); + box-shadow: var(--shadow-md); +} + +.font-card.active { + border-color: var(--accent-primary); + box-shadow: 0 0 0 3px rgba(66, 165, 245, 0.2); +} + +.font-card.active::before { + content: '✓'; + position: absolute; + top: var(--spacing-xs); + right: var(--spacing-xs); + background: var(--accent-primary); + color: white; + width: 24px; + height: 24px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + font-size: 0.8rem; +} + +.font-card-header { + display: flex; + align-items: center; + gap: var(--spacing-sm); + margin-bottom: var(--spacing-md); +} + +.font-card-icon { + font-size: 1.3rem; +} + +.font-card-name { + font-weight: 600; + color: var(--text-primary); + font-size: 0.95rem; +} + +.font-preview { + background: var(--bg-primary); + border: 1px solid var(--border-primary); + border-radius: var(--radius-sm); + padding: var(--spacing-md); + margin-bottom: var(--spacing-sm); + font-size: 1.1rem; + text-align: center; + color: var(--text-primary); + min-height: 50px; + display: flex; + align-items: center; + justify-content: center; +} + +.font-description { + font-size: 0.8rem; + color: var(--text-secondary); + line-height: 1.4; +} + +/* Sélecteur de taille de police */ +.font-size-selector { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); + gap: var(--spacing-sm); +} + +.font-size-option { + background: var(--bg-tertiary); + border: 2px solid var(--border-primary); + border-radius: var(--radius-md); + padding: var(--spacing-md); + cursor: pointer; + transition: all var(--transition-fast); + display: flex; + flex-direction: column; + align-items: center; + gap: var(--spacing-xs); +} + +.font-size-option:hover { + border-color: var(--accent-primary); + transform: translateY(-2px); + box-shadow: var(--shadow-sm); +} + +.font-size-option.active { + border-color: var(--accent-primary); + background: rgba(66, 165, 245, 0.1); + box-shadow: 0 0 0 3px rgba(66, 165, 245, 0.2); +} + +.size-label { + font-size: 0.85rem; + color: var(--text-secondary); + font-weight: 500; +} + +.font-size-option.active .size-label { + color: var(--accent-primary); +} + +.size-preview { + font-weight: 600; + color: var(--text-primary); + line-height: 1; +} + +/* =========================== + TOGGLE SWITCH (pour Mode Vim) + =========================== */ +.toggle-switch { + position: relative; + display: inline-block; + width: 50px; + height: 26px; + flex-shrink: 0; +} + +.toggle-switch input { + opacity: 0; + width: 0; + height: 0; +} + +.toggle-slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: var(--bg-tertiary); + transition: 0.3s; + border-radius: 26px; + border: 1px solid var(--border-primary); +} + +.toggle-slider:before { + position: absolute; + content: ""; + height: 18px; + width: 18px; + left: 4px; + bottom: 3px; + background-color: var(--text-muted); + transition: 0.3s; + border-radius: 50%; +} + +.toggle-switch input:checked + .toggle-slider { + background-color: var(--accent-primary); + border-color: var(--accent-primary); +} + +.toggle-switch input:checked + .toggle-slider:before { + transform: translateX(24px); + background-color: white; +} + +.toggle-switch input:focus + .toggle-slider { + box-shadow: 0 0 0 3px rgba(66, 165, 245, 0.2); +} diff --git a/static/theme.css b/static/theme.css index 811d3f5..aa0e521 100644 --- a/static/theme.css +++ b/static/theme.css @@ -17,35 +17,33 @@ --text-secondary: #b0b0b0; --text-muted: #6e6e6e; - /* Accent colors - Blue focused */ + /* Accent color - Single blue accent for consistency */ --accent-primary: #42a5f5; - --accent-primary-hover: #64b5f6; - --accent-secondary: #29b6f6; - --accent-secondary-hover: #4fc3f7; + --accent-hover: #64b5f6; /* Semantic colors */ --success: #66bb6a; --warning: #ffa726; --error: #ef5350; - /* Spacing */ - --spacing-xs: 0.25rem; - --spacing-sm: 0.5rem; - --spacing-md: 1rem; - --spacing-lg: 1.5rem; - --spacing-xl: 2rem; - - /* Sidebar compact spacing */ - --sidebar-item-gap: 0.05rem; - --sidebar-padding-v: 0.3rem; - --sidebar-padding-h: 0.75rem; - --sidebar-indent: 1rem; + /* Spacing - 8px base unit system */ + --spacing-xs: 0.5rem; /* 8px - 1 unit */ + --spacing-sm: 1rem; /* 16px - 2 units */ + --spacing-md: 1.5rem; /* 24px - 3 units */ + --spacing-lg: 2rem; /* 32px - 4 units */ + --spacing-xl: 3rem; /* 48px - 6 units */ - /* Shadows */ - --shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.4); - --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -2px rgba(0, 0, 0, 0.3); - --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.4), 0 4px 6px -4px rgba(0, 0, 0, 0.3); - --shadow-glow: 0 0 20px rgba(66, 165, 245, 0.2); + /* Sidebar spacing - aligned to 8px grid */ + --sidebar-item-gap: 0.125rem; /* 2px - minimal spacing between items */ + --sidebar-padding-v: 0.25rem; /* 4px - compact vertical padding */ + --sidebar-padding-h: 1rem; /* 16px */ + --sidebar-indent: 1.5rem; /* 24px */ + + /* Shadows - reduced opacity for minimal look */ + --shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.1); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.08); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.12), 0 4px 6px -4px rgba(0, 0, 0, 0.1); + --shadow-glow: 0 0 20px rgba(66, 165, 245, 0.1); /* Border radius */ --radius-sm: 4px; @@ -55,8 +53,59 @@ /* Transitions */ --transition-fast: 150ms ease; --transition-normal: 250ms ease; + + /* Typography scale - consistent font sizes */ + --text-xs: 0.75rem; /* 12px */ + --text-sm: 0.875rem; /* 14px */ + --text-base: 1rem; /* 16px - default */ + --text-md: 1.125rem; /* 18px */ + --text-lg: 1.25rem; /* 20px */ + --text-xl: 1.5rem; /* 24px */ + --text-2xl: 2rem; /* 32px */ + --text-3xl: 3rem; /* 48px */ + + /* Touch targets - minimum sizes for accessibility */ + --touch-sm: 2rem; /* 32px - compact */ + --touch-md: 2.75rem; /* 44px - standard mobile minimum */ + --touch-lg: 3rem; /* 48px - comfortable */ + + /* Lucide Icons - Professional SVG icons */ + --icon-xs: 14px; + --icon-sm: 16px; + --icon-md: 20px; + --icon-lg: 24px; + --icon-xl: 32px; } +/* Lucide Icons Styling */ +.lucide { + display: inline-block; + vertical-align: middle; + stroke-width: 2; + stroke: currentColor; + fill: none; +} + +/* Icon size variants */ +.icon-xs { width: var(--icon-xs); height: var(--icon-xs); } +.icon-sm { width: var(--icon-sm); height: var(--icon-sm); } +.icon-md { width: var(--icon-md); height: var(--icon-md); } +.icon-lg { width: var(--icon-lg); height: var(--icon-lg); } +.icon-xl { width: var(--icon-xl); height: var(--icon-xl); } + +/* Icon with text alignment */ +.icon-text { + display: inline-flex; + align-items: center; + gap: 0.5rem; +} + +/* Icon color variants */ +.icon-primary { color: var(--text-primary); } +.icon-secondary { color: var(--text-secondary); } +.icon-muted { color: var(--text-muted); } +.icon-accent { color: var(--accent-primary); } + /* Base styles */ html { font-size: 16px; @@ -90,7 +139,7 @@ header h1 { font-size: 1.25rem; font-weight: 600; color: var(--text-primary); - background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); + background: var(--accent-primary); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; @@ -257,25 +306,7 @@ aside hr { margin: var(--spacing-sm) 0; } -/* File tree and search results */ -#file-tree a, -#search-results a { - display: block; - padding: var(--spacing-sm) var(--spacing-md); - color: var(--text-primary); - text-decoration: none; - border-radius: var(--radius-sm); - transition: all var(--transition-fast); - font-size: 0.9rem; - margin-bottom: var(--spacing-xs); -} - -#file-tree a:hover, -#search-results a:hover { - background: var(--bg-tertiary); - color: var(--accent-primary); - transform: translateX(2px); -} +/* File tree and search results - styles now handled by .file-item class */ /* Search results header */ .search-results-header { @@ -411,7 +442,7 @@ aside hr { } .search-no-results-text strong { - color: var(--accent-secondary); + color: var(--accent-primary); } .search-no-results-hint { @@ -455,7 +486,7 @@ aside hr { .search-help-example code { background: var(--bg-primary); - color: var(--accent-secondary); + color: var(--accent-primary); padding: 0.2rem 0.4rem; border-radius: var(--radius-sm); font-size: 0.8rem; @@ -720,7 +751,7 @@ main::-webkit-scrollbar-thumb:hover { .preview h2 { font-size: 1.5em; - color: var(--accent-secondary); + color: var(--accent-primary); margin-bottom: 0.9em; } @@ -771,7 +802,7 @@ main::-webkit-scrollbar-thumb:hover { } .preview ol > li::marker { - color: var(--accent-secondary); + color: var(--accent-primary); font-weight: 600; } @@ -797,7 +828,7 @@ main::-webkit-scrollbar-thumb:hover { .preview a:hover { text-decoration: underline; - color: var(--accent-primary-hover); + color: var(--accent-hover); } .preview strong, .preview b { @@ -815,7 +846,7 @@ main::-webkit-scrollbar-thumb:hover { padding: 0.2em 0.4em; border-radius: var(--radius-sm); font-size: 0.85em; - color: var(--accent-secondary); + color: var(--accent-primary); font-weight: 500; } @@ -853,7 +884,7 @@ main::-webkit-scrollbar-thumb:hover { } .preview table thead { - background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); + background: var(--accent-primary); } .preview table thead th { @@ -895,7 +926,7 @@ main::-webkit-scrollbar-thumb:hover { button, [type="submit"], [type="button"] { - background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); + background: var(--accent-primary); color: white; border: none; border-radius: var(--radius-md); @@ -984,7 +1015,7 @@ button.secondary:hover { } #slash-commands-palette li[style*="background-color"] { - background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)) !important; + background: var(--accent-primary) !important; color: white !important; font-weight: 500; transform: translateX(2px); @@ -1016,7 +1047,7 @@ progress::-webkit-progress-bar { } progress::-webkit-progress-value { - background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary)); + background: var(--accent-primary); border-radius: var(--radius-sm); } @@ -1236,39 +1267,26 @@ body, html { } .folder-item.drag-over .folder-header { - background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); + background: var(--accent-primary); color: white; box-shadow: var(--shadow-glow); border: 2px solid var(--accent-primary); border-radius: var(--radius-md); - animation: pulse 1s ease-in-out infinite; -} - -@keyframes pulse { - 0%, 100% { - transform: scale(1); - box-shadow: var(--shadow-glow); - } - 50% { - transform: scale(1.02); - box-shadow: 0 0 30px rgba(88, 166, 255, 0.4); - } } .file-item.drag-over { - background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); + background: var(--accent-primary); color: white; box-shadow: var(--shadow-glow); } /* Style pour la racine en drag-over */ .sidebar-section-header.drag-over { - background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)) !important; + background: var(--accent-primary) !important; color: white !important; box-shadow: var(--shadow-glow); border: 2px solid var(--accent-primary); border-radius: var(--radius-md); - animation: pulse 1s ease-in-out infinite; } .sidebar-section-header.drag-over .folder-name, @@ -1401,11 +1419,10 @@ body, html { /* Quand on drag au-dessus de la racine */ .root-drop-zone.drag-over .root-folder-header { - background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); + background: var(--accent-primary); color: white; border-color: var(--accent-primary); box-shadow: var(--shadow-glow); - animation: pulse 1s ease-in-out infinite; } .root-drop-zone.drag-over .root-folder-header .folder-name, @@ -2053,7 +2070,7 @@ body, html { } .search-modal-result-item.selected { - background: linear-gradient(135deg, rgba(130, 170, 255, 0.15), rgba(199, 146, 234, 0.15)); + background: var(--bg-secondary); border-color: var(--accent-primary); } @@ -2074,7 +2091,7 @@ body, html { } .search-modal-result-title mark { - background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); + background: var(--accent-primary); color: white; padding: 2px 4px; border-radius: 3px; @@ -2200,11 +2217,6 @@ body, html { border: 3px solid var(--border-primary); border-top-color: var(--accent-primary); border-radius: 50%; - animation: spin 1s linear infinite; -} - -@keyframes spin { - to { transform: rotate(360deg); } } .search-modal-loading p { @@ -2566,12 +2578,12 @@ body, html { /* Today */ .calendar-day-today { - border-color: var(--accent-secondary); - background: linear-gradient(135deg, rgba(130, 170, 255, 0.1), rgba(199, 146, 234, 0.1)); + border-color: var(--accent-primary); + background: var(--bg-secondary); } .calendar-day-today .calendar-day-number { - color: var(--accent-secondary); + color: var(--accent-primary); font-weight: 700; } @@ -2596,7 +2608,7 @@ body, html { width: 100%; margin-top: var(--spacing-sm); padding: var(--spacing-sm); - background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); + background: var(--accent-primary); color: white; border: none; border-radius: var(--radius-md); @@ -2681,11 +2693,12 @@ body, html { display: flex; align-items: center; padding: 0; - margin: var(--sidebar-item-gap) 0; + margin: 0; /* No margin on wrapper - use same spacing as folders */ } .file-item-wrapper .file-item { flex: 1; + margin: 0 !important; /* Force remove margin to avoid double spacing */ } /* Bouton de mode sélection */ @@ -2715,8 +2728,8 @@ body, html { } .icon-button.active:hover { - background: var(--accent-primary-hover); - border-color: var(--accent-primary-hover); + background: var(--accent-hover); + border-color: var(--accent-hover); } /* Toolbar de sélection flottante */ @@ -2784,7 +2797,7 @@ body, html { background: #ff5370; border-color: #ff5370; transform: translateY(-1px); - box-shadow: 0 4px 12px rgba(240, 113, 120, 0.4); + box-shadow: 0 4px 12px rgba(240, 113, 120, 0.15); } .danger-button:active { @@ -2974,30 +2987,38 @@ body, html { /* Bouton d'ajout aux favoris (dans le file tree) */ .add-to-favorites { - opacity: 0; + opacity: 0.4; /* Always slightly visible */ background: transparent; border: none; color: var(--text-muted); cursor: pointer; - font-size: 0.9rem; - padding: 0 0.2rem; + font-size: 1rem; + padding: 0.25rem; transition: all var(--transition-fast); margin-left: auto; + border-radius: var(--radius-sm); } .folder-header:hover .add-to-favorites, .file-item:hover .add-to-favorites { opacity: 1; + background: var(--bg-tertiary); } .add-to-favorites:hover { color: var(--warning); - transform: scale(1.2); + transform: scale(1.15); + background: rgba(255, 193, 7, 0.1); /* Subtle yellow background */ } .add-to-favorites.is-favorite { - opacity: 1; + opacity: 1 !important; /* Always fully visible when favorited */ color: var(--warning); + background: rgba(255, 193, 7, 0.15); +} + +.add-to-favorites.is-favorite:hover { + background: rgba(255, 193, 7, 0.2); } /* Responsive - Hauteurs adaptatives pour #favorites-list */ @@ -3162,7 +3183,7 @@ body, html { } .link-inserter-result-item.selected { - background: linear-gradient(135deg, rgba(130, 170, 255, 0.15), rgba(199, 146, 234, 0.15)); + background: var(--bg-secondary); border-color: var(--accent-primary); } @@ -3184,7 +3205,7 @@ body, html { } .link-inserter-result-title mark { - background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); + background: var(--accent-primary); color: white; padding: 2px 4px; border-radius: 3px; @@ -3241,7 +3262,6 @@ body, html { border: 3px solid var(--bg-tertiary); border-top-color: var(--accent-primary); border-radius: 50%; - animation: spin 0.8s linear infinite; } .link-inserter-loading p { @@ -3575,7 +3595,7 @@ body, html { .breadcrumb-link:hover { background: var(--bg-tertiary); - color: var(--accent-secondary); + color: var(--accent-primary); } .breadcrumb-separator { @@ -3624,3 +3644,20 @@ body, html { color: var(--accent-primary); } + + +/* Public toggle button */ +#toggle-public-btn { + transition: all 0.3s ease; +} + +#toggle-public-btn.public-active { + background: var(--accent-green, #2ecc71); + color: white; + border-color: var(--accent-green, #2ecc71); +} + +#toggle-public-btn.public-active:hover { + background: var(--accent-green-dark, #27ae60); + border-color: var(--accent-green-dark, #27ae60); +} diff --git a/static/themes.css b/static/themes.css index 344907d..1b64352 100644 --- a/static/themes.css +++ b/static/themes.css @@ -21,9 +21,7 @@ --text-muted: #6e6e6e; --accent-primary: #42a5f5; - --accent-primary-hover: #5ab3f7; - --accent-secondary: #29b6f6; - --accent-secondary-hover: #4fc3f7; + --accent-hover: #5ab3f7; --success: #66bb6a; --warning: #ffa726; @@ -47,9 +45,7 @@ --text-muted: #75715e; --accent-primary: #66d9ef; - --accent-primary-hover: #7ee5f7; - --accent-secondary: #88c070; - --accent-secondary-hover: #9acc84; + --accent-hover: #7ee5f7; --success: #88c070; --warning: #e6db74; @@ -73,9 +69,7 @@ --text-muted: #6272a4; --accent-primary: #8be9fd; - --accent-primary-hover: #9ff3ff; - --accent-secondary: #bd93f9; - --accent-secondary-hover: #cba6ff; + --accent-hover: #9ff3ff; --success: #50fa7b; --warning: #f1fa8c; @@ -99,9 +93,7 @@ --text-muted: #5c6370; --accent-primary: #61afef; - --accent-primary-hover: #75bdf5; - --accent-secondary: #c678dd; - --accent-secondary-hover: #d48ae9; + --accent-hover: #75bdf5; --success: #98c379; --warning: #e5c07b; @@ -125,9 +117,7 @@ --text-muted: #586e75; --accent-primary: #268bd2; - --accent-primary-hover: #4098d9; - --accent-secondary: #2aa198; - --accent-secondary-hover: #3eb3a8; + --accent-hover: #4098d9; --success: #859900; --warning: #b58900; @@ -151,9 +141,7 @@ --text-muted: #616e88; --accent-primary: #88c0d0; - --accent-primary-hover: #9dcadb; - --accent-secondary: #81a1c1; - --accent-secondary-hover: #94b0cc; + --accent-hover: #9dcadb; --success: #a3be8c; --warning: #ebcb8b; @@ -177,9 +165,7 @@ --text-muted: #6c7086; --accent-primary: #89b4fa; - --accent-primary-hover: #a6c8ff; - --accent-secondary: #f5c2e7; - --accent-secondary-hover: #f9d5ee; + --accent-hover: #a6c8ff; --success: #a6e3a1; --warning: #f9e2af; @@ -203,9 +189,7 @@ --text-muted: #7a8478; --accent-primary: #7fbbb3; - --accent-primary-hover: #93c9c1; - --accent-secondary: #a7c080; - --accent-secondary-hover: #b8cc94; + --accent-hover: #93c9c1; --success: #a7c080; --warning: #dbbc7f; diff --git a/templates/about.html b/templates/about.html index 1daecc1..38d4c5d 100644 --- a/templates/about.html +++ b/templates/about.html @@ -1,7 +1,7 @@

- 📝 About PersoNotes + About PersoNotes

Un gestionnaire de notes Markdown moderne et puissant @@ -10,23 +10,23 @@

- 🚀 Démarrage rapide + Démarrage rapide

-

📁 Parcourir

+

Parcourir

Explorez vos notes dans l'arborescence à gauche

-

🔍 Rechercher

+

Rechercher

Utilisez la barre de recherche en haut pour trouver vos notes

-

⚡ Slash commands

+

Slash commands

Tapez / dans l'éditeur pour insérer du Markdown

@@ -36,7 +36,7 @@

- ✨ Fonctionnalités + Fonctionnalités

  • @@ -62,13 +62,13 @@

    - 💡 Astuce : Cliquez sur une note dans l'arborescence pour commencer à éditer + Astuce : Cliquez sur une note dans l'arborescence pour commencer à éditer

    - ⌨️ Raccourcis clavier + Raccourcis clavier

    @@ -113,7 +113,7 @@

    - 💡 Sur Mac, utilisez Cmd au lieu de Ctrl + Sur Mac, utilisez Cmd au lieu de Ctrl

\ No newline at end of file diff --git a/templates/daily-calendar.html b/templates/daily-calendar.html index 4e71df2..1571b88 100644 --- a/templates/daily-calendar.html +++ b/templates/daily-calendar.html @@ -57,7 +57,7 @@ data-i18n="calendar.today" title="Ouvrir la note du jour (Ctrl/Cmd+D)" style="flex: 1; padding: 0.5rem; font-size: 0.85rem;"> - 📅 Aujourd'hui + Aujourd'hui
diff --git a/templates/editor.html b/templates/editor.html index 8999b00..d0f060f 100644 --- a/templates/editor.html +++ b/templates/editor.html @@ -19,7 +19,7 @@ {{if .IsHome}} {{else}}
{{if .Backlinks}}