Change d'interface plus légére, modification side barre

This commit is contained in:
2025-12-24 16:14:17 +01:00
parent cc1d6880a7
commit 917a31d5a8
46 changed files with 7484 additions and 298 deletions

View File

@ -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": []

377
API.md
View File

@ -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
<VirtualHost *:80>
ServerName notes.example.com
DocumentRoot /var/www/html/notes
</VirtualHost>
```
#### 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)
---

155
CLAUDE.md
View File

@ -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

360
EXPORT_GUIDE.md Normal file
View File

@ -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
<VirtualHost *:80>
ServerName notes.example.com
DocumentRoot /var/www/html/notes
<Directory /var/www/html/notes>
Options Indexes FollowSymLinks
AllowOverride All
Require all granted
</Directory>
</VirtualHost>
```
### 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
<head>
<!-- Meta tags SEO -->
<meta name="description" content="%s">
<meta property="og:title" content="%s">
<meta property="og:type" content="article">
<!-- Votre titre existant -->
<title>%s - PersoNotes</title>
```
### Ajouter Google Analytics
Ajoutez dans `generateStandaloneHTML()` avant `</head>` :
```html
<!-- Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'GA_MEASUREMENT_ID');
</script>
```
## 🐛 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.

188
PUBLIC_NOTES.md Normal file
View File

@ -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
<meta property="og:title" content="{{.Title}}">
<meta property="og:type" content="article">
<meta property="og:url" content="https://votre-domaine.com/public/view/{{.Path}}">
<meta property="og:description" content="Note publique partagée">
```
## 🚀 É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`

119
QUICK_START_PUBLIC.md Normal file
View File

@ -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
<VirtualHost *:80>
ServerName notes.example.com
DocumentRoot /var/www/html/notes
</VirtualHost>
```
### 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.

View File

@ -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()

View File

@ -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 = '<i data-lucide="star" class="icon-sm"></i>';
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 = '<i data-lucide="star" class="icon-sm"></i>';
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();
}
});
}
}

View File

@ -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 = '<i data-lucide="folder-open" class="icon-sm"></i>';
this.saveFolderState(folderPath, true);
} else {
// Fermer le dossier
children.style.display = 'none';
toggle.classList.remove('expanded');
icon.textContent = '📁';
icon.innerHTML = '<i data-lucide="folder" class="icon-sm"></i>';
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 = '<i data-lucide="folder-open" class="icon-sm"></i>';
}
}
});
// 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 ? '📁' : '📄'} <code>${path}</code>`;
li.innerHTML = `${isDir ? '<i data-lucide="folder" class="icon-sm"></i>' : '<i data-lucide="file-text" class="icon-sm"></i>'} <code>${path}</code>`;
ul.appendChild(li);
});

View File

@ -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(() => {

View File

@ -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 = `<i data-lucide="home" class="icon-sm"></i> ${t('menu.home')}`;
}
const newNoteButton = document.querySelector('header button[onclick="showNewNoteModal()"]');
if (newNoteButton && !newNoteButton.hasAttribute('data-i18n')) {
newNoteButton.innerHTML = ` ${t('menu.newNote')}`;
newNoteButton.innerHTML = `<i data-lucide="file-plus" class="icon-sm"></i> ${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 = `<i data-lucide="file-text" class="icon-sm"></i> ${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 = `<i data-lucide="folder-plus" class="icon-sm"></i> ${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 = `<i data-lucide="settings" class="icon-sm"></i> ${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 = `<i data-lucide="palette" class="icon-sm"></i> ${t('tabs.themes')}`;
tabs[1].innerHTML = `<i data-lucide="type" class="icon-sm"></i> ${t('tabs.fonts')}`;
tabs[2].innerHTML = `<i data-lucide="keyboard" class="icon-sm"></i> ${t('tabs.shortcuts')}`;
tabs[3].innerHTML = `<i data-lucide="settings" class="icon-sm"></i> ${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 = `<i data-lucide="languages" class="icon-sm"></i> ${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 = `<i data-lucide="search" class="icon-sm"></i> ${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 = `<i data-lucide="folder-plus" class="icon-sm"></i> ${t('fileTree.newFolder')}`;
}
// Sidebar "Paramètres" button span

View File

@ -309,7 +309,7 @@ class LinkInserter {
this.results = [];
this.resultsContainer.innerHTML = `
<div class="link-inserter-no-results">
<div class="link-inserter-no-results-icon">🔍</div>
<div class="link-inserter-no-results-icon"><i data-lucide="search" class="icon-lg"></i></div>
<p>Aucune note trouvée pour « <strong>${this.escapeHtml(query)}</strong> »</p>
</div>
`;
@ -318,7 +318,7 @@ class LinkInserter {
showError() {
this.resultsContainer.innerHTML = `
<div class="link-inserter-error">
<div class="link-inserter-error-icon">⚠️</div>
<div class="link-inserter-error-icon"><i data-lucide="alert-triangle" class="icon-lg"></i></div>
<p>Erreur lors de la recherche</p>
</div>
`;

View File

@ -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();

View File

@ -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 = `<span data-i18n="${textKey}">${isNowPublic ? '🌐 ' + t('public.buttonPublic') : '🔒 ' + t('public.buttonPrivate')}</span>`;
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);
}

View File

@ -51,7 +51,7 @@ class SearchModal {
<div class="search-modal-body">
<div class="search-modal-results">
<div class="search-modal-help">
<div class="search-modal-help-title">💡 Recherche avancée</div>
<div class="search-modal-help-title"><i data-lucide="lightbulb" class="icon-sm"></i> Recherche avancée</div>
<div class="search-modal-help-items">
<div class="search-modal-help-item">
<code>tag:projet</code>
@ -289,7 +289,7 @@ class SearchModal {
this.results = [];
this.resultsContainer.innerHTML = `
<div class="search-modal-help">
<div class="search-modal-help-title">💡 Recherche avancée</div>
<div class="search-modal-help-title"><i data-lucide="lightbulb" class="icon-sm"></i> Recherche avancée</div>
<div class="search-modal-help-items">
<div class="search-modal-help-item">
<code>tag:projet</code>
@ -325,7 +325,7 @@ class SearchModal {
this.results = [];
this.resultsContainer.innerHTML = `
<div class="search-modal-no-results">
<div class="search-modal-no-results-icon">🔍</div>
<div class="search-modal-no-results-icon"><i data-lucide="search" class="icon-lg"></i></div>
<p class="search-modal-no-results-text">Aucun résultat pour « <strong>${this.escapeHtml(query)}</strong> »</p>
<p class="search-modal-no-results-hint">Essayez d'autres mots-clés ou utilisez les filtres</p>
</div>
@ -335,7 +335,7 @@ class SearchModal {
showError() {
this.resultsContainer.innerHTML = `
<div class="search-modal-error">
<div class="search-modal-error-icon">⚠️</div>
<div class="search-modal-error-icon"><i data-lucide="alert-triangle" class="icon-lg"></i></div>
<p>Une erreur s'est produite lors de la recherche</p>
</div>
`;

View File

@ -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'
}
];

1
go.mod
View File

@ -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
)

2
go.sum
View File

@ -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=

View File

@ -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("<div class=\"home-section\">\n")
sb.WriteString(" <div class=\"home-section-header\" onclick=\"toggleFolder('tags')\">\n")
sb.WriteString(" <h2 class=\"home-section-title\">🏷️ Tags</h2>\n")
sb.WriteString(" <h2 class=\"home-section-title\"><i data-lucide=\"tags\" class=\"icon-sm\"></i> Tags</h2>\n")
sb.WriteString(" </div>\n")
sb.WriteString(" <div class=\"home-section-content\" id=\"folder-tags\">\n")
sb.WriteString(" <div class=\"tags-cloud\">\n")
@ -408,7 +423,7 @@ func (h *Handler) generateFavoritesSection(sb *strings.Builder, r *http.Request)
sb.WriteString("<div class=\"home-section\">\n")
sb.WriteString(" <div class=\"home-section-header\" onclick=\"toggleFolder('favorites')\">\n")
sb.WriteString(" <h2 class=\"home-section-title\"> " + h.t(r, "favorites.title") + "</h2>\n")
sb.WriteString(" <h2 class=\"home-section-title\"><i data-lucide=\"star\" class=\"icon-sm\"></i> " + h.t(r, "favorites.title") + "</h2>\n")
sb.WriteString(" </div>\n")
sb.WriteString(" <div class=\"home-section-content\" id=\"folder-favorites\">\n")
sb.WriteString(" <div class=\"note-tree favorites-tree\">\n")
@ -420,7 +435,7 @@ func (h *Handler) generateFavoritesSection(sb *strings.Builder, r *http.Request)
// Dossier - avec accordéon
sb.WriteString(fmt.Sprintf(" <div class=\"folder indent-level-1\">\n"))
sb.WriteString(fmt.Sprintf(" <div class=\"folder-header\" onclick=\"toggleFolder('%s')\">\n", safeID))
sb.WriteString(fmt.Sprintf(" <span class=\"folder-icon\" id=\"icon-%s\">📁</span>\n", safeID))
sb.WriteString(fmt.Sprintf(" <span class=\"folder-icon\" id=\"icon-%s\"><i data-lucide=\"folder\" class=\"icon-sm\"></i></span>\n", safeID))
sb.WriteString(fmt.Sprintf(" <strong>%s</strong>\n", fav.Title))
sb.WriteString(fmt.Sprintf(" </div>\n"))
sb.WriteString(fmt.Sprintf(" <div class=\"folder-content\" id=\"folder-%s\">\n", safeID))
@ -445,6 +460,44 @@ func (h *Handler) generateFavoritesSection(sb *strings.Builder, r *http.Request)
sb.WriteString("</div>\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("<div class=\"home-section\">\n")
sb.WriteString(" <div class=\"home-section-header\" onclick=\"toggleFolder('public-notes')\">\n")
sb.WriteString(fmt.Sprintf(" <h2 class=\"home-section-title\"><i data-lucide=\"globe\" class=\"icon-sm\"></i> %s (%d)</h2>\n", h.t(r, "publicNotes.title"), len(publicNotes.Notes)))
sb.WriteString(" </div>\n")
sb.WriteString(" <div class=\"home-section-content\" id=\"folder-public-notes\">\n")
sb.WriteString(" <div class=\"public-notes-list\">\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(" <div class=\"public-note-card\">\n")
sb.WriteString(fmt.Sprintf(" <div class=\"public-note-header\">\n"))
sb.WriteString(fmt.Sprintf(" <a href=\"#\" class=\"public-note-edit\" hx-get=\"/api/notes/%s\" hx-target=\"#editor-container\" hx-swap=\"innerHTML\" hx-push-url=\"true\" title=\"%s\">", note.Path, h.t(r, "publicNotes.editNote")))
sb.WriteString(fmt.Sprintf(" <span class=\"public-note-title\">%s</span>", note.Title))
sb.WriteString(" </a>\n")
sb.WriteString(fmt.Sprintf(" <a href=\"%s\" target=\"_blank\" class=\"public-note-view\" title=\"%s\">🌐</a>\n", publicURL, h.t(r, "publicNotes.viewPublic")))
sb.WriteString(" </div>\n")
sb.WriteString(fmt.Sprintf(" <div class=\"public-note-meta\">\n"))
sb.WriteString(fmt.Sprintf(" <span class=\"public-note-source\">📄 %s</span>\n", note.Path))
sb.WriteString(fmt.Sprintf(" <span class=\"public-note-date\"><i data-lucide=\"calendar\" class=\"icon-sm\"></i> %s</span>\n", note.PublishedAt.Format("02/01/2006")))
sb.WriteString(" </div>\n")
sb.WriteString(" </div>\n")
}
sb.WriteString(" </div>\n")
sb.WriteString(" </div>\n")
sb.WriteString("</div>\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(" <a href=\"#\" class=\"recent-note-link\" hx-get=\"/api/notes/%s\" hx-target=\"#editor-container\" hx-swap=\"innerHTML\" hx-push-url=\"true\">\n", doc.Path))
sb.WriteString(fmt.Sprintf(" <div class=\"recent-note-title\">%s</div>\n", doc.Title))
sb.WriteString(fmt.Sprintf(" <div class=\"recent-note-meta\">\n"))
sb.WriteString(fmt.Sprintf(" <span class=\"recent-note-date\">📅 %s</span>\n", dateStr))
sb.WriteString(fmt.Sprintf(" <span class=\"recent-note-date\"><i data-lucide=\"calendar\" class=\"icon-sm\"></i> %s</span>\n", dateStr))
if len(doc.Tags) > 0 {
sb.WriteString(fmt.Sprintf(" <span class=\"recent-note-tags\">"))
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<div class=\"folder %s\">\n", indent, indentClass))
sb.WriteString(fmt.Sprintf("%s <div class=\"folder-header\" onclick=\"toggleFolder('%s')\">\n", indent, safeID))
sb.WriteString(fmt.Sprintf("%s <span class=\"folder-icon\" id=\"icon-%s\">📁</span>\n", indent, safeID))
sb.WriteString(fmt.Sprintf("%s <span class=\"folder-icon\" id=\"icon-%s\"><i data-lucide=\"folder\" class=\"icon-sm\"></i></span>\n", indent, safeID))
sb.WriteString(fmt.Sprintf("%s <strong>%s</strong>\n", indent, name))
sb.WriteString(fmt.Sprintf("%s </div>\n", indent))
sb.WriteString(fmt.Sprintf("%s <div class=\"folder-content\" id=\"folder-%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<div class=\"folder %s\">\n", indent, indentClass))
sb.WriteString(fmt.Sprintf("%s <div class=\"folder-header\" onclick=\"toggleFolder('%s')\">\n", indent, safeID))
sb.WriteString(fmt.Sprintf("%s <span class=\"folder-icon\" id=\"icon-%s\">📁</span>\n", indent, safeID))
sb.WriteString(fmt.Sprintf("%s <span class=\"folder-icon\" id=\"icon-%s\"><i data-lucide=\"folder\" class=\"icon-sm\"></i></span>\n", indent, safeID))
sb.WriteString(fmt.Sprintf("%s <strong>%s</strong>\n", indent, node.Name))
sb.WriteString(fmt.Sprintf("%s </div>\n", indent))
sb.WriteString(fmt.Sprintf("%s <div class=\"folder-content\" id=\"folder-%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(`<strong>📁 Racine</strong>`)
return template.HTML(`<strong><i data-lucide="folder" class="icon-sm"></i> Racine</strong>`)
}
parts := strings.Split(filepath.ToSlash(path), "/")
@ -1379,7 +1438,7 @@ func (h *Handler) generateBreadcrumb(path string) template.HTML {
sb.WriteString(`<span class="breadcrumb">`)
// Lien racine
sb.WriteString(`<a href="#" hx-get="/api/home" hx-target="#editor-container" hx-swap="innerHTML" hx-push-url="true" class="breadcrumb-link">📁 Racine</a>`)
sb.WriteString(`<a href="#" hx-get="/api/home" hx-target="#editor-container" hx-swap="innerHTML" hx-push-url="true" class="breadcrumb-link"><i data-lucide="folder" class="icon-sm"></i> Racine</a>`)
// Construire les liens pour chaque partie
currentPath := ""

607
internal/api/public.go Normal file
View File

@ -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 = `<div class="public-tags">`
for _, tag := range tags {
tagsHTML += fmt.Sprintf(`<span class="tag">#%s</span>`, template.HTMLEscapeString(tag))
}
tagsHTML += `</div>`
}
dateHTML := ""
if date != "" {
dateHTML = fmt.Sprintf(`<span><i data-lucide="calendar" class="icon-sm"></i> %s</span>`, template.HTMLEscapeString(date))
}
return fmt.Sprintf(`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>%s - PersoNotes</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="static/theme.css" />
<link rel="stylesheet" href="static/themes.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/styles/atom-one-dark.min.css" />
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js"></script>
<!-- Lucide Icons -->
<script src="https://unpkg.com/lucide@latest"></script>
<style>
body { margin: 0; padding: 0; background: var(--bg-primary, #1e1e1e); color: var(--text-primary, #e0e0e0); font-family: 'JetBrains Mono', monospace; }
.public-view-container { max-width: 800px; margin: 0 auto; padding: 2rem; }
.public-nav { margin-bottom: 2rem; }
.public-nav a { color: var(--accent-blue, #42a5f5); text-decoration: none; display: inline-flex; align-items: center; gap: 0.5rem; }
.public-nav a:hover { text-decoration: underline; }
.public-header { margin-bottom: 2rem; padding-bottom: 1.5rem; border-bottom: 2px solid var(--bg-tertiary, #2d2d2d); }
.public-title { font-size: 2.5rem; margin: 0 0 1rem 0; color: var(--text-primary, #e0e0e0); }
.public-meta { display: flex; gap: 1.5rem; color: var(--text-secondary, #b0b0b0); font-size: 0.95rem; }
.public-tags { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 1rem; }
.tag { background: var(--bg-tertiary, #2d2d2d); color: var(--text-secondary, #b0b0b0); padding: 0.25rem 0.75rem; border-radius: 4px; font-size: 0.85rem; }
.public-content { line-height: 1.8; color: var(--text-primary, #e0e0e0); font-size: 1.05rem; }
.public-content h1, .public-content h2, .public-content h3, .public-content h4, .public-content h5, .public-content h6 { color: var(--text-primary, #e0e0e0); margin-top: 2rem; margin-bottom: 1rem; }
.public-content h1 { font-size: 2rem; } .public-content h2 { font-size: 1.75rem; } .public-content h3 { font-size: 1.5rem; } .public-content h4 { font-size: 1.25rem; }
.public-content p { margin-bottom: 1rem; }
.public-content a { color: var(--accent-blue, #42a5f5); text-decoration: none; }
.public-content a:hover { text-decoration: underline; }
.public-content pre { background: var(--bg-secondary, #252525); border: 1px solid var(--bg-tertiary, #2d2d2d); border-radius: 6px; padding: 1rem; overflow-x: auto; margin: 1rem 0; }
.public-content code { font-family: 'JetBrains Mono', monospace; background: var(--bg-secondary, #252525); padding: 0.2rem 0.4rem; border-radius: 3px; font-size: 0.9em; }
.public-content pre code { background: none; padding: 0; }
.public-content blockquote { border-left: 4px solid var(--accent-blue, #42a5f5); padding-left: 1rem; margin: 1rem 0; color: var(--text-secondary, #b0b0b0); font-style: italic; }
.public-content ul, .public-content ol { margin: 1rem 0; padding-left: 2rem; }
.public-content li { margin-bottom: 0.5rem; }
.public-content table { width: 100%%; border-collapse: collapse; margin: 1rem 0; }
.public-content th, .public-content td { border: 1px solid var(--bg-tertiary, #2d2d2d); padding: 0.75rem; text-align: left; }
.public-content th { background: var(--bg-secondary, #252525); font-weight: 600; }
.public-content img { max-width: 100%%; height: auto; border-radius: 6px; margin: 1rem 0; }
.public-content hr { border: none; border-top: 2px solid var(--bg-tertiary, #2d2d2d); margin: 2rem 0; }
</style>
</head>
<body>
<div class="public-view-container">
<div class="public-nav">
<a href="index.html">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="19" y1="12" x2="5" y2="12"></line>
<polyline points="12 19 5 12 12 5"></polyline>
</svg>
Back to public notes
</a>
</div>
<article>
<div class="public-header">
<h1 class="public-title">%s</h1>
<div class="public-meta">%s</div>
%s
</div>
<div class="public-content">
%s
</div>
</article>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('pre code').forEach((block) => {
hljs.highlightElement(block);
});
});
</script>
<script>
// Initialize Lucide icons
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
</script>
</body>
</html>`, 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 = `<ul class="notes-list">`
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 = `<div class="note-tags">`
for _, tag := range tags {
tagsHTML += fmt.Sprintf(`<span class="tag">#%s</span>`, template.HTMLEscapeString(tag))
}
tagsHTML += `</div>`
}
notesHTML += fmt.Sprintf(`
<li class="note-item">
<a href="%s">
<h2 class="note-title">%s</h2>
<div class="note-meta">
<span><i data-lucide="calendar" class="icon-sm"></i> Published on %s</span>
</div>
%s
</a>
</li>`, path, template.HTMLEscapeString(title), publishedAt, tagsHTML)
}
notesHTML += `</ul>`
} else {
notesHTML = `
<div class="empty-state">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
</svg>
<p>No public notes yet</p>
</div>`
}
return fmt.Sprintf(`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Public Notes - PersoNotes</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="static/theme.css" />
<link rel="stylesheet" href="static/themes.css" />
<!-- Lucide Icons -->
<script src="https://unpkg.com/lucide@latest"></script>
<style>
body { margin: 0; padding: 0; background: var(--bg-primary, #1e1e1e); color: var(--text-primary, #e0e0e0); font-family: 'JetBrains Mono', monospace; }
.public-container { max-width: 900px; margin: 0 auto; padding: 2rem; }
.public-header { text-align: center; margin-bottom: 3rem; padding-bottom: 1.5rem; border-bottom: 2px solid var(--bg-tertiary, #2d2d2d); }
.public-header h1 { font-size: 2.5rem; margin: 0 0 0.5rem 0; color: var(--text-primary, #e0e0e0); }
.public-header p { color: var(--text-secondary, #b0b0b0); font-size: 1.1rem; }
.notes-list { list-style: none; padding: 0; margin: 0; }
.note-item { background: var(--bg-secondary, #252525); border: 1px solid var(--bg-tertiary, #2d2d2d); border-radius: 8px; padding: 1.5rem; margin-bottom: 1rem; transition: transform 0.2s, box-shadow 0.2s; }
.note-item:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); }
.note-item a { text-decoration: none; color: inherit; display: block; }
.note-title { font-size: 1.5rem; font-weight: 600; margin: 0 0 0.5rem 0; color: var(--text-primary, #e0e0e0); }
.note-meta { display: flex; gap: 1rem; color: var(--text-secondary, #b0b0b0); font-size: 0.9rem; margin-bottom: 0.5rem; }
.note-tags { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.tag { background: var(--bg-tertiary, #2d2d2d); color: var(--text-secondary, #b0b0b0); padding: 0.25rem 0.75rem; border-radius: 4px; font-size: 0.85rem; }
.empty-state { text-align: center; padding: 3rem; color: var(--text-secondary, #b0b0b0); }
.empty-state svg { width: 64px; height: 64px; margin-bottom: 1rem; opacity: 0.5; }
</style>
</head>
<body>
<div class="public-container">
<div class="public-header">
<h1><i data-lucide="book-open" class="icon-md"></i> Public Notes</h1>
<p>Discover my shared notes</p>
</div>
%s
</div>
<script>
// Initialize Lucide icons
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
</script>
</body>
</html>`, notesHTML)
}

View File

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

View File

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

View File

@ -7,40 +7,61 @@
"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
}
]
}

29
notes/.public.json Normal file
View File

@ -0,0 +1,29 @@
{
"notes": [
{
"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"
}
]
}

25
notes/daily/2025/12/24.md Normal file
View File

@ -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
-

95
public/11.html Normal file
View File

@ -0,0 +1,95 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Daily Note - 2025-11-11 - PersoNotes</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="static/theme.css" />
<link rel="stylesheet" href="static/themes.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/styles/atom-one-dark.min.css" />
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js"></script>
<style>
body { margin: 0; padding: 0; background: var(--bg-primary, #1e1e1e); color: var(--text-primary, #e0e0e0); font-family: 'JetBrains Mono', monospace; }
.public-view-container { max-width: 800px; margin: 0 auto; padding: 2rem; }
.public-nav { margin-bottom: 2rem; }
.public-nav a { color: var(--accent-blue, #42a5f5); text-decoration: none; display: inline-flex; align-items: center; gap: 0.5rem; }
.public-nav a:hover { text-decoration: underline; }
.public-header { margin-bottom: 2rem; padding-bottom: 1.5rem; border-bottom: 2px solid var(--bg-tertiary, #2d2d2d); }
.public-title { font-size: 2.5rem; margin: 0 0 1rem 0; color: var(--text-primary, #e0e0e0); }
.public-meta { display: flex; gap: 1.5rem; color: var(--text-secondary, #b0b0b0); font-size: 0.95rem; }
.public-tags { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 1rem; }
.tag { background: var(--bg-tertiary, #2d2d2d); color: var(--text-secondary, #b0b0b0); padding: 0.25rem 0.75rem; border-radius: 4px; font-size: 0.85rem; }
.public-content { line-height: 1.8; color: var(--text-primary, #e0e0e0); font-size: 1.05rem; }
.public-content h1, .public-content h2, .public-content h3, .public-content h4, .public-content h5, .public-content h6 { color: var(--text-primary, #e0e0e0); margin-top: 2rem; margin-bottom: 1rem; }
.public-content h1 { font-size: 2rem; } .public-content h2 { font-size: 1.75rem; } .public-content h3 { font-size: 1.5rem; } .public-content h4 { font-size: 1.25rem; }
.public-content p { margin-bottom: 1rem; }
.public-content a { color: var(--accent-blue, #42a5f5); text-decoration: none; }
.public-content a:hover { text-decoration: underline; }
.public-content pre { background: var(--bg-secondary, #252525); border: 1px solid var(--bg-tertiary, #2d2d2d); border-radius: 6px; padding: 1rem; overflow-x: auto; margin: 1rem 0; }
.public-content code { font-family: 'JetBrains Mono', monospace; background: var(--bg-secondary, #252525); padding: 0.2rem 0.4rem; border-radius: 3px; font-size: 0.9em; }
.public-content pre code { background: none; padding: 0; }
.public-content blockquote { border-left: 4px solid var(--accent-blue, #42a5f5); padding-left: 1rem; margin: 1rem 0; color: var(--text-secondary, #b0b0b0); font-style: italic; }
.public-content ul, .public-content ol { margin: 1rem 0; padding-left: 2rem; }
.public-content li { margin-bottom: 0.5rem; }
.public-content table { width: 100%; border-collapse: collapse; margin: 1rem 0; }
.public-content th, .public-content td { border: 1px solid var(--bg-tertiary, #2d2d2d); padding: 0.75rem; text-align: left; }
.public-content th { background: var(--bg-secondary, #252525); font-weight: 600; }
.public-content img { max-width: 100%; height: auto; border-radius: 6px; margin: 1rem 0; }
.public-content hr { border: none; border-top: 2px solid var(--bg-tertiary, #2d2d2d); margin: 2rem 0; }
</style>
</head>
<body>
<div class="public-view-container">
<div class="public-nav">
<a href="index.html">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="19" y1="12" x2="5" y2="12"></line>
<polyline points="12 19 5 12 12 5"></polyline>
</svg>
Back to public notes
</a>
</div>
<article>
<div class="public-header">
<h1 class="public-title">Daily Note - 2025-11-11</h1>
<div class="public-meta"><span>📅 11-11-2025</span></div>
<div class="public-tags"><span class="tag">#daily</span></div>
</div>
<div class="public-content">
<h1 id="-mardi-11-novembre-2025">📅 Mardi 11 novembre 2025</h1>
<h2 id="-objectifs-du-jour">🎯 Objectifs du jour</h2>
<ul>
<li></li>
</ul>
<h2 id="-notes">📝 Notes</h2>
<ul>
<li></li>
</ul>
<h2 id="-accompli">✅ Accompli</h2>
<ul>
<li></li>
</ul>
<h2 id="-rflexions">💭 Réflexions</h2>
<ul>
<li></li>
</ul>
<h2 id="-liens">🔗 Liens</h2>
<ul>
<li></li>
</ul>
</div>
</article>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('pre code').forEach((block) => {
hljs.highlightElement(block);
});
});
</script>
</body>
</html>

95
public/backlog.html Normal file
View File

@ -0,0 +1,95 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Product Backlog - PersoNotes</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="static/theme.css" />
<link rel="stylesheet" href="static/themes.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/styles/atom-one-dark.min.css" />
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js"></script>
<style>
body { margin: 0; padding: 0; background: var(--bg-primary, #1e1e1e); color: var(--text-primary, #e0e0e0); font-family: 'JetBrains Mono', monospace; }
.public-view-container { max-width: 800px; margin: 0 auto; padding: 2rem; }
.public-nav { margin-bottom: 2rem; }
.public-nav a { color: var(--accent-blue, #42a5f5); text-decoration: none; display: inline-flex; align-items: center; gap: 0.5rem; }
.public-nav a:hover { text-decoration: underline; }
.public-header { margin-bottom: 2rem; padding-bottom: 1.5rem; border-bottom: 2px solid var(--bg-tertiary, #2d2d2d); }
.public-title { font-size: 2.5rem; margin: 0 0 1rem 0; color: var(--text-primary, #e0e0e0); }
.public-meta { display: flex; gap: 1.5rem; color: var(--text-secondary, #b0b0b0); font-size: 0.95rem; }
.public-tags { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 1rem; }
.tag { background: var(--bg-tertiary, #2d2d2d); color: var(--text-secondary, #b0b0b0); padding: 0.25rem 0.75rem; border-radius: 4px; font-size: 0.85rem; }
.public-content { line-height: 1.8; color: var(--text-primary, #e0e0e0); font-size: 1.05rem; }
.public-content h1, .public-content h2, .public-content h3, .public-content h4, .public-content h5, .public-content h6 { color: var(--text-primary, #e0e0e0); margin-top: 2rem; margin-bottom: 1rem; }
.public-content h1 { font-size: 2rem; } .public-content h2 { font-size: 1.75rem; } .public-content h3 { font-size: 1.5rem; } .public-content h4 { font-size: 1.25rem; }
.public-content p { margin-bottom: 1rem; }
.public-content a { color: var(--accent-blue, #42a5f5); text-decoration: none; }
.public-content a:hover { text-decoration: underline; }
.public-content pre { background: var(--bg-secondary, #252525); border: 1px solid var(--bg-tertiary, #2d2d2d); border-radius: 6px; padding: 1rem; overflow-x: auto; margin: 1rem 0; }
.public-content code { font-family: 'JetBrains Mono', monospace; background: var(--bg-secondary, #252525); padding: 0.2rem 0.4rem; border-radius: 3px; font-size: 0.9em; }
.public-content pre code { background: none; padding: 0; }
.public-content blockquote { border-left: 4px solid var(--accent-blue, #42a5f5); padding-left: 1rem; margin: 1rem 0; color: var(--text-secondary, #b0b0b0); font-style: italic; }
.public-content ul, .public-content ol { margin: 1rem 0; padding-left: 2rem; }
.public-content li { margin-bottom: 0.5rem; }
.public-content table { width: 100%; border-collapse: collapse; margin: 1rem 0; }
.public-content th, .public-content td { border: 1px solid var(--bg-tertiary, #2d2d2d); padding: 0.75rem; text-align: left; }
.public-content th { background: var(--bg-secondary, #252525); font-weight: 600; }
.public-content img { max-width: 100%; height: auto; border-radius: 6px; margin: 1rem 0; }
.public-content hr { border: none; border-top: 2px solid var(--bg-tertiary, #2d2d2d); margin: 2rem 0; }
</style>
</head>
<body>
<div class="public-view-container">
<div class="public-nav">
<a href="index.html">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="19" y1="12" x2="5" y2="12"></line>
<polyline points="12 19 5 12 12 5"></polyline>
</svg>
Back to public notes
</a>
</div>
<article>
<div class="public-header">
<h1 class="public-title">Product Backlog</h1>
<div class="public-meta"><span>📅 10-11-2025</span></div>
<div class="public-tags"><span class="tag">#task</span><span class="tag">#planning</span></div>
</div>
<div class="public-content">
<h1 id="product-backlog">Product Backlog</h1>
<h2 id="high-priority">High Priority</h2>
<ul>
<li><input disabled="" type="checkbox" /> Export notes to PDF</li>
<li><input disabled="" type="checkbox" /> Bulk operations (delete, move)</li>
<li><input disabled="" type="checkbox" /> Tags management page</li>
<li><input disabled="" type="checkbox" /> Keyboard shortcuts documentation</li>
</ul>
<h2 id="medium-priority">Medium Priority</h2>
<ul>
<li><input disabled="" type="checkbox" /> Note templates</li>
<li><input disabled="" type="checkbox" /> Trash/Recycle bin</li>
<li><input disabled="" type="checkbox" /> Note history/versions</li>
<li><input disabled="" type="checkbox" /> Full-text search improvements</li>
</ul>
<h2 id="low-priority">Low Priority</h2>
<ul>
<li><input disabled="" type="checkbox" /> Themes customization</li>
<li><input disabled="" type="checkbox" /> Plugin system</li>
<li><input disabled="" type="checkbox" /> Graph view of notes links</li>
</ul>
</div>
</article>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('pre code').forEach((block) => {
hljs.highlightElement(block);
});
});
</script>
</body>
</html>

89
public/collaboration.html Normal file
View File

@ -0,0 +1,89 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Real-time Collaboration - PersoNotes</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="static/theme.css" />
<link rel="stylesheet" href="static/themes.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/styles/atom-one-dark.min.css" />
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js"></script>
<style>
body { margin: 0; padding: 0; background: var(--bg-primary, #1e1e1e); color: var(--text-primary, #e0e0e0); font-family: 'JetBrains Mono', monospace; }
.public-view-container { max-width: 800px; margin: 0 auto; padding: 2rem; }
.public-nav { margin-bottom: 2rem; }
.public-nav a { color: var(--accent-blue, #42a5f5); text-decoration: none; display: inline-flex; align-items: center; gap: 0.5rem; }
.public-nav a:hover { text-decoration: underline; }
.public-header { margin-bottom: 2rem; padding-bottom: 1.5rem; border-bottom: 2px solid var(--bg-tertiary, #2d2d2d); }
.public-title { font-size: 2.5rem; margin: 0 0 1rem 0; color: var(--text-primary, #e0e0e0); }
.public-meta { display: flex; gap: 1.5rem; color: var(--text-secondary, #b0b0b0); font-size: 0.95rem; }
.public-tags { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 1rem; }
.tag { background: var(--bg-tertiary, #2d2d2d); color: var(--text-secondary, #b0b0b0); padding: 0.25rem 0.75rem; border-radius: 4px; font-size: 0.85rem; }
.public-content { line-height: 1.8; color: var(--text-primary, #e0e0e0); font-size: 1.05rem; }
.public-content h1, .public-content h2, .public-content h3, .public-content h4, .public-content h5, .public-content h6 { color: var(--text-primary, #e0e0e0); margin-top: 2rem; margin-bottom: 1rem; }
.public-content h1 { font-size: 2rem; } .public-content h2 { font-size: 1.75rem; } .public-content h3 { font-size: 1.5rem; } .public-content h4 { font-size: 1.25rem; }
.public-content p { margin-bottom: 1rem; }
.public-content a { color: var(--accent-blue, #42a5f5); text-decoration: none; }
.public-content a:hover { text-decoration: underline; }
.public-content pre { background: var(--bg-secondary, #252525); border: 1px solid var(--bg-tertiary, #2d2d2d); border-radius: 6px; padding: 1rem; overflow-x: auto; margin: 1rem 0; }
.public-content code { font-family: 'JetBrains Mono', monospace; background: var(--bg-secondary, #252525); padding: 0.2rem 0.4rem; border-radius: 3px; font-size: 0.9em; }
.public-content pre code { background: none; padding: 0; }
.public-content blockquote { border-left: 4px solid var(--accent-blue, #42a5f5); padding-left: 1rem; margin: 1rem 0; color: var(--text-secondary, #b0b0b0); font-style: italic; }
.public-content ul, .public-content ol { margin: 1rem 0; padding-left: 2rem; }
.public-content li { margin-bottom: 0.5rem; }
.public-content table { width: 100%; border-collapse: collapse; margin: 1rem 0; }
.public-content th, .public-content td { border: 1px solid var(--bg-tertiary, #2d2d2d); padding: 0.75rem; text-align: left; }
.public-content th { background: var(--bg-secondary, #252525); font-weight: 600; }
.public-content img { max-width: 100%; height: auto; border-radius: 6px; margin: 1rem 0; }
.public-content hr { border: none; border-top: 2px solid var(--bg-tertiary, #2d2d2d); margin: 2rem 0; }
</style>
</head>
<body>
<div class="public-view-container">
<div class="public-nav">
<a href="index.html">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="19" y1="12" x2="5" y2="12"></line>
<polyline points="12 19 5 12 12 5"></polyline>
</svg>
Back to public notes
</a>
</div>
<article>
<div class="public-header">
<h1 class="public-title">Real-time Collaboration</h1>
<div class="public-meta"><span>📅 10-11-2025</span></div>
<div class="public-tags"><span class="tag">#idea</span><span class="tag">#collaboration</span></div>
</div>
<div class="public-content">
<h1 id="real-time-collaboration">Real-time Collaboration</h1>
<h2 id="goal">Goal</h2>
<p>Plusieurs utilisateurs éditent la même note simultanément.</p>
<h2 id="technology">Technology</h2>
<ul>
<li>WebSockets</li>
<li>Operational Transforms ou CRDT</li>
<li>Presence indicators</li>
</ul>
<h2 id="challenges">Challenges</h2>
<ul>
<li>Conflict resolution</li>
<li>Performance at scale</li>
<li>User permissions</li>
</ul>
</div>
</article>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('pre code').forEach((block) => {
hljs.highlightElement(block);
});
});
</script>
</body>
</html>

92
public/index.html Normal file
View File

@ -0,0 +1,92 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Public Notes - PersoNotes</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="static/theme.css" />
<link rel="stylesheet" href="static/themes.css" />
<!-- Lucide Icons -->
<script src="https://unpkg.com/lucide@latest"></script>
<style>
body { margin: 0; padding: 0; background: var(--bg-primary, #1e1e1e); color: var(--text-primary, #e0e0e0); font-family: 'JetBrains Mono', monospace; }
.public-container { max-width: 900px; margin: 0 auto; padding: 2rem; }
.public-header { text-align: center; margin-bottom: 3rem; padding-bottom: 1.5rem; border-bottom: 2px solid var(--bg-tertiary, #2d2d2d); }
.public-header h1 { font-size: 2.5rem; margin: 0 0 0.5rem 0; color: var(--text-primary, #e0e0e0); }
.public-header p { color: var(--text-secondary, #b0b0b0); font-size: 1.1rem; }
.notes-list { list-style: none; padding: 0; margin: 0; }
.note-item { background: var(--bg-secondary, #252525); border: 1px solid var(--bg-tertiary, #2d2d2d); border-radius: 8px; padding: 1.5rem; margin-bottom: 1rem; transition: transform 0.2s, box-shadow 0.2s; }
.note-item:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); }
.note-item a { text-decoration: none; color: inherit; display: block; }
.note-title { font-size: 1.5rem; font-weight: 600; margin: 0 0 0.5rem 0; color: var(--text-primary, #e0e0e0); }
.note-meta { display: flex; gap: 1rem; color: var(--text-secondary, #b0b0b0); font-size: 0.9rem; margin-bottom: 0.5rem; }
.note-tags { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.tag { background: var(--bg-tertiary, #2d2d2d); color: var(--text-secondary, #b0b0b0); padding: 0.25rem 0.75rem; border-radius: 4px; font-size: 0.85rem; }
.empty-state { text-align: center; padding: 3rem; color: var(--text-secondary, #b0b0b0); }
.empty-state svg { width: 64px; height: 64px; margin-bottom: 1rem; opacity: 0.5; }
</style>
</head>
<body>
<div class="public-container">
<div class="public-header">
<h1><i data-lucide="book-open" class="icon-md"></i> Public Notes</h1>
<p>Discover my shared notes</p>
</div>
<ul class="notes-list">
<li class="note-item">
<a href="collaboration.html">
<h2 class="note-title">Real-time Collaboration</h2>
<div class="note-meta">
<span><i data-lucide="calendar" class="icon-sm"></i> Published on 24/12/2025</span>
</div>
</a>
</li>
<li class="note-item">
<a href="11.html">
<h2 class="note-title">Daily Note - 2025-11-11</h2>
<div class="note-meta">
<span><i data-lucide="calendar" class="icon-sm"></i> Published on 24/12/2025</span>
</div>
</a>
</li>
<li class="note-item">
<a href="mobile-app.html">
<h2 class="note-title">Native Mobile App</h2>
<div class="note-meta">
<span><i data-lucide="calendar" class="icon-sm"></i> Published on 24/12/2025</span>
</div>
</a>
</li>
<li class="note-item">
<a href="backlog.html">
<h2 class="note-title">Product Backlog</h2>
<div class="note-meta">
<span><i data-lucide="calendar" class="icon-sm"></i> Published on 13/11/2025</span>
</div>
</a>
</li>
<li class="note-item">
<a href="endpoints.html">
<h2 class="note-title">API Endpoints Reference</h2>
<div class="note-meta">
<span><i data-lucide="calendar" class="icon-sm"></i> Published on 13/11/2025</span>
</div>
</a>
</li></ul>
</div>
<script>
// Initialize Lucide icons
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
</script>
</body>
</html>

93
public/mobile-app.html Normal file
View File

@ -0,0 +1,93 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Native Mobile App - PersoNotes</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="static/theme.css" />
<link rel="stylesheet" href="static/themes.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/styles/atom-one-dark.min.css" />
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js"></script>
<style>
body { margin: 0; padding: 0; background: var(--bg-primary, #1e1e1e); color: var(--text-primary, #e0e0e0); font-family: 'JetBrains Mono', monospace; }
.public-view-container { max-width: 800px; margin: 0 auto; padding: 2rem; }
.public-nav { margin-bottom: 2rem; }
.public-nav a { color: var(--accent-blue, #42a5f5); text-decoration: none; display: inline-flex; align-items: center; gap: 0.5rem; }
.public-nav a:hover { text-decoration: underline; }
.public-header { margin-bottom: 2rem; padding-bottom: 1.5rem; border-bottom: 2px solid var(--bg-tertiary, #2d2d2d); }
.public-title { font-size: 2.5rem; margin: 0 0 1rem 0; color: var(--text-primary, #e0e0e0); }
.public-meta { display: flex; gap: 1.5rem; color: var(--text-secondary, #b0b0b0); font-size: 0.95rem; }
.public-tags { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 1rem; }
.tag { background: var(--bg-tertiary, #2d2d2d); color: var(--text-secondary, #b0b0b0); padding: 0.25rem 0.75rem; border-radius: 4px; font-size: 0.85rem; }
.public-content { line-height: 1.8; color: var(--text-primary, #e0e0e0); font-size: 1.05rem; }
.public-content h1, .public-content h2, .public-content h3, .public-content h4, .public-content h5, .public-content h6 { color: var(--text-primary, #e0e0e0); margin-top: 2rem; margin-bottom: 1rem; }
.public-content h1 { font-size: 2rem; } .public-content h2 { font-size: 1.75rem; } .public-content h3 { font-size: 1.5rem; } .public-content h4 { font-size: 1.25rem; }
.public-content p { margin-bottom: 1rem; }
.public-content a { color: var(--accent-blue, #42a5f5); text-decoration: none; }
.public-content a:hover { text-decoration: underline; }
.public-content pre { background: var(--bg-secondary, #252525); border: 1px solid var(--bg-tertiary, #2d2d2d); border-radius: 6px; padding: 1rem; overflow-x: auto; margin: 1rem 0; }
.public-content code { font-family: 'JetBrains Mono', monospace; background: var(--bg-secondary, #252525); padding: 0.2rem 0.4rem; border-radius: 3px; font-size: 0.9em; }
.public-content pre code { background: none; padding: 0; }
.public-content blockquote { border-left: 4px solid var(--accent-blue, #42a5f5); padding-left: 1rem; margin: 1rem 0; color: var(--text-secondary, #b0b0b0); font-style: italic; }
.public-content ul, .public-content ol { margin: 1rem 0; padding-left: 2rem; }
.public-content li { margin-bottom: 0.5rem; }
.public-content table { width: 100%; border-collapse: collapse; margin: 1rem 0; }
.public-content th, .public-content td { border: 1px solid var(--bg-tertiary, #2d2d2d); padding: 0.75rem; text-align: left; }
.public-content th { background: var(--bg-secondary, #252525); font-weight: 600; }
.public-content img { max-width: 100%; height: auto; border-radius: 6px; margin: 1rem 0; }
.public-content hr { border: none; border-top: 2px solid var(--bg-tertiary, #2d2d2d); margin: 2rem 0; }
</style>
</head>
<body>
<div class="public-view-container">
<div class="public-nav">
<a href="index.html">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="19" y1="12" x2="5" y2="12"></line>
<polyline points="12 19 5 12 12 5"></polyline>
</svg>
Back to public notes
</a>
</div>
<article>
<div class="public-header">
<h1 class="public-title">Native Mobile App</h1>
<div class="public-meta"><span>📅 10-11-2025</span></div>
<div class="public-tags"><span class="tag">#idea</span><span class="tag">#mobile</span></div>
</div>
<div class="public-content">
<h1 id="native-mobile-app-idea">Native Mobile App Idea</h1>
<h2 id="concept">Concept</h2>
<p>Créer une app native iOS/Android pour l&rsquo;édition de notes.</p>
<h2 id="tech-stack">Tech Stack</h2>
<ul>
<li>React Native ou Flutter</li>
<li>Sync avec l&rsquo;API REST</li>
<li>Offline-first architecture</li>
</ul>
<h2 id="features">Features</h2>
<ul>
<li>Push notifications</li>
<li>Widget home screen</li>
<li>Voice notes</li>
<li>Photo attachments</li>
</ul>
<h2 id="timeline">Timeline</h2>
<p>Q2 2025 - Prototype<br />
Q3 2025 - Beta testing</p>
</div>
</article>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('pre code').forEach((block) => {
hljs.highlightElement(block);
});
});
</script>
</body>
</html>

View File

@ -0,0 +1,94 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>2025 Learning Goals - PersoNotes</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="../static/theme.css" />
<link rel="stylesheet" href="../static/themes.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/styles/atom-one-dark.min.css" />
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js"></script>
<style>
body { margin: 0; padding: 0; background: var(--bg-primary, #1e1e1e); color: var(--text-primary, #e0e0e0); font-family: 'JetBrains Mono', monospace; }
.public-view-container { max-width: 800px; margin: 0 auto; padding: 2rem; }
.public-nav { margin-bottom: 2rem; }
.public-nav a { color: var(--accent-blue, #42a5f5); text-decoration: none; display: inline-flex; align-items: center; gap: 0.5rem; }
.public-nav a:hover { text-decoration: underline; }
.public-header { margin-bottom: 2rem; padding-bottom: 1.5rem; border-bottom: 2px solid var(--bg-tertiary, #2d2d2d); }
.public-title { font-size: 2.5rem; margin: 0 0 1rem 0; color: var(--text-primary, #e0e0e0); }
.public-meta { display: flex; gap: 1.5rem; color: var(--text-secondary, #b0b0b0); font-size: 0.95rem; }
.public-tags { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 1rem; }
.tag { background: var(--bg-tertiary, #2d2d2d); color: var(--text-secondary, #b0b0b0); padding: 0.25rem 0.75rem; border-radius: 4px; font-size: 0.85rem; }
.public-content { line-height: 1.8; color: var(--text-primary, #e0e0e0); font-size: 1.05rem; }
.public-content h1, .public-content h2, .public-content h3, .public-content h4, .public-content h5, .public-content h6 { color: var(--text-primary, #e0e0e0); margin-top: 2rem; margin-bottom: 1rem; }
.public-content h1 { font-size: 2rem; } .public-content h2 { font-size: 1.75rem; } .public-content h3 { font-size: 1.5rem; } .public-content h4 { font-size: 1.25rem; }
.public-content p { margin-bottom: 1rem; }
.public-content a { color: var(--accent-blue, #42a5f5); text-decoration: none; }
.public-content a:hover { text-decoration: underline; }
.public-content pre { background: var(--bg-secondary, #252525); border: 1px solid var(--bg-tertiary, #2d2d2d); border-radius: 6px; padding: 1rem; overflow-x: auto; margin: 1rem 0; }
.public-content code { font-family: 'JetBrains Mono', monospace; background: var(--bg-secondary, #252525); padding: 0.2rem 0.4rem; border-radius: 3px; font-size: 0.9em; }
.public-content pre code { background: none; padding: 0; }
.public-content blockquote { border-left: 4px solid var(--accent-blue, #42a5f5); padding-left: 1rem; margin: 1rem 0; color: var(--text-secondary, #b0b0b0); font-style: italic; }
.public-content ul, .public-content ol { margin: 1rem 0; padding-left: 2rem; }
.public-content li { margin-bottom: 0.5rem; }
.public-content table { width: 100%; border-collapse: collapse; margin: 1rem 0; }
.public-content th, .public-content td { border: 1px solid var(--bg-tertiary, #2d2d2d); padding: 0.75rem; text-align: left; }
.public-content th { background: var(--bg-secondary, #252525); font-weight: 600; }
.public-content img { max-width: 100%; height: auto; border-radius: 6px; margin: 1rem 0; }
.public-content hr { border: none; border-top: 2px solid var(--bg-tertiary, #2d2d2d); margin: 2rem 0; }
</style>
</head>
<body>
<div class="public-view-container">
<div class="public-nav">
<a href="../index.html">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="19" y1="12" x2="5" y2="12"></line>
<polyline points="12 19 5 12 12 5"></polyline>
</svg>
Back to public notes
</a>
</div>
<article>
<div class="public-header">
<h1 class="public-title">2025 Learning Goals</h1>
<div class="public-meta"><span>📅 10-11-2025</span></div>
<div class="public-tags"><span class="tag">#personal</span><span class="tag">#learning</span></div>
</div>
<div class="public-content">
<h1 id="learning-goals-2025">Learning Goals 2025</h1>
<h2 id="technical">Technical</h2>
<ul>
<li><input checked="" disabled="" type="checkbox" /> Master Go concurrency patterns</li>
<li><input disabled="" type="checkbox" /> Learn Rust basics</li>
<li><input disabled="" type="checkbox" /> Deep dive into databases</li>
<li><input disabled="" type="checkbox" /> System design courses</li>
</ul>
<h2 id="soft-skills">Soft Skills</h2>
<ul>
<li><input disabled="" type="checkbox" /> Technical writing</li>
<li><input disabled="" type="checkbox" /> Public speaking</li>
<li><input disabled="" type="checkbox" /> Mentoring</li>
</ul>
<h2 id="books-to-read">Books to Read</h2>
<ol>
<li>Designing Data-Intensive Applications</li>
<li>The Pragmatic Programmer</li>
<li>Clean Architecture</li>
</ol>
</div>
</article>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('pre code').forEach((block) => {
hljs.highlightElement(block);
});
});
</script>
</body>
</html>

3643
public/static/theme.css Normal file

File diff suppressed because it is too large Load Diff

695
public/static/themes.css Normal file
View File

@ -0,0 +1,695 @@
/*
* 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-primary-hover: #5ab3f7;
--accent-secondary: #29b6f6;
--accent-secondary-hover: #4fc3f7;
--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-primary-hover: #7ee5f7;
--accent-secondary: #88c070;
--accent-secondary-hover: #9acc84;
--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-primary-hover: #9ff3ff;
--accent-secondary: #bd93f9;
--accent-secondary-hover: #cba6ff;
--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-primary-hover: #75bdf5;
--accent-secondary: #c678dd;
--accent-secondary-hover: #d48ae9;
--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-primary-hover: #4098d9;
--accent-secondary: #2aa198;
--accent-secondary-hover: #3eb3a8;
--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-primary-hover: #9dcadb;
--accent-secondary: #81a1c1;
--accent-secondary-hover: #94b0cc;
--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-primary-hover: #a6c8ff;
--accent-secondary: #f5c2e7;
--accent-secondary-hover: #f9d5ee;
--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-primary-hover: #93c9c1;
--accent-secondary: #a7c080;
--accent-secondary-hover: #b8cc94;
--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);
}

View File

@ -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);
}

View File

@ -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;

View File

@ -1,7 +1,7 @@
<div id="about-content" style="padding: 3rem; max-width: 900px; margin: 0 auto;">
<div style="text-align: center; margin-bottom: 3rem;">
<h1 style="font-size: 2.5rem; color: #c792ea; margin-bottom: 1rem;">
📝 About PersoNotes
<i data-lucide="file-text" class="icon-md"></i> About PersoNotes
</h1>
<p style="font-size: 1.2rem; color: var(--text-secondary); margin-bottom: 2rem;">
Un gestionnaire de notes Markdown moderne et puissant
@ -10,23 +10,23 @@
<div style="margin-top: 3rem;">
<h2 style="color: #82aaff; font-size: 1.5rem; margin-bottom: 1.5rem;">
🚀 Démarrage rapide
<i data-lucide="rocket" class="icon-sm"></i> Démarrage rapide
</h2>
<div style="display: grid; gap: 1.5rem; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));">
<div style="background: var(--bg-secondary); border: 1px solid var(--border-primary); border-radius: var(--radius-md); padding: 1.5rem;">
<h3 style="color: #89ddff; margin-bottom: 0.5rem;">📁 Parcourir</h3>
<h3 style="color: #89ddff; margin-bottom: 0.5rem;"><i data-lucide="folder-open" class="icon-sm"></i> Parcourir</h3>
<p style="color: var(--text-secondary); font-size: 0.9rem; margin-bottom: 1rem;">
Explorez vos notes dans l'arborescence à gauche
</p>
</div>
<div style="background: var(--bg-secondary); border: 1px solid var(--border-primary); border-radius: var(--radius-md); padding: 1.5rem;">
<h3 style="color: #c3e88d; margin-bottom: 0.5rem;">🔍 Rechercher</h3>
<h3 style="color: #c3e88d; margin-bottom: 0.5rem;"><i data-lucide="search" class="icon-sm"></i> Rechercher</h3>
<p style="color: var(--text-secondary); font-size: 0.9rem; margin-bottom: 1rem;">
Utilisez la barre de recherche en haut pour trouver vos notes
</p>
</div>
<div style="background: var(--bg-secondary); border: 1px solid var(--border-primary); border-radius: var(--radius-md); padding: 1.5rem;">
<h3 style="color: #ffcb6b; margin-bottom: 0.5rem;"> Slash commands</h3>
<h3 style="color: #ffcb6b; margin-bottom: 0.5rem;"><i data-lucide="zap" class="icon-sm"></i> Slash commands</h3>
<p style="color: var(--text-secondary); font-size: 0.9rem; margin-bottom: 1rem;">
Tapez <code style="color: #f07178;">/</code> dans l'éditeur pour insérer du Markdown
</p>
@ -36,7 +36,7 @@
<div style="margin-top: 3rem;">
<h2 style="color: #82aaff; font-size: 1.5rem; margin-bottom: 1.5rem;">
Fonctionnalités
<i data-lucide="sparkles" class="icon-sm"></i> Fonctionnalités
</h2>
<ul style="color: var(--text-secondary); line-height: 2; list-style: none; padding: 0;">
<li style="padding: 0.5rem 0; border-bottom: 1px solid var(--border-secondary);">
@ -62,13 +62,13 @@
<div style="margin-top: 3rem; text-align: center; padding: 2rem; background: var(--bg-secondary); border-radius: var(--radius-md); border: 1px solid var(--border-primary);">
<p style="color: var(--text-muted); font-size: 0.9rem;">
💡 Astuce : Cliquez sur une note dans l'arborescence pour commencer à éditer
<i data-lucide="lightbulb" class="icon-sm"></i> Astuce : Cliquez sur une note dans l'arborescence pour commencer à éditer
</p>
</div>
<div style="margin-top: 3rem;">
<h2 style="color: #82aaff; font-size: 1.5rem; margin-bottom: 1.5rem;">
⌨️ Raccourcis clavier
<i data-lucide="keyboard" class="icon-sm"></i> Raccourcis clavier
</h2>
<div style="display: grid; gap: 0.75rem; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));">
<div style="background: var(--bg-secondary); border: 1px solid var(--border-primary); border-radius: var(--radius-sm); padding: 0.75rem; display: flex; justify-content: space-between; align-items: center;">
@ -113,7 +113,7 @@
</div>
</div>
<p style="color: var(--text-muted); font-size: 0.85rem; text-align: center; margin-top: 1.5rem;">
💡 Sur Mac, utilisez Cmd au lieu de Ctrl
<i data-lucide="lightbulb" class="icon-sm"></i> Sur Mac, utilisez Cmd au lieu de Ctrl
</p>
</div>
</div>

View File

@ -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
<i data-lucide="calendar-check" class="icon-sm"></i> Aujourd'hui
</button>
<button class="daily-today-btn"
hx-get="/api/daily/calendar/{{.CurrentMonth}}"
@ -66,7 +66,7 @@
data-i18n="calendar.thisMonth"
title="Revenir au mois actuel"
style="flex: 1; padding: 0.5rem; font-size: 0.85rem;">
🗓️ Ce mois
<i data-lucide="calendar-days" class="icon-sm"></i> Ce mois
</button>
</div>
</div>

View File

@ -19,7 +19,7 @@
</label>
{{if .IsHome}}
<button type="button" class="toggle-preview-btn" hx-get="/api/home" hx-target="#editor-container" hx-swap="innerHTML" hx-push-url="true" data-i18n="editor.refresh" title="Actualiser la page d'accueil">
🔄 Actualiser
<i data-lucide="refresh-cw" class="icon-sm"></i> Actualiser
</button>
{{else}}
<button type="button" id="toggle-preview-btn" class="toggle-preview-btn" onclick="togglePreview()" data-i18n-title="editor.togglePreview" title="Mode: Éditeur + Preview (cliquer pour Éditeur seul)">
@ -36,12 +36,12 @@
</div>
{{if .Backlinks}}
<div id="backlinks-section" class="backlinks-section">
<h3 class="backlinks-title">🔗 Référencé par</h3>
<h3 class="backlinks-title"><i data-lucide="link" class="icon-sm"></i> Référencé par</h3>
<ul class="backlinks-list">
{{range .Backlinks}}
<li class="backlink-item">
<a href="#" onclick="return false;" hx-get="/api/notes/{{.Path}}" hx-target="#editor-container" hx-swap="innerHTML" hx-push-url="true" class="backlink-link">
📄 {{.Title}}
<i data-lucide="file-text" class="icon-sm"></i> {{.Title}}
</a>
</li>
{{end}}
@ -54,6 +54,17 @@
<div class="editor-actions">
<div class="editor-actions-primary">
<button type="submit" data-i18n="editor.save">Enregistrer</button>
<button
id="toggle-public-btn"
type="button"
class="secondary {{if .IsPublic}}public-active{{end}}"
data-path="{{.Filename}}"
data-is-public="{{.IsPublic}}"
data-i18n-title="{{if .IsPublic}}public.titlePublic{{else}}public.titlePrivate{{end}}"
title="{{if .IsPublic}}This note is public - Click to make it private{{else}}This note is private - Click to make it public{{end}}"
>
{{if .IsPublic}}<span data-i18n="public.buttonPublic"><i data-lucide="globe" class="icon-sm"></i> Public</span>{{else}}<span data-i18n="public.buttonPrivate"><i data-lucide="lock" class="icon-sm"></i> Private</span>{{end}}
</button>
<button
hx-delete="/api/notes/{{.Filename}}"
hx-confirm="Êtes-vous sûr de vouloir supprimer cette note ({{.Filename}}) ?"

View File

@ -3,12 +3,12 @@
<div class="favorite-item" data-path="{{.Path}}">
{{if .IsDir}}
<div class="favorite-folder" data-path="{{.Path}}">
<span class="favorite-icon"></span>
<span class="favorite-icon"><i data-lucide="star" class="icon-sm"></i></span>
<span class="favorite-folder-icon">{{.Icon}}</span>
<span class="favorite-name">{{.Title}}</span>
<button class="favorite-remove"
onclick="removeFavorite('{{.Path}}')"
title="Retirer des favoris">×</button>
<button class="favorite-remove"
onclick="removeFavorite('{{.Path}}')"
title="Retirer des favoris"><i data-lucide="x" class="icon-xs"></i></button>
</div>
{{else}}
<a href="#"
@ -18,12 +18,12 @@
hx-target="#editor-container"
hx-swap="innerHTML"
hx-push-url="true">
<span class="favorite-icon"></span>
<span class="favorite-icon"><i data-lucide="star" class="icon-sm"></i></span>
<span class="favorite-file-icon">{{.Icon}}</span>
<span class="favorite-name">{{.Title}}</span>
<button class="favorite-remove"
onclick="event.preventDefault(); event.stopPropagation(); removeFavorite('{{.Path}}')"
title="Retirer des favoris">×</button>
<button class="favorite-remove"
onclick="event.preventDefault(); event.stopPropagation(); removeFavorite('{{.Path}}')"
title="Retirer des favoris"><i data-lucide="x" class="icon-xs"></i></button>
</a>
{{end}}
</div>
@ -32,7 +32,7 @@
<p class="favorites-empty">
Aucun favori.<br>
<span style="font-size: 0.75rem; color: var(--text-muted);">
Cliquez sur à côté d'une note ou d'un dossier pour l'ajouter.
Cliquez sur <i data-lucide="star" class="icon-sm" style="display: inline; width: 14px; height: 14px; vertical-align: middle;"></i> à côté d'une note ou d'un dossier pour l'ajouter.
</span>
</p>
{{end}}

View File

@ -1,7 +1,7 @@
<!-- Indicateur de racine (maintenant cliquable et rétractable) -->
<div class="sidebar-section-header" data-section="notes" data-path="" data-is-dir="true" onclick="toggleSidebarSection('notes', event)" style="cursor: pointer;">
<span class="section-toggle expanded"></span>
<span class="folder-icon">🏠</span>
<span class="folder-icon"><i data-lucide="home" class="icon-sm"></i></span>
<span class="folder-name">Racine</span>
<span class="root-hint">(notes/)</span>
</div>
@ -28,7 +28,7 @@
<div class="folder-header">
<input type="checkbox" class="selection-checkbox folder-checkbox" data-path="{{.Path}}" data-is-dir="true" style="display: none;">
<span class="folder-toggle"></span>
<span class="folder-icon">📁</span>
<span class="folder-icon"><i data-lucide="folder" class="icon-sm"></i></span>
<span class="folder-name">{{.Name}}</span>
</div>
<div class="folder-children" style="display: none;">
@ -49,7 +49,7 @@
hx-swap="innerHTML"
hx-push-url="true"
draggable="true">
📄 {{.Name}}
<i data-lucide="file-text" class="icon-sm"></i> {{.Name}}
</a>
</div>
{{end}}

View File

@ -16,13 +16,15 @@
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify/dist/purify.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js"></script>
<!-- Lucide Icons - Professional SVG icons -->
<script src="https://unpkg.com/lucide@latest"></script>
<script src="/static/sidebar-resize.js"></script>
<script type="module" src="/static/dist/personotes-frontend.es.js"></script>
</head>
<body>
<header>
<button id="toggle-sidebar-btn" title="Afficher/Masquer la barre latérale (Ctrl/Cmd+B)" style="background: none; border: none; padding: 0; margin-right: 1rem; cursor: pointer; color: var(--text-primary); display: flex; align-items: center;">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line></svg>
<i data-lucide="menu" style="width: 24px; height: 24px;"></i>
</button>
<div style="display: flex; align-items: center; gap: 0.75rem;">
<img src="/static/images/logo.svg" alt="Logo" style="width: 40px; height: 40px;">
@ -46,10 +48,10 @@
style="white-space: nowrap;"
data-i18n="menu.home"
title="Retour à la page d'accueil (Ctrl/Cmd+H)">
🏠 Accueil
<i data-lucide="home" class="icon-sm"></i> Accueil
</button>
<button onclick="showNewNoteModal()" style="white-space: nowrap;" data-i18n="menu.newNote" title="Créer une nouvelle note (Ctrl/Cmd+N)">
Nouvelle note
<i data-lucide="file-plus" class="icon-sm"></i> Nouvelle note
</button>
</header>
@ -57,7 +59,7 @@
<div id="new-note-modal" style="display: none;">
<div class="modal-overlay" onclick="hideNewNoteModal()"></div>
<div class="modal-content">
<h2>📝 Nouvelle note</h2>
<h2><i data-lucide="file-text" class="icon-md"></i> Nouvelle note</h2>
<form onsubmit="handleNewNote(event)">
<label for="note-name">Nom de la note</label>
<input
@ -69,7 +71,7 @@
required
/>
<p style="color: var(--text-muted); font-size: 0.85rem; margin-top: 0.5rem;">
💡 Si la note existe déjà, elle sera ouverte.
<i data-lucide="lightbulb" class="icon-sm"></i> Si la note existe déjà, elle sera ouverte.
</p>
<div style="display: flex; gap: 0.5rem; margin-top: 1.5rem;">
<button type="submit">Créer / Ouvrir</button>
@ -83,7 +85,7 @@
<div id="new-folder-modal" style="display: none;">
<div class="modal-overlay" onclick="hideNewFolderModal()"></div>
<div class="modal-content">
<h2>📁 Nouveau dossier</h2>
<h2><i data-lucide="folder-plus" class="icon-md"></i> Nouveau dossier</h2>
<form onsubmit="handleNewFolder(event)">
<label for="folder-name">Nom du dossier</label>
<input
@ -95,7 +97,7 @@
required
/>
<p style="color: var(--text-muted); font-size: 0.85rem; margin-top: 0.5rem;">
💡 Vous pouvez créer des sous-dossiers avec "/", ex: projets/backend
<i data-lucide="lightbulb" class="icon-sm"></i> Vous pouvez créer des sous-dossiers avec "/", ex: projets/backend
</p>
<div style="display: flex; gap: 0.5rem; margin-top: 1.5rem;">
<button type="submit">Créer</button>
@ -114,10 +116,7 @@
<span id="selection-count" class="selection-count">0 élément(s) sélectionné(s)</span>
<div class="toolbar-actions">
<button onclick="deleteSelected()" class="danger-button">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
<i data-lucide="trash-2" class="icon-sm"></i>
Supprimer
</button>
<button onclick="cancelSelection()" class="secondary">
@ -131,13 +130,13 @@
<div id="delete-confirmation-modal" style="display: none;">
<div class="modal-overlay" onclick="hideDeleteConfirmationModal()"></div>
<div class="modal-content">
<h2 style="color: var(--error);">⚠️ Confirmer la suppression</h2>
<h2 style="color: var(--error);"><i data-lucide="alert-triangle" class="icon-md"></i> Confirmer la suppression</h2>
<p>Vous êtes sur le point de supprimer <strong id="delete-count">0</strong> élément(s) :</p>
<div id="delete-items-list" style="max-height: 300px; overflow-y: auto; margin: 1rem 0; padding: 0.5rem; background: var(--bg-tertiary); border-radius: var(--radius-md);">
<!-- Liste des éléments à supprimer -->
</div>
<p style="color: var(--warning); font-size: 0.9rem;">
⚠️ Cette action est <strong>irréversible</strong>. Les dossiers seront supprimés avec tout leur contenu.
<i data-lucide="alert-triangle" class="icon-sm"></i> Cette action est <strong>irréversible</strong>. Les dossiers seront supprimés avec tout leur contenu.
</p>
<div style="display: flex; gap: 0.5rem; margin-top: 1.5rem;">
<button onclick="confirmDelete()" class="danger-button">Confirmer la suppression</button>
@ -151,22 +150,22 @@
<div class="theme-modal-overlay" onclick="closeThemeModal()"></div>
<div class="theme-modal-content">
<h2>
⚙️ Paramètres d'apparence
<i data-lucide="settings" class="icon-md"></i> Paramètres d'apparence
</h2>
<!-- Onglets -->
<div class="settings-tabs">
<button class="settings-tab active" onclick="switchSettingsTab('themes')">
🎨 Thèmes
<i data-lucide="palette" class="icon-sm"></i> Thèmes
</button>
<button class="settings-tab" onclick="switchSettingsTab('fonts')">
🔤 Polices
<i data-lucide="type" class="icon-sm"></i> Polices
</button>
<button class="settings-tab" onclick="switchSettingsTab('editor')">
⌨️ Éditeur
<i data-lucide="keyboard" class="icon-sm"></i> Éditeur
</button>
<button class="settings-tab" onclick="switchSettingsTab('other')">
⚙️ Autre
<i data-lucide="settings" class="icon-sm"></i> Autre
</button>
</div>
@ -176,7 +175,7 @@
<!-- Material Dark -->
<div class="theme-card active" data-theme="material-dark" onclick="selectTheme('material-dark')">
<div class="theme-card-header">
<span class="theme-card-icon">🌙</span>
<span class="theme-card-icon"><i data-lucide="moon" class="icon-sm"></i></span>
<span class="theme-card-name">Material Dark</span>
</div>
<div class="theme-preview">
@ -191,7 +190,7 @@
<!-- Monokai Dark -->
<div class="theme-card" data-theme="monokai-dark" onclick="selectTheme('monokai-dark')">
<div class="theme-card-header">
<span class="theme-card-icon">🎨</span>
<span class="theme-card-icon"><i data-lucide="palette" class="icon-sm"></i></span>
<span class="theme-card-name">Monokai Dark</span>
</div>
<div class="theme-preview">
@ -206,7 +205,7 @@
<!-- Dracula -->
<div class="theme-card" data-theme="dracula" onclick="selectTheme('dracula')">
<div class="theme-card-header">
<span class="theme-card-icon">🧛</span>
<span class="theme-card-icon"><i data-lucide="moon-star" class="icon-sm"></i></span>
<span class="theme-card-name">Dracula</span>
</div>
<div class="theme-preview">
@ -221,7 +220,7 @@
<!-- One Dark -->
<div class="theme-card" data-theme="one-dark" onclick="selectTheme('one-dark')">
<div class="theme-card-header">
<span class="theme-card-icon"></span>
<span class="theme-card-icon"><i data-lucide="zap" class="icon-sm"></i></span>
<span class="theme-card-name">One Dark</span>
</div>
<div class="theme-preview">
@ -236,7 +235,7 @@
<!-- Solarized Dark -->
<div class="theme-card" data-theme="solarized-dark" onclick="selectTheme('solarized-dark')">
<div class="theme-card-header">
<span class="theme-card-icon">☀️</span>
<span class="theme-card-icon"><i data-lucide="sun" class="icon-sm"></i></span>
<span class="theme-card-name">Solarized Dark</span>
</div>
<div class="theme-preview">
@ -251,7 +250,7 @@
<!-- Nord -->
<div class="theme-card" data-theme="nord" onclick="selectTheme('nord')">
<div class="theme-card-header">
<span class="theme-card-icon">❄️</span>
<span class="theme-card-icon"><i data-lucide="snowflake" class="icon-sm"></i></span>
<span class="theme-card-name">Nord</span>
</div>
<div class="theme-preview">
@ -266,7 +265,7 @@
<!-- Catppuccin -->
<div class="theme-card" data-theme="catppuccin" onclick="selectTheme('catppuccin')">
<div class="theme-card-header">
<span class="theme-card-icon">🌸</span>
<span class="theme-card-icon"><i data-lucide="flower" class="icon-sm"></i></span>
<span class="theme-card-name">Catppuccin</span>
</div>
<div class="theme-preview">
@ -281,7 +280,7 @@
<!-- Everforest -->
<div class="theme-card" data-theme="everforest" onclick="selectTheme('everforest')">
<div class="theme-card-header">
<span class="theme-card-icon">🌲</span>
<span class="theme-card-icon"><i data-lucide="tree-deciduous" class="icon-sm"></i></span>
<span class="theme-card-name">Everforest</span>
</div>
<div class="theme-preview">
@ -301,7 +300,7 @@
<!-- Fira Code -->
<div class="font-card" data-font="fira-code" onclick="selectFont('fira-code')">
<div class="font-card-header">
<span class="font-card-icon">💻</span>
<span class="font-card-icon"><i data-lucide="code" class="icon-sm"></i></span>
<span class="font-card-name">Fira Code</span>
</div>
<div class="font-preview" style="font-family: 'Fira Code', monospace;">
@ -313,7 +312,7 @@
<!-- Sans-serif -->
<div class="font-card" data-font="sans-serif" onclick="selectFont('sans-serif')">
<div class="font-card-header">
<span class="font-card-icon">📝</span>
<span class="font-card-icon"><i data-lucide="type" class="icon-sm"></i></span>
<span class="font-card-name">Sans-serif</span>
</div>
<div class="font-preview" style="font-family: -apple-system, sans-serif;">
@ -325,7 +324,7 @@
<!-- Inter -->
<div class="font-card" data-font="inter" onclick="selectFont('inter')">
<div class="font-card-header">
<span class="font-card-icon"></span>
<span class="font-card-icon"><i data-lucide="sparkles" class="icon-sm"></i></span>
<span class="font-card-name">Inter</span>
</div>
<div class="font-preview" style="font-family: 'Inter', sans-serif;">
@ -337,7 +336,7 @@
<!-- Poppins -->
<div class="font-card" data-font="poppins" onclick="selectFont('poppins')">
<div class="font-card-header">
<span class="font-card-icon">🎯</span>
<span class="font-card-icon"><i data-lucide="target" class="icon-sm"></i></span>
<span class="font-card-name">Poppins</span>
</div>
<div class="font-preview" style="font-family: 'Poppins', sans-serif;">
@ -349,7 +348,7 @@
<!-- Public Sans -->
<div class="font-card" data-font="public-sans" onclick="selectFont('public-sans')">
<div class="font-card-header">
<span class="font-card-icon">🏛️</span>
<span class="font-card-icon"><i data-lucide="building" class="icon-sm"></i></span>
<span class="font-card-name">Public Sans</span>
</div>
<div class="font-preview" style="font-family: 'Public Sans', sans-serif;">
@ -361,7 +360,7 @@
<!-- JetBrains Mono -->
<div class="font-card active" data-font="jetbrains-mono" onclick="selectFont('jetbrains-mono')">
<div class="font-card-header">
<span class="font-card-icon"></span>
<span class="font-card-icon"><i data-lucide="zap" class="icon-sm"></i></span>
<span class="font-card-name">JetBrains Mono</span>
</div>
<div class="font-preview" style="font-family: 'JetBrains Mono', monospace;">
@ -373,7 +372,7 @@
<!-- Cascadia Code -->
<div class="font-card" data-font="cascadia-code" onclick="selectFont('cascadia-code')">
<div class="font-card-header">
<span class="font-card-icon">🪟</span>
<span class="font-card-icon"><i data-lucide="square" class="icon-sm"></i></span>
<span class="font-card-name">Cascadia Code</span>
</div>
<div class="font-preview" style="font-family: 'Cascadia Code', monospace;">
@ -385,7 +384,7 @@
<!-- Source Code Pro -->
<div class="font-card" data-font="source-code-pro" onclick="selectFont('source-code-pro')">
<div class="font-card-header">
<span class="font-card-icon">🔧</span>
<span class="font-card-icon"><i data-lucide="wrench" class="icon-sm"></i></span>
<span class="font-card-name">Source Code Pro</span>
</div>
<div class="font-preview" style="font-family: 'Source Code Pro', monospace;">
@ -397,7 +396,7 @@
<!-- Sélecteur de taille de police -->
<div style="margin-top: var(--spacing-lg); padding-top: var(--spacing-lg); border-top: 1px solid var(--border-primary);">
<h3 style="font-size: 1rem; color: var(--text-primary); margin-bottom: var(--spacing-md);">📏 Taille de police</h3>
<h3 style="font-size: 1rem; color: var(--text-primary); margin-bottom: var(--spacing-md);"><i data-lucide="ruler" class="icon-sm"></i> Taille de police</h3>
<div class="font-size-selector">
<button class="font-size-option" data-size="small" onclick="selectFontSize('small')">
<span class="size-label">Petite</span>
@ -421,7 +420,7 @@
<!-- Section Éditeur -->
<div id="editor-section" class="settings-section" style="display: none;">
<h3 style="font-size: 1.1rem; color: var(--text-primary); margin-bottom: var(--spacing-lg);">⌨️ Mode d'édition</h3>
<h3 style="font-size: 1.1rem; color: var(--text-primary); margin-bottom: var(--spacing-lg);"><i data-lucide="keyboard" class="icon-sm"></i> Mode d'édition</h3>
<!-- Toggle Mode Vim -->
<div style="display: flex; align-items: center; justify-content: space-between; padding: var(--spacing-lg); background: var(--bg-secondary); border-radius: var(--radius-md); border: 1px solid var(--border-primary);">
@ -444,21 +443,21 @@
<div style="margin-top: var(--spacing-md); padding: var(--spacing-md); background: var(--bg-tertiary); border-radius: var(--radius-sm); border-left: 3px solid var(--accent-primary);">
<p style="font-size: 0.85rem; color: var(--text-muted); margin: 0;">
💡 <strong>Astuce :</strong> Le mode Vim sera appliqué immédiatement à l'éditeur actuel. Si vous ouvrez une nouvelle note, le mode restera activé.
<i data-lucide="lightbulb" class="icon-sm"></i> <strong>Astuce :</strong> Le mode Vim sera appliqué immédiatement à l'éditeur actuel. Si vous ouvrez une nouvelle note, le mode restera activé.
</p>
</div>
</div>
<!-- Section Autre (Langue) -->
<div id="other-section" class="settings-section" style="display: none;">
<h3 style="font-size: 1.1rem; color: var(--text-primary); margin-bottom: var(--spacing-lg);">🌍 Langue / Language</h3>
<h3 style="font-size: 1.1rem; color: var(--text-primary); margin-bottom: var(--spacing-lg);"><i data-lucide="languages" class="icon-sm"></i> Langue / Language</h3>
<div style="display: flex; flex-direction: column; gap: var(--spacing-md);">
<label class="language-option" style="display: flex; align-items: center; padding: var(--spacing-lg); background: var(--bg-secondary); border-radius: var(--radius-md); border: 2px solid var(--border-primary); cursor: pointer; transition: all 0.2s ease;">
<input type="radio" name="language" value="en" style="margin-right: var(--spacing-md); width: 20px; height: 20px; cursor: pointer;">
<div style="flex: 1;">
<div style="font-weight: 500; color: var(--text-primary); margin-bottom: 0.25rem; font-size: 1rem;">
🇬🇧 English
English
</div>
<div style="font-size: 0.85rem; color: var(--text-secondary);">
English interface
@ -470,7 +469,7 @@
<input type="radio" name="language" value="fr" style="margin-right: var(--spacing-md); width: 20px; height: 20px; cursor: pointer;">
<div style="flex: 1;">
<div style="font-weight: 500; color: var(--text-primary); margin-bottom: 0.25rem; font-size: 1rem;">
🇫🇷 Français
Français
</div>
<div style="font-size: 0.85rem; color: var(--text-secondary);">
Interface en français
@ -489,10 +488,10 @@
<div class="main-layout">
<aside id="sidebar">
<button class="sidebar-close-btn" onclick="toggleSidebar()" title="Fermer le menu">
<i data-lucide="x" class="icon-md"></i>
</button>
<section>
<h2 class="sidebar-section-title">🔍 Recherche</h2>
<h2 class="sidebar-section-title"><i data-lucide="search" class="icon-sm"></i> Recherche</h2>
<div id="search-results">
<!-- Les résultats de la recherche apparaîtront ici -->
</div>
@ -503,7 +502,7 @@
<section>
<div class="sidebar-section-header" data-section="favorites" onclick="toggleSidebarSection('favorites', event)" style="cursor: pointer; display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem 0;">
<span class="section-toggle expanded"></span>
<h2 class="sidebar-section-title" data-i18n="sidebar.favorites" style="margin: 0; flex: 1;"> Favoris</h2>
<h2 class="sidebar-section-title" data-i18n="sidebar.favorites" style="margin: 0; flex: 1;"><i data-lucide="star" class="icon-sm"></i> Favoris</h2>
</div>
<div class="sidebar-section-content" id="favorites-content" style="display: block;">
<div id="favorites-list"
@ -521,7 +520,7 @@
<section>
<div class="sidebar-section-header" data-section="daily-notes" onclick="toggleSidebarSection('daily-notes', event)" style="cursor: pointer; display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem 0;">
<span class="section-toggle expanded"></span>
<h2 class="sidebar-section-title" data-i18n="sidebar.daily" style="margin: 0; flex: 1;">📅 Daily Notes</h2>
<h2 class="sidebar-section-title" data-i18n="sidebar.daily" style="margin: 0; flex: 1;"><i data-lucide="calendar" class="icon-sm"></i> Daily Notes</h2>
</div>
<div class="sidebar-section-content" id="daily-notes-content" style="display: block;">
<div id="daily-calendar-container"
@ -546,12 +545,9 @@
<section>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--spacing-sm);">
<h2 class="sidebar-section-title" style="margin: 0;">📚 Notes</h2>
<h2 class="sidebar-section-title" style="margin: 0;"><i data-lucide="notebook" class="icon-sm"></i> Notes</h2>
<button id="toggle-selection-mode" onclick="toggleSelectionMode()" class="icon-button" title="Mode sélection">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="9 11 12 14 22 4"></polyline>
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path>
</svg>
<i data-lucide="check-square" style="width: 18px; height: 18px;"></i>
</button>
</div>
<div id="file-tree" hx-get="/api/tree" hx-trigger="load once delay:250ms" hx-swap="innerHTML">
@ -562,33 +558,26 @@
<!-- Bouton Nouveau dossier avant les paramètres -->
<button onclick="showNewFolderModal()" class="folder-create-btn sidebar-action-btn" data-i18n="fileTree.newFolder" title="Créer un nouveau dossier (Ctrl/Cmd+Shift+F)">
📁 Nouveau dossier
<i data-lucide="folder-plus" class="icon-sm"></i> Nouveau dossier
</button>
<!-- Boutons du bas de la sidebar -->
<div style="display: flex; gap: 0.5rem; align-items: stretch;">
<!-- Bouton Paramètres (thèmes) -->
<button id="theme-settings-btn" class="sidebar-action-btn" onclick="openThemeModal()" title="Ouvrir les paramètres (Ctrl/Cmd+,)" style="flex: 1; display: flex; align-items: center; gap: 0.5rem; padding: 0.6rem 0.8rem; font-size: 0.85rem;">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="3"></circle>
<path d="M12 1v6m0 6v6m-6-6h6m6 0h-6m-5.3-5.3l4.2 4.2m4.2 4.2l4.2 4.2m0-12.6l-4.2 4.2m-4.2 4.2L2.7 19.3"></path>
</svg>
<i data-lucide="settings" style="width: 16px; height: 16px;"></i>
<span data-i18n="settings.title">Paramètres</span>
</button>
<!-- Bouton À propos -->
<button
class="sidebar-action-btn"
<button
class="sidebar-action-btn"
title="À propos de PersoNotes"
hx-get="/api/about"
hx-target="#editor-container"
hx-swap="innerHTML"
style="display: flex; align-items: center; justify-content: center; padding: 0.6rem; min-width: auto; opacity: 0.7;">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="16" x2="12" y2="12"></line>
<line x1="12" y1="8" x2="12.01" y2="8"></line>
</svg>
<i data-lucide="info" style="width: 16px; height: 16px;"></i>
</button>
</div>
</aside>
@ -598,9 +587,7 @@
hx-trigger="load once"
hx-swap="innerHTML">
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 50vh; text-align: center; color: var(--text-secondary);">
<svg style="width: 64px; height: 64px; margin-bottom: 1rem; opacity: 0.5;" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
<i data-lucide="file-text" style="width: 64px; height: 64px; margin-bottom: 1rem; opacity: 0.5;"></i>
<p style="font-size: 1.1rem; margin: 0;">Chargement...</p>
</div>
</div>
@ -695,5 +682,33 @@
}
});
</script>
<!-- Lucide Icons Initialization & htmx Integration -->
<script>
// Initialize Lucide icons on page load
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
// Reinitialize Lucide icons after htmx swaps
document.body.addEventListener('htmx:afterSwap', function(event) {
if (typeof lucide !== 'undefined') {
lucide.createIcons({
nameAttr: 'data-lucide'
});
}
});
// Reinitialize after out-of-band swaps (file-tree updates)
document.body.addEventListener('htmx:oobAfterSwap', function(event) {
if (typeof lucide !== 'undefined') {
lucide.createIcons({
nameAttr: 'data-lucide'
});
}
});
</script>
</body>
</html>

View File

@ -12,7 +12,7 @@
hx-target="#editor-container"
hx-swap="innerHTML"
hx-push-url="true">
<div class="search-result-icon">📄</div>
<div class="search-result-icon"><i data-lucide="file-text" class="icon-md"></i></div>
<div class="search-result-content">
<div class="search-result-header">
<span class="search-result-title">{{.Title}}</span>
@ -40,14 +40,14 @@
</ul>
{{else}}
<div class="search-no-results">
<div class="search-no-results-icon">🔍</div>
<div class="search-no-results-icon"><i data-lucide="search" class="icon-lg"></i></div>
<p class="search-no-results-text">Aucun résultat pour « <strong>{{.Query}}</strong> »</p>
<p class="search-no-results-hint">Essayez d'autres mots-clés ou utilisez les filtres</p>
</div>
{{end}}
{{else}}
<div class="search-help">
<p class="search-help-title">💡 Recherche avancée</p>
<p class="search-help-title"><i data-lucide="lightbulb" class="icon-sm"></i> Recherche avancée</p>
<p class="search-help-text">Saisissez des mots-clés pour rechercher dans vos notes</p>
<div class="search-help-examples">
<div class="search-help-example">