Merge pull request 'gui-v2' (#1) from gui-v2 into main

Reviewed-on: #1
This commit is contained in:
2025-12-24 16:57:08 +01:00
53 changed files with 8222 additions and 305 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-1.md)",
"Bash(/home/mathieu/git/project-notes/notes/test-delete-2.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(/home/mathieu/git/project-notes/notes/test-delete-folder/test.md)",
"Bash(npm install)" "Bash(npm install)",
"Bash(go get:*)",
"Bash(ls:*)"
], ],
"deny": [], "deny": [],
"ask": [] "ask": []

377
API.md
View File

@ -6,6 +6,7 @@ Base URL: `http://localhost:8080/api/v1`
## Table des matières ## Table des matières
- [Vue d'ensemble](#vue-densemble) - [Vue d'ensemble](#vue-densemble)
- [Commandes CLI](#commandes-cli)
- [Authentification](#authentification) - [Authentification](#authentification)
- [Formats de données](#formats-de-données) - [Formats de données](#formats-de-données)
- [Endpoints](#endpoints) - [Endpoints](#endpoints)
@ -13,6 +14,9 @@ Base URL: `http://localhost:8080/api/v1`
- [Récupérer une note](#récupérer-une-note) - [Récupérer une note](#récupérer-une-note)
- [Créer/Mettre à jour une note](#créermettre-à-jour-une-note) - [Créer/Mettre à jour une note](#créermettre-à-jour-une-note)
- [Supprimer une note](#supprimer-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) - [Codes de statut HTTP](#codes-de-statut-http)
- [Exemples d'utilisation](#exemples-dutilisation) - [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 ## Authentification
**Version actuelle : Aucune authentification requise** **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 ## Codes de statut HTTP
| Code | Signification | Description | | Code | Signification | Description |
@ -556,13 +930,14 @@ echo "$STATS" | jq -r '.notes[].tags[]' | sort | uniq -c | sort -rn | head -5
## Changelog ## Changelog
### v1 (2025-11-10) ### v1 (2025-11-13)
- ✨ Première version de l'API REST - ✨ Première version de l'API REST
- ✅ Endpoints: LIST, GET, PUT, DELETE - ✅ Endpoints: LIST, GET, PUT, DELETE
- ✅ Content negotiation JSON/Markdown - ✅ Content negotiation JSON/Markdown
- ✅ Support sous-dossiers - ✅ Support sous-dossiers
- ✅ Gestion automatique du front matter - ✅ Gestion automatique du front matter
- ✅ Ré-indexation automatique - ✅ 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**: **Key Features**:
- **Daily Notes**: Quick daily journaling with interactive calendar, keyboard shortcuts (Ctrl/Cmd+D), and structured templates - **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 - **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 - **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 - **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) - **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 - **Font Customization**: 8 fonts (JetBrains Mono, Fira Code, Inter, etc.) with 4 size options
- **Keyboard Shortcuts**: 10+ global shortcuts for navigation, editing, and productivity - **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. **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) ### 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. - **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. - **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. - **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 - `handler.go` - Main HTML endpoints for the web interface
- `rest_handler.go` - REST API endpoints (v1) - `rest_handler.go` - REST API endpoints (v1)
- `daily_notes.go` - Daily note creation and calendar functionality - `daily_notes.go` - Daily note creation and calendar functionality
- `favorites.go` - Favorites management (star/unstar notes and folders) - `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: The server (`cmd/server/main.go`) coordinates these components:
1. Loads initial index from notes directory 1. Loads initial index from notes directory
2. Starts filesystem watcher for automatic re-indexing 2. Loads translations from `locales/` directory (JSON files for each language)
3. Pre-parses HTML templates from `templates/` 3. Starts filesystem watcher for automatic re-indexing
4. Serves routes: 4. Pre-parses HTML templates from `templates/`
5. Serves routes:
- `/` (main page) - `/` (main page)
- `/api/v1/notes` and `/api/v1/notes/*` (REST API - JSON responses) - `/api/v1/notes` and `/api/v1/notes/*` (REST API - JSON responses)
- `/api/i18n/{lang}` (Translation JSON endpoint)
- `/api/search` (HTML search results) - `/api/search` (HTML search results)
- `/api/notes/*` (HTML editor for notes) - `/api/notes/*` (HTML editor for notes)
- `/api/tree` (HTML file tree) - `/api/tree` (HTML file tree)
- `/api/folders/create` (Folder management) - `/api/folders/create` (Folder management)
- `/api/files/move` (File/folder moving) - `/api/files/move` (File/folder moving)
- `/api/home` (Home page) - `/api/home` (Home page)
- `/api/daily-notes/*` (Daily note creation and calendar) - `/api/about` (About page)
- `/api/favorites/*` (Favorites management) - `/api/daily` and `/api/daily/*` (Daily note creation and calendar)
5. Handles static files from `static/` directory - `/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 ### Frontend
@ -69,9 +81,14 @@ frontend/src/
├── file-tree.js # Drag-and-drop file organization ├── file-tree.js # Drag-and-drop file organization
├── favorites.js # Favorites system (star/unstar functionality) ├── favorites.js # Favorites system (star/unstar functionality)
├── daily-notes.js # Daily notes creation and calendar widget ├── 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 ├── keyboard-shortcuts.js # Global keyboard shortcuts management
├── theme-manager.js # Theme switching and persistence ├── theme-manager.js # Theme switching and persistence
├── font-manager.js # Font selection and size management ├── 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 └── 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. 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 ### Note Format
Notes have YAML front matter with these fields: Notes have YAML front matter with these fields:
@ -372,6 +416,7 @@ go mod tidy
Key backend dependencies: Key backend dependencies:
- `github.com/fsnotify/fsnotify` - Filesystem watcher - `github.com/fsnotify/fsnotify` - Filesystem watcher
- `gopkg.in/yaml.v3` - YAML parsing for front matter - `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 ### 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. 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 ### Vim Mode
**Implementation**: `frontend/src/vim-mode-manager.js` using `@replit/codemirror-vim` **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) - CORS not configured (same-origin only)
- No rate limiting (add middleware if needed) - 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 ### 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. 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 │ │ ├── handler.go # HTTP handlers for CRUD operations
│ │ ├── rest_handler.go # REST API v1 endpoints │ │ ├── rest_handler.go # REST API v1 endpoints
│ │ ├── daily_notes.go # Daily notes functionality │ │ ├── daily_notes.go # Daily notes functionality
│ │ ── favorites.go # Favorites management │ │ ── favorites.go # Favorites management
│ │ └── public.go # Public notes sharing
│ ├── indexer/ │ ├── indexer/
│ │ ├── indexer.go # Note indexing and search │ │ ├── indexer.go # Note indexing and search
│ │ └── indexer_test.go # Indexer tests │ │ └── indexer_test.go # Indexer tests
│ ├── i18n/
│ │ ├── i18n.go # Internationalization engine
│ │ └── i18n_test.go # i18n tests
│ └── watcher/ │ └── watcher/
│ └── watcher.go # Filesystem watcher with fsnotify │ └── watcher.go # Filesystem watcher with fsnotify
├── frontend/ # Frontend build system ├── frontend/ # Frontend build system
@ -767,9 +882,15 @@ personotes/
│ │ ├── file-tree.js # Drag-and-drop file tree │ │ ├── file-tree.js # Drag-and-drop file tree
│ │ ├── favorites.js # Favorites system │ │ ├── favorites.js # Favorites system
│ │ ├── daily-notes.js # Daily notes and calendar widget │ │ ├── daily-notes.js # Daily notes and calendar widget
│ │ ├── public-toggle.js # Public/private status toggle
│ │ ├── keyboard-shortcuts.js # Global keyboard shortcuts │ │ ├── keyboard-shortcuts.js # Global keyboard shortcuts
│ │ ├── theme-manager.js # Theme switching │ │ ├── theme-manager.js # Theme switching
│ │ ├── font-manager.js # Font customization │ │ ├── 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 │ │ └── ui.js # Sidebar toggle
│ ├── package.json # NPM dependencies │ ├── package.json # NPM dependencies
│ ├── package-lock.json │ ├── package-lock.json
@ -785,11 +906,20 @@ personotes/
│ ├── file-tree.html # File tree sidebar │ ├── file-tree.html # File tree sidebar
│ ├── search-results.html # Search results │ ├── search-results.html # Search results
│ └── new-note-prompt.html # New note modal │ └── 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 ├── notes/ # Note storage directory
│ ├── *.md # Markdown files with YAML front matter │ ├── *.md # Markdown files with YAML front matter
│ ├── daily/ # Daily notes (YYYY-MM-DD.md) │ ├── daily/ # Daily notes (YYYY-MM-DD.md)
│ ├── .favorites.json # Favorites list (auto-generated) │ ├── .favorites.json # Favorites list (auto-generated)
│ ├── .public.json # Public notes list (auto-generated)
│ └── daily-note-template.md # Optional daily note template │ └── 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 ├── docs/ # Documentation
│ ├── KEYBOARD_SHORTCUTS.md # Keyboard shortcuts reference │ ├── KEYBOARD_SHORTCUTS.md # Keyboard shortcuts reference
│ ├── DAILY_NOTES.md # Daily notes guide │ ├── DAILY_NOTES.md # Daily notes guide
@ -798,6 +928,8 @@ personotes/
├── go.mod # Go dependencies ├── go.mod # Go dependencies
├── go.sum ├── go.sum
├── API.md # REST API documentation ├── API.md # REST API documentation
├── I18N_IMPLEMENTATION.md # i18n implementation guide
├── PUBLIC_NOTES.md # Public notes documentation and security guide
└── CLAUDE.md # This file └── CLAUDE.md # This file
``` ```
@ -809,8 +941,10 @@ personotes/
- `internal/api/rest_handler.go` - REST API v1 endpoints - `internal/api/rest_handler.go` - REST API v1 endpoints
- `internal/api/daily_notes.go` - Daily notes and calendar functionality - `internal/api/daily_notes.go` - Daily notes and calendar functionality
- `internal/api/favorites.go` - Favorites management - `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/indexer/indexer.go` - Search and indexing logic
- `internal/watcher/watcher.go` - Filesystem monitoring - `internal/watcher/watcher.go` - Filesystem monitoring
- `internal/i18n/i18n.go` - Internationalization engine
**Frontend Development**: **Frontend Development**:
- `frontend/src/editor.js` - CodeMirror editor, preview, slash commands - `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/file-tree.js` - File tree interactions and drag-and-drop
- `frontend/src/favorites.js` - Favorites system - `frontend/src/favorites.js` - Favorites system
- `frontend/src/daily-notes.js` - Daily notes creation and calendar widget - `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/keyboard-shortcuts.js` - Global keyboard shortcuts
- `frontend/src/theme-manager.js` - Theme switching logic - `frontend/src/theme-manager.js` - Theme switching logic
- `frontend/src/font-manager.js` - Font customization 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) - `frontend/src/ui.js` - UI utilities (sidebar toggle)
- `static/theme.css` - Styling and theming (8 themes) - `static/theme.css` - Styling and theming (8 themes)
- `templates/*.html` - HTML templates (Go template syntax) - `templates/*.html` - HTML templates (Go template syntax)
- `locales/*.json` - Translation files (en.json, fr.json)
**Configuration**: **Configuration**:
- `frontend/vite.config.js` - Frontend build 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 ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"flag" "flag"
"fmt" "fmt"
@ -10,6 +11,7 @@ import (
"net/http" "net/http"
"os" "os"
"os/signal" "os/signal"
"path/filepath"
"syscall" "syscall"
"time" "time"
@ -20,21 +22,82 @@ import (
) )
func main() { 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") addr := flag.String("addr", ":8080", "Adresse d ecoute HTTP")
notesDir := flag.String("notes-dir", "./notes", "Repertoire contenant les notes Markdown") notesDir := flag.String("notes-dir", "./notes", "Repertoire contenant les notes Markdown")
flag.Parse() flag.Parse()
logger := log.New(os.Stdout, "[server] ", log.LstdFlags) 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) ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop() defer stop()
if err := ensureDir(*notesDir); err != nil { if err := ensureDir(notesDir); err != nil {
logger.Fatalf("repertoire notes invalide: %v", err) logger.Fatalf("repertoire notes invalide: %v", err)
} }
idx := indexer.New() 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) logger.Fatalf("echec de l indexation initiale: %v", err)
} }
@ -45,7 +108,7 @@ func main() {
} }
logger.Printf("traductions chargees: %v", translator.GetAvailableLanguages()) 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 { if err != nil {
logger.Fatalf("echec du watcher: %v", err) 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("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static"))))
mux.Handle("/frontend/", http.StripPrefix("/frontend/", http.FileServer(http.Dir("./frontend")))) 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) { mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" { if r.URL.Path != "/" {
http.NotFound(w, r) 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/i18n/", apiHandler) // I18n translations
mux.Handle("/api/v1/notes", apiHandler) // REST API v1 mux.Handle("/api/v1/notes", apiHandler) // REST API v1
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/folder/", apiHandler) // Folder view
mux.Handle("/api/notes/", apiHandler) mux.Handle("/api/notes/", apiHandler)
mux.Handle("/api/tree", 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{ srv := &http.Server{
Addr: *addr, Addr: addr,
Handler: mux, Handler: mux,
ReadTimeout: 15 * time.Second, ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second, WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second, IdleTimeout: 60 * time.Second,
} }
logger.Printf("demarrage du serveur sur %s", *addr) logger.Printf("demarrage du serveur sur %s", addr)
go func() { go func() {
<-ctx.Done() <-ctx.Done()

360
docs/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.

View File

@ -133,13 +133,13 @@ class FavoritesManager {
attachFavoriteButtons() { attachFavoriteButtons() {
debug('attachFavoriteButtons: Début...'); 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 // Ajouter des boutons étoile aux éléments du file tree
this.getFavoritesPaths().then(favoritePaths => { this.getFavoritesPaths().then(favoritePaths => {
debug('Chemins favoris:', 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 // Dossiers
const folderHeaders = document.querySelectorAll('.folder-header'); const folderHeaders = document.querySelectorAll('.folder-header');
debug('Nombre de folder-header trouvés:', folderHeaders.length); debug('Nombre de folder-header trouvés:', folderHeaders.length);
@ -153,7 +153,7 @@ class FavoritesManager {
if (path) { if (path) {
const button = document.createElement('button'); const button = document.createElement('button');
button.className = 'add-to-favorites'; button.className = 'add-to-favorites';
button.innerHTML = ''; button.innerHTML = '<i data-lucide="star" class="icon-sm"></i>';
button.title = 'Ajouter aux favoris'; button.title = 'Ajouter aux favoris';
// Extraire le nom avant d'ajouter le bouton // Extraire le nom avant d'ajouter le bouton
@ -191,11 +191,11 @@ class FavoritesManager {
if (path) { if (path) {
const button = document.createElement('button'); const button = document.createElement('button');
button.className = 'add-to-favorites'; button.className = 'add-to-favorites';
button.innerHTML = ''; button.innerHTML = '<i data-lucide="star" class="icon-sm"></i>';
button.title = 'Ajouter aux favoris'; button.title = 'Ajouter aux favoris';
// Extraire le nom avant d'ajouter le bouton // 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) => { button.onclick = (e) => {
e.preventDefault(); e.preventDefault();
@ -220,6 +220,11 @@ class FavoritesManager {
}); });
debug('attachFavoriteButtons: Terminé'); 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() { init() {
this.setupEventListeners(); this.setupEventListeners();
// Restaurer l'état des dossiers au démarrage
setTimeout(() => this.restoreFolderStates(), 500);
debug('FileTree initialized with event delegation'); debug('FileTree initialized with event delegation');
} }
@ -67,17 +70,60 @@ class FileTree {
const children = folderItem.querySelector('.folder-children'); const children = folderItem.querySelector('.folder-children');
const toggle = header.querySelector('.folder-toggle'); const toggle = header.querySelector('.folder-toggle');
const icon = header.querySelector('.folder-icon'); const icon = header.querySelector('.folder-icon');
const folderPath = folderItem.getAttribute('data-path');
if (children.style.display === 'none') { if (children.style.display === 'none') {
// Ouvrir le dossier // Ouvrir le dossier
children.style.display = 'block'; children.style.display = 'block';
toggle.classList.add('expanded'); toggle.classList.add('expanded');
icon.textContent = '📂'; icon.innerHTML = '<i data-lucide="folder-open" class="icon-sm"></i>';
this.saveFolderState(folderPath, true);
} else { } else {
// Fermer le dossier // Fermer le dossier
children.style.display = 'none'; children.style.display = 'none';
toggle.classList.remove('expanded'); 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 // Vérifier si le swap concerne le file-tree
const target = event.detail?.target; const target = event.detail?.target;
if (target && (target.id === 'file-tree' || target.closest('#file-tree'))) { 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(); this.updateDraggableAttributes();
setTimeout(() => this.restoreFolderStates(), 50);
} }
}); });
@ -169,8 +216,9 @@ class FileTree {
const target = event.detail?.target; const target = event.detail?.target;
// Ignorer les swaps de statut (auto-save-status, save-status) // Ignorer les swaps de statut (auto-save-status, save-status)
if (target && target.id === 'file-tree') { if (target && target.id === 'file-tree') {
debug('FileTree: oobAfterSwap detected, updating attributes...'); debug('FileTree: oobAfterSwap detected, updating attributes and restoring folder states...');
this.updateDraggableAttributes(); this.updateDraggableAttributes();
setTimeout(() => this.restoreFolderStates(), 50);
} }
}); });
@ -181,6 +229,7 @@ class FileTree {
setTimeout(() => { setTimeout(() => {
this.setupEventListeners(); this.setupEventListeners();
this.updateDraggableAttributes(); this.updateDraggableAttributes();
this.restoreFolderStates();
}, 50); }, 50);
}); });
} }
@ -678,7 +727,7 @@ class SelectionManager {
const checkbox = document.querySelector(`.selection-checkbox[data-path="${path}"]`); const checkbox = document.querySelector(`.selection-checkbox[data-path="${path}"]`);
const isDir = checkbox?.dataset.isDir === 'true'; 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); ul.appendChild(li);
}); });

View File

@ -231,8 +231,10 @@ class I18n {
// Create singleton instance // Create singleton instance
export const i18n = new I18n(); export const i18n = new I18n();
// Export convenience function // Export convenience functions
export const t = (key, args) => i18n.t(key, args); export const t = (key, args) => i18n.t(key, args);
export const loadTranslations = () => i18n.loadTranslations();
export const translatePage = () => i18n.translatePage();
// Initialize on import // Initialize on import
i18n.init().then(() => { i18n.init().then(() => {

View File

@ -181,12 +181,12 @@ class LanguageManager {
// Header buttons // Header buttons
const homeButton = document.querySelector('button[hx-get="/api/home"]'); const homeButton = document.querySelector('button[hx-get="/api/home"]');
if (homeButton && !homeButton.hasAttribute('data-i18n')) { 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()"]'); const newNoteButton = document.querySelector('header button[onclick="showNewNoteModal()"]');
if (newNoteButton && !newNoteButton.hasAttribute('data-i18n')) { 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 // Search placeholder
@ -199,7 +199,7 @@ class LanguageManager {
const newNoteModal = document.getElementById('new-note-modal'); const newNoteModal = document.getElementById('new-note-modal');
if (newNoteModal) { if (newNoteModal) {
const title = newNoteModal.querySelector('h2'); 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"]'); const label = newNoteModal.querySelector('label[for="note-name"]');
if (label) label.textContent = t('newNoteModal.label'); if (label) label.textContent = t('newNoteModal.label');
@ -218,7 +218,7 @@ class LanguageManager {
const newFolderModal = document.getElementById('new-folder-modal'); const newFolderModal = document.getElementById('new-folder-modal');
if (newFolderModal) { if (newFolderModal) {
const title = newFolderModal.querySelector('h2'); 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"]'); const label = newFolderModal.querySelector('label[for="folder-name"]');
if (label) label.textContent = t('newFolderModal.label'); if (label) label.textContent = t('newFolderModal.label');
@ -256,16 +256,16 @@ class LanguageManager {
// Theme modal // Theme modal
const modalTitle = document.querySelector('.theme-modal-content h2'); const modalTitle = document.querySelector('.theme-modal-content h2');
if (modalTitle) { if (modalTitle) {
modalTitle.textContent = `⚙️ ${t('settings.title')}`; modalTitle.innerHTML = `<i data-lucide="settings" class="icon-sm"></i> ${t('settings.title')}`;
} }
// Translate tabs // Translate tabs
const tabs = document.querySelectorAll('.settings-tab'); const tabs = document.querySelectorAll('.settings-tab');
if (tabs.length >= 4) { if (tabs.length >= 4) {
tabs[0].innerHTML = `🎨 ${t('tabs.themes')}`; tabs[0].innerHTML = `<i data-lucide="palette" class="icon-sm"></i> ${t('tabs.themes')}`;
tabs[1].innerHTML = `🔤 ${t('tabs.fonts')}`; tabs[1].innerHTML = `<i data-lucide="type" class="icon-sm"></i> ${t('tabs.fonts')}`;
tabs[2].innerHTML = `⌨️ ${t('tabs.shortcuts')}`; tabs[2].innerHTML = `<i data-lucide="keyboard" class="icon-sm"></i> ${t('tabs.shortcuts')}`;
tabs[3].innerHTML = `⚙️ ${t('tabs.other')}`; tabs[3].innerHTML = `<i data-lucide="settings" class="icon-sm"></i> ${t('tabs.other')}`;
} }
// Translate close button in settings // Translate close button in settings
@ -281,20 +281,20 @@ class LanguageManager {
if (langSection) { if (langSection) {
const heading = langSection.querySelector('h3'); const heading = langSection.querySelector('h3');
if (heading) { if (heading) {
heading.textContent = `🌍 ${t('languages.title')}`; heading.innerHTML = `<i data-lucide="languages" class="icon-sm"></i> ${t('languages.title')}`;
} }
} }
// Sidebar sections // Sidebar sections
const searchSectionTitle = document.querySelector('.sidebar-section-title'); const searchSectionTitle = document.querySelector('.sidebar-section-title');
if (searchSectionTitle && searchSectionTitle.textContent.includes('🔍')) { if (searchSectionTitle && (searchSectionTitle.textContent.includes('🔍') || searchSectionTitle.querySelector('[data-lucide="search"]'))) {
searchSectionTitle.textContent = `🔍 ${t('search.title') || 'Recherche'}`; searchSectionTitle.innerHTML = `<i data-lucide="search" class="icon-sm"></i> ${t('search.title') || 'Recherche'}`;
} }
// Sidebar "Nouveau dossier" button // Sidebar "Nouveau dossier" button
const newFolderBtn = document.querySelector('.folder-create-btn'); const newFolderBtn = document.querySelector('.folder-create-btn');
if (newFolderBtn && !newFolderBtn.hasAttribute('data-i18n')) { 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 // Sidebar "Paramètres" button span

View File

@ -309,7 +309,7 @@ class LinkInserter {
this.results = []; this.results = [];
this.resultsContainer.innerHTML = ` this.resultsContainer.innerHTML = `
<div class="link-inserter-no-results"> <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> <p>Aucune note trouvée pour « <strong>${this.escapeHtml(query)}</strong> »</p>
</div> </div>
`; `;
@ -318,7 +318,7 @@ class LinkInserter {
showError() { showError() {
this.resultsContainer.innerHTML = ` this.resultsContainer.innerHTML = `
<div class="link-inserter-error"> <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> <p>Erreur lors de la recherche</p>
</div> </div>
`; `;

View File

@ -12,3 +12,7 @@ import './vim-mode-manager.js';
import './favorites.js'; import './favorites.js';
import './sidebar-sections.js'; import './sidebar-sections.js';
import './keyboard-shortcuts.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-body">
<div class="search-modal-results"> <div class="search-modal-results">
<div class="search-modal-help"> <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-items">
<div class="search-modal-help-item"> <div class="search-modal-help-item">
<code>tag:projet</code> <code>tag:projet</code>
@ -289,7 +289,7 @@ class SearchModal {
this.results = []; this.results = [];
this.resultsContainer.innerHTML = ` this.resultsContainer.innerHTML = `
<div class="search-modal-help"> <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-items">
<div class="search-modal-help-item"> <div class="search-modal-help-item">
<code>tag:projet</code> <code>tag:projet</code>
@ -325,7 +325,7 @@ class SearchModal {
this.results = []; this.results = [];
this.resultsContainer.innerHTML = ` this.resultsContainer.innerHTML = `
<div class="search-modal-no-results"> <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-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> <p class="search-modal-no-results-hint">Essayez d'autres mots-clés ou utilisez les filtres</p>
</div> </div>
@ -335,7 +335,7 @@ class SearchModal {
showError() { showError() {
this.resultsContainer.innerHTML = ` this.resultsContainer.innerHTML = `
<div class="search-modal-error"> <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> <p>Une erreur s'est produite lors de la recherche</p>
</div> </div>
`; `;

View File

@ -9,49 +9,49 @@ class ThemeManager {
{ {
id: 'material-dark', id: 'material-dark',
name: 'Material Dark', name: 'Material Dark',
icon: '🌙', icon: 'moon',
description: 'Thème professionnel inspiré de Material Design' description: 'Thème professionnel inspiré de Material Design'
}, },
{ {
id: 'monokai-dark', id: 'monokai-dark',
name: 'Monokai Dark', name: 'Monokai Dark',
icon: '🎨', icon: 'palette',
description: 'Palette Monokai classique pour les développeurs' description: 'Palette Monokai classique pour les développeurs'
}, },
{ {
id: 'dracula', id: 'dracula',
name: 'Dracula', name: 'Dracula',
icon: '🧛', icon: 'moon-star',
description: 'Thème sombre élégant avec des accents violets et cyan' description: 'Thème sombre élégant avec des accents violets et cyan'
}, },
{ {
id: 'one-dark', id: 'one-dark',
name: 'One Dark', name: 'One Dark',
icon: '', icon: 'zap',
description: 'Thème populaire d\'Atom avec des couleurs douces' description: 'Thème populaire d\'Atom avec des couleurs douces'
}, },
{ {
id: 'solarized-dark', id: 'solarized-dark',
name: 'Solarized Dark', name: 'Solarized Dark',
icon: '☀️', icon: 'sun',
description: 'Palette scientifiquement optimisée pour réduire la fatigue oculaire' description: 'Palette scientifiquement optimisée pour réduire la fatigue oculaire'
}, },
{ {
id: 'nord', id: 'nord',
name: 'Nord', name: 'Nord',
icon: '❄️', icon: 'snowflake',
description: 'Palette arctique apaisante avec des tons bleus froids' description: 'Palette arctique apaisante avec des tons bleus froids'
}, },
{ {
id: 'catppuccin', id: 'catppuccin',
name: 'Catppuccin', name: 'Catppuccin',
icon: '🌸', icon: 'flower-2',
description: 'Thème pastel doux et chaleureux avec des accents roses et bleus' description: 'Thème pastel doux et chaleureux avec des accents roses et bleus'
}, },
{ {
id: 'everforest', id: 'everforest',
name: 'Everforest', name: 'Everforest',
icon: '🌲', icon: 'tree-pine',
description: 'Palette naturelle inspirée de la forêt avec des tons verts et beiges' 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 ( require (
github.com/fsnotify/fsnotify v1.7.0 github.com/fsnotify/fsnotify v1.7.0
github.com/yuin/goldmark v1.7.13
gopkg.in/yaml.v3 v3.0.1 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 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 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 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 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 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 // Legacy/HTML endpoints
if strings.HasPrefix(path, "/api/search") { if strings.HasPrefix(path, "/api/search") {
h.handleSearch(w, r) h.handleSearch(w, r)
@ -295,12 +305,14 @@ func (h *Handler) handleHome(w http.ResponseWriter, r *http.Request) {
Filename string Filename string
Content string Content string
IsHome bool IsHome bool
IsPublic bool
Backlinks []BacklinkInfo Backlinks []BacklinkInfo
Breadcrumb template.HTML Breadcrumb template.HTML
}{ }{
Filename: "🏠 Accueil - Index", Filename: "🏠 Accueil - Index",
Content: content, Content: content,
IsHome: true, IsHome: true,
IsPublic: false,
Backlinks: nil, // Pas de backlinks pour la page d'accueil Backlinks: nil, // Pas de backlinks pour la page d'accueil
Breadcrumb: h.generateBreadcrumb(""), Breadcrumb: h.generateBreadcrumb(""),
} }
@ -351,7 +363,10 @@ func (h *Handler) generateHomeMarkdown(r *http.Request) string {
// Section des favoris (après les tags) // Section des favoris (après les tags)
h.generateFavoritesSection(&sb, r) 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) h.generateRecentNotesSection(&sb, r)
// Section de toutes les notes avec accordéon // 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\">\n")
sb.WriteString(" <div class=\"home-section-header\" onclick=\"toggleFolder('tags')\">\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>\n")
sb.WriteString(" <div class=\"home-section-content\" id=\"folder-tags\">\n") sb.WriteString(" <div class=\"home-section-content\" id=\"folder-tags\">\n")
sb.WriteString(" <div class=\"tags-cloud\">\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\">\n")
sb.WriteString(" <div class=\"home-section-header\" onclick=\"toggleFolder('favorites')\">\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>\n")
sb.WriteString(" <div class=\"home-section-content\" id=\"folder-favorites\">\n") sb.WriteString(" <div class=\"home-section-content\" id=\"folder-favorites\">\n")
sb.WriteString(" <div class=\"note-tree favorites-tree\">\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 // Dossier - avec accordéon
sb.WriteString(fmt.Sprintf(" <div class=\"folder indent-level-1\">\n")) 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(" <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(" <strong>%s</strong>\n", fav.Title))
sb.WriteString(fmt.Sprintf(" </div>\n")) sb.WriteString(fmt.Sprintf(" </div>\n"))
sb.WriteString(fmt.Sprintf(" <div class=\"folder-content\" id=\"folder-%s\">\n", safeID)) 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") 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 // generateRecentNotesSection génère la section des notes récemment modifiées
func (h *Handler) generateRecentNotesSection(sb *strings.Builder, r *http.Request) { func (h *Handler) generateRecentNotesSection(sb *strings.Builder, r *http.Request) {
recentDocs := h.idx.GetRecentDocuments(5) 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(" <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-title\">%s</div>\n", doc.Title))
sb.WriteString(fmt.Sprintf(" <div class=\"recent-note-meta\">\n")) 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 { if len(doc.Tags) > 0 {
sb.WriteString(fmt.Sprintf(" <span class=\"recent-note-tags\">")) sb.WriteString(fmt.Sprintf(" <span class=\"recent-note-tags\">"))
for i, tag := range doc.Tags { for i, tag := range doc.Tags {
@ -523,7 +576,7 @@ func (h *Handler) generateFavoriteFolderContent(sb *strings.Builder, folderPath
// Sous-dossier // Sous-dossier
sb.WriteString(fmt.Sprintf("%s<div class=\"folder %s\">\n", indent, indentClass)) 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 <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 <strong>%s</strong>\n", indent, name))
sb.WriteString(fmt.Sprintf("%s </div>\n", indent)) sb.WriteString(fmt.Sprintf("%s </div>\n", indent))
sb.WriteString(fmt.Sprintf("%s <div class=\"folder-content\" id=\"folder-%s\">\n", indent, safeID)) 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) 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 %s\">\n", indent, indentClass))
sb.WriteString(fmt.Sprintf("%s <div class=\"folder-header\" onclick=\"toggleFolder('%s')\">\n", indent, safeID)) 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 <strong>%s</strong>\n", indent, node.Name))
sb.WriteString(fmt.Sprintf("%s </div>\n", indent)) sb.WriteString(fmt.Sprintf("%s </div>\n", indent))
sb.WriteString(fmt.Sprintf("%s <div class=\"folder-content\" id=\"folder-%s\">\n", indent, safeID)) 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 Filename string
Content string Content string
IsHome bool IsHome bool
IsPublic bool
Backlinks []BacklinkInfo Backlinks []BacklinkInfo
}{ }{
Filename: filename, Filename: filename,
Content: initialContent, Content: initialContent,
IsHome: false, 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) 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 Filename string
Content string Content string
IsHome bool IsHome bool
IsPublic bool
Backlinks []BacklinkInfo Backlinks []BacklinkInfo
Breadcrumb template.HTML Breadcrumb template.HTML
}{ }{
Filename: filename, Filename: filename,
Content: string(content), Content: string(content),
IsHome: false, IsHome: false,
IsPublic: h.isPublic(filename),
Backlinks: backlinkData, Backlinks: backlinkData,
Breadcrumb: h.generateBreadcrumb(filename), Breadcrumb: h.generateBreadcrumb(filename),
} }
@ -1347,15 +1404,17 @@ func (h *Handler) handleFolderView(w http.ResponseWriter, r *http.Request) {
// Utiliser le template editor.html // Utiliser le template editor.html
data := struct { data := struct {
Filename string Filename string
Content string Content string
IsHome bool IsHome bool
Backlinks []BacklinkInfo IsPublic bool
Backlinks []BacklinkInfo
Breadcrumb template.HTML Breadcrumb template.HTML
}{ }{
Filename: cleanPath, Filename: cleanPath,
Content: content, Content: content,
IsHome: true, // Pas d'édition pour une vue de dossier IsHome: true, // Pas d'édition pour une vue de dossier
IsPublic: false,
Backlinks: nil, Backlinks: nil,
Breadcrumb: h.generateBreadcrumb(cleanPath), 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 // generateBreadcrumb génère un fil d'Ariane HTML cliquable
func (h *Handler) generateBreadcrumb(path string) template.HTML { func (h *Handler) generateBreadcrumb(path string) template.HTML {
if path == "" { 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), "/") parts := strings.Split(filepath.ToSlash(path), "/")
@ -1379,7 +1438,7 @@ func (h *Handler) generateBreadcrumb(path string) template.HTML {
sb.WriteString(`<span class="breadcrumb">`) sb.WriteString(`<span class="breadcrumb">`)
// Lien racine // 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 // Construire les liens pour chaque partie
currentPath := "" 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", "nextMonth": "Next month",
"noNote": "No note", "noNote": "No note",
"noteOf": "Note of" "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", "nextMonth": "Mois suivant",
"noNote": "Pas de note", "noNote": "Pas de note",
"noteOf": "Note du" "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,68 @@
"added_at": "2025-11-11T13:55:49.371541279+01:00", "added_at": "2025-11-11T13:55:49.371541279+01:00",
"order": 0 "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", "path": "ideas/client-feedback.md",
"is_dir": false, "is_dir": false,
"title": "client-feedback", "title": "client-feedback",
"added_at": "2025-11-11T14:22:16.497953232+01:00", "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 "order": 2
}, },
{ {
"path": "ideas/collaboration.md", "path": "archive",
"is_dir": false, "is_dir": true,
"title": "collaboration", "title": "archive",
"added_at": "2025-11-11T14:22:18.012032002+01:00", "added_at": "2025-12-24T15:48:42.323990909+01:00",
"order": 3 "order": 3
}, },
{ {
"path": "ideas/mobile-app.md", "path": "archive/ai-assistant.md",
"is_dir": false, "is_dir": false,
"title": "mobile-app", "title": "ai-assistant",
"added_at": "2025-11-11T14:22:19.048311608+01:00", "added_at": "2025-12-24T15:49:08.265811752+01:00",
"order": 4 "order": 4
}, },
{ {
"path": "documentation/guides", "path": "meetings/2025/sprint-planning.md",
"is_dir": true, "is_dir": false,
"title": "guides", "title": "sprint-planning",
"added_at": "2025-11-12T18:18:20.53353467+01:00", "added_at": "2025-12-24T15:55:04.58786532+01:00",
"order": 5 "order": 5
},
{
"path": "meetings",
"is_dir": true,
"title": "meetings",
"added_at": "2025-12-24T15:56:40.332077313+01:00",
"order": 6
},
{
"path": "Myfolder.txt",
"is_dir": true,
"title": "Myfolder.txt",
"added_at": "2025-12-24T15:57:09.512148418+01:00",
"order": 7
},
{
"path": "projets",
"is_dir": true,
"title": "projets",
"added_at": "2025-12-24T15:59:24.938636283+01:00",
"order": 8
},
{
"path": "documentation/bienvenue.md",
"is_dir": false,
"title": "bienvenue",
"added_at": "2025-12-24T16:30:46.322365652+01:00",
"order": 9
} }
] ]
} }

39
notes/.public.json Normal file
View File

@ -0,0 +1,39 @@
{
"notes": [
{
"path": "documentation/bienvenue.md",
"title": "Bienvenue dans PersoNotes",
"published_at": "2025-12-24T16:28:26.173656053+01:00"
},
{
"path": "documentation/authentication.md",
"title": "Authentication Guide",
"published_at": "2025-12-24T16:26:46.9731494+01:00"
},
{
"path": "ideas/collaboration.md",
"title": "Real-time Collaboration",
"published_at": "2025-12-24T12:40:15.778895438+01:00"
},
{
"path": "daily/2025/11/11.md",
"title": "Daily Note - 2025-11-11",
"published_at": "2025-12-24T12:38:12.77519956+01:00"
},
{
"path": "ideas/mobile-app.md",
"title": "Native Mobile App",
"published_at": "2025-12-24T12:37:20.649319402+01:00"
},
{
"path": "tasks/backlog.md",
"title": "Product Backlog",
"published_at": "2025-11-13T20:13:05.36334514+01:00"
},
{
"path": "documentation/api/endpoints.md",
"title": "API Endpoints Reference",
"published_at": "2025-11-13T19:36:57.522466752+01:00"
}
]
}

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
-

View File

@ -1,7 +1,7 @@
--- ---
title: Bienvenue dans PersoNotes title: Bienvenue dans PersoNotes
date: 08-11-2025 date: 08-11-2025
last_modified: 09-11-2025:01:13 last_modified: 24-12-2025:16:28
tags: tags:
- aide - aide
- documentation - documentation

View File

@ -1,7 +1,7 @@
--- ---
title: Client Feedback Session title: Client Feedback Session
date: 10-11-2025 date: 10-11-2025
last_modified: 11-11-2025:11:12 last_modified: 24-12-2025:16:45
tags: tags:
- meeting - meeting
- client - client
@ -28,3 +28,7 @@ Focus sur l'export PDF pour la v1.1
# DERNIER EDIT # DERNIER EDIT
[Progressive Web App](projets/mobile/pwa.md)
`This is a `

View File

@ -1,7 +1,7 @@
--- ---
title: CodeMirror Integration title: CodeMirror Integration
date: 10-11-2025 date: 10-11-2025
last_modified: 12-11-2025:09:37 last_modified: 24-12-2025:16:46
tags: tags:
- projet - projet
- frontend - frontend

View File

@ -1,8 +1,11 @@
--- ---
title: "Vite Build Process" title: Vite Build Process
date: "10-11-2025" date: 10-11-2025
last_modified: "10-11-2025:19:21" last_modified: 24-12-2025:16:41
tags: ["projet", "frontend", "build"] tags:
- projet
- frontend
- build
--- ---
# Vite Build Process # Vite Build Process

108
public/authentication.html Normal file
View File

@ -0,0 +1,108 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Authentication Guide - 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">Authentication Guide</h1>
<div class="public-meta"><span><i data-lucide="calendar" class="icon-sm"></i> 10-11-2025</span></div>
<div class="public-tags"><span class="tag">#documentation</span><span class="tag">#api</span><span class="tag">#security</span></div>
</div>
<div class="public-content">
<h1 id="authentication">Authentication</h1>
<h2 id="current-status">Current Status</h2>
<p>⚠️ No authentication currently implemented.</p>
<h2 id="future-implementation">Future Implementation</h2>
<h3 id="jwt-tokens">JWT Tokens</h3>
<pre><code>POST /api/auth/login
{
&quot;username&quot;: &quot;user&quot;,
&quot;password&quot;: &quot;pass&quot;
}
Response:
{
&quot;token&quot;: &quot;eyJhbGc...&quot;
}
</code></pre>
<h3 id="bearer-token">Bearer Token</h3>
<pre><code>Authorization: Bearer eyJhbGc...
</code></pre>
<h2 id="security">Security</h2>
<ul>
<li>HTTPS only in production</li>
<li>Reverse proxy with nginx</li>
<li>Rate limiting</li>
</ul>
<p><!-- raw HTML omitted -->Test Delete 1<!-- raw HTML omitted --></p>
</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>

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>

312
public/bienvenue.html Normal file
View File

@ -0,0 +1,312 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bienvenue dans PersoNotes - 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">Bienvenue dans PersoNotes</h1>
<div class="public-meta"><span><i data-lucide="calendar" class="icon-sm"></i> 08-11-2025</span></div>
<div class="public-tags"><span class="tag">#aide</span><span class="tag">#documentation</span><span class="tag">#tutorial</span></div>
</div>
<div class="public-content">
<p>08/11/2025 -</p>
<p>C&rsquo;est mon application de prise de note</p>
<h2 id="jespre-quelle-va-bien-marcher">J&rsquo;espére qu&rsquo;elle va bien marcher</h2>
<h1 id="bienvenue-dans-personotes">Bienvenue dans PersoNotes</h1>
<p>Bienvenue dans votre application de prise de notes en Markdown ! Cette page vous explique comment utiliser l&rsquo;application et le format front matter.</p>
<h2 id="quest-ce-que-le-front-matter-">Qu&rsquo;est-ce que le Front Matter ?</h2>
<p>Le <strong>front matter</strong> est un bloc de métadonnées en YAML placé au début de chaque note, entre deux lignes <code>---</code>. Il permet d&rsquo;ajouter des informations structurées à vos notes.</p>
<h3 id="format-du-front-matter">Format du Front Matter</h3>
<pre><code class="language-yaml">---
title: Titre de votre note
date: 08-11-2025
last_modified: 08-11-2025:14:10
tags: [projet, urgent, backend]
---
</code></pre>
<h3 id="champs-disponibles">Champs disponibles</h3>
<ul>
<li><strong>title</strong> : Le titre de votre note (généré automatiquement depuis le nom du fichier)</li>
<li><strong>date</strong> : Date de création (format: JJ-MM-AAAA)</li>
<li><strong>last_modified</strong> : Dernière modification (format: JJ-MM-AAAA:HH:MM) - mis à jour automatiquement</li>
<li><strong>tags</strong> : Liste de tags pour organiser et rechercher vos notes</li>
</ul>
<h3 id="exemples-de-tags">Exemples de tags</h3>
<p>Vous pouvez écrire vos tags de deux façons :</p>
<pre><code class="language-yaml"># Format inline
tags: [projet, urgent, backend, api]
# Format liste
tags:
- projet
- urgent
- backend
- api
</code></pre>
<p>Les tags sont indexés et permettent de rechercher vos notes via la barre de recherche.</p>
<h2 id="guide-markdown">Guide Markdown</h2>
<h3 id="titres">Titres</h3>
<pre><code class="language-markdown"># Titre niveau 1
## Titre niveau 2
### Titre niveau 3
</code></pre>
<h3 id="emphase">Emphase</h3>
<pre><code class="language-markdown">*italique* ou _italique_
**gras** ou __gras__
***gras et italique***
~~barré~~
</code></pre>
<p>Rendu : <em>italique</em>, <strong>gras</strong>, <em><strong>gras et italique</strong></em></p>
<h3 id="listes">Listes</h3>
<h4 id="liste-non-ordonne">Liste non ordonnée</h4>
<pre><code class="language-markdown">- Élément 1
- Élément 2
- Sous-élément 2.1
- Sous-élément 2.2
- Élément 3
</code></pre>
<p>Rendu :</p>
<ul>
<li>Élément 1</li>
<li>Élément 2
<ul>
<li>Sous-élément 2.1</li>
<li>Sous-élément 2.2</li>
</ul>
</li>
<li>Élément 3</li>
</ul>
<h4 id="liste-ordonne">Liste ordonnée</h4>
<pre><code class="language-markdown">1. Premier élément
2. Deuxième élément
3. Troisième élément
</code></pre>
<p>Rendu :</p>
<ol>
<li>Premier élément</li>
<li>Deuxième élément</li>
<li>Troisième élément</li>
</ol>
<h3 id="liens-et-images">Liens et Images</h3>
<pre><code class="language-markdown">[Texte du lien](https://example.com)
![Texte alternatif](url-de-image.jpg)
</code></pre>
<p>Exemple : <a href="https://www.markdownguide.org/">Documentation Markdown</a></p>
<h3 id="code">Code</h3>
<h4 id="code-inline">Code inline</h4>
<p>Utilisez des backticks : <code>code inline</code></p>
<h4 id="bloc-de-code">Bloc de code</h4>
<pre><code class="language-markdown">```javascript
function hello() {
console.log(&quot;Hello World!&quot;);
}
```
</code></pre>
<p>Rendu :</p>
<pre><code class="language-javascript">function hello() {
console.log(&quot;Hello World!&quot;);
}
</code></pre>
<h3 id="citations">Citations</h3>
<pre><code class="language-markdown">&gt; Ceci est une citation
&gt; sur plusieurs lignes
</code></pre>
<p>Rendu :</p>
<blockquote>
<p>Ceci est une citation<br />
sur plusieurs lignes</p>
</blockquote>
<h3 id="tableaux">Tableaux</h3>
<pre><code class="language-markdown">| Colonne 1 | Colonne 2 | Colonne 3 |
|-----------|-----------|-----------|
| Ligne 1 | Données | Données |
| Ligne 2 | Données | Données |
</code></pre>
<p>Rendu :</p>
<table>
<thead>
<tr>
<th>Colonne 1</th>
<th>Colonne 2</th>
<th>Colonne 3</th>
</tr>
</thead>
<tbody>
<tr>
<td>Ligne 1</td>
<td>Données</td>
<td>Données</td>
</tr>
<tr>
<td>Ligne 2</td>
<td>Données</td>
<td>Données</td>
</tr>
</tbody>
</table>
<h3 id="sparateurs">Séparateurs</h3>
<pre><code class="language-markdown">---
</code></pre>
<p>Rendu :</p>
<hr />
<h2 id="commandes-slash">Commandes Slash</h2>
<p>Utilisez le caractère <code>/</code> au début d&rsquo;une ligne pour accéder aux commandes rapides :</p>
<ul>
<li><code>/h1</code>, <code>/h2</code>, <code>/h3</code> - Titres</li>
<li><code>/list</code> - Liste à puces</li>
<li><code>/date</code> - Insérer la date du jour</li>
<li><code>/link</code> - Créer un lien</li>
<li><code>/bold</code> - Texte en gras</li>
<li><code>/italic</code> - Texte en italique</li>
<li><code>/code</code> - Code inline</li>
<li><code>/codeblock</code> - Bloc de code</li>
<li><code>/quote</code> - Citation</li>
<li><code>/hr</code> - Ligne de séparation</li>
<li><code>/table</code> - Créer un tableau</li>
</ul>
<p><strong>Navigation</strong> : Utilisez les flèches ↑↓ pour naviguer, Entrée ou Tab pour insérer, Échap pour annuler.</p>
<h2 id="raccourcis-et-astuces">Raccourcis et Astuces</h2>
<h3 id="crer-une-note">Créer une note</h3>
<p>Cliquez sur le bouton <strong>✨ Nouvelle note</strong> dans l&rsquo;en-tête. Si la note existe déjà, elle sera ouverte, sinon elle sera créée.</p>
<h3 id="rechercher-des-notes">Rechercher des notes</h3>
<p>Utilisez la barre de recherche en haut pour filtrer vos notes par tags. La recherche est mise à jour en temps réel.</p>
<h3 id="sauvegarder">Sauvegarder</h3>
<p>Cliquez sur le bouton <strong>💾 Enregistrer</strong> pour sauvegarder vos modifications. Le champ <code>last_modified</code> du front matter sera automatiquement mis à jour.</p>
<h3 id="supprimer-une-note">Supprimer une note</h3>
<p>Cliquez sur l&rsquo;icône 🗑️ à côté du nom de la note dans la sidebar.</p>
<h2 id="organisation-avec-les-tags">Organisation avec les tags</h2>
<p>Les tags sont un excellent moyen d&rsquo;organiser vos notes. Voici quelques suggestions :</p>
<ul>
<li><strong>Par projet</strong> : <code>projet-notes</code>, <code>projet-api</code>, <code>projet-frontend</code></li>
<li><strong>Par priorité</strong> : <code>urgent</code>, <code>important</code>, <code>backlog</code></li>
<li><strong>Par type</strong> : <code>documentation</code>, <code>tutorial</code>, <code>meeting</code>, <code>todo</code></li>
<li><strong>Par technologie</strong> : <code>javascript</code>, <code>go</code>, <code>python</code>, <code>docker</code></li>
<li><strong>Par statut</strong> : <code>en-cours</code>, <code>terminé</code>, <code>archive</code></li>
</ul>
<h2 id="exemple-complet">Exemple complet</h2>
<p>Voici un exemple de note complète :</p>
<pre><code class="language-markdown">---
title: Réunion API Backend
date: 08-11-2025
last_modified: 08-11-2025:15:30
tags: [meeting, backend, api, urgent]
---
# Réunion API Backend
## Participants
- Alice (Lead Dev)
- Bob (Backend)
- Charlie (Frontend)
## Points discutés
### 1. Architecture de l'API
Nous avons décidé d'utiliser une architecture REST avec les endpoints suivants :
- `GET /api/notes` - Liste toutes les notes
- `POST /api/notes` - Créer une note
- `PUT /api/notes/:id` - Modifier une note
- `DELETE /api/notes/:id` - Supprimer une note
### 2. Authentification
&gt; Utilisation de JWT pour l'authentification
Code d'exemple :
```go
func generateToken(userID string) (string, error) {
// Implementation
}
```
### 3. Prochaines étapes
- [ ] Implémenter les endpoints
- [ ] Écrire les tests
- [ ] Documentation API
## Actions
| Qui | Action | Deadline |
|---------|---------------------|------------|
| Bob | Endpoints API | 15-11-2025 |
| Charlie | Interface Frontend | 20-11-2025 |
| Alice | Review &amp; Deploy | 25-11-2025 |
</code></pre>
<hr />
<p>Bonne prise de notes ! 📝</p>
</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>

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>

110
public/index.html Normal file
View File

@ -0,0 +1,110 @@
<!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="bienvenue.html">
<h2 class="note-title">Bienvenue dans PersoNotes</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="authentication.html">
<h2 class="note-title">Authentication Guide</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="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>

3663
public/static/theme.css Normal file

File diff suppressed because it is too large Load Diff

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

@ -0,0 +1,679 @@
/*
* PersoNotes - Multi-Theme System
* Supports: Material Dark (default), Monokai Dark, Dracula, One Dark, Solarized Dark, Nord
*/
/* ===========================
THEME: MATERIAL DARK (défaut)
=========================== */
:root,
[data-theme="material-dark"] {
--bg-primary: #1e1e1e;
--bg-secondary: #252525;
--bg-tertiary: #2d2d2d;
--bg-elevated: #323232;
--border-primary: #3e3e3e;
--border-secondary: #2a2a2a;
--text-primary: #e0e0e0;
--text-secondary: #b0b0b0;
--text-muted: #6e6e6e;
--accent-primary: #42a5f5;
--accent-hover: #5ab3f7;
--success: #66bb6a;
--warning: #ffa726;
--error: #ef5350;
}
/* ===========================
THEME: MONOKAI DARK
=========================== */
[data-theme="monokai-dark"] {
--bg-primary: #272822;
--bg-secondary: #2d2e27;
--bg-tertiary: #3e3d32;
--bg-elevated: #49483e;
--border-primary: #49483e;
--border-secondary: #3e3d32;
--text-primary: #f8f8f2;
--text-secondary: #cfcfc2;
--text-muted: #75715e;
--accent-primary: #66d9ef;
--accent-hover: #7ee5f7;
--success: #88c070;
--warning: #e6db74;
--error: #f92672;
}
/* ===========================
THEME: DRACULA
=========================== */
[data-theme="dracula"] {
--bg-primary: #282a36;
--bg-secondary: #2f3241;
--bg-tertiary: #373844;
--bg-elevated: #44475a;
--border-primary: #44475a;
--border-secondary: #373844;
--text-primary: #f8f8f2;
--text-secondary: #d6d6d6;
--text-muted: #6272a4;
--accent-primary: #8be9fd;
--accent-hover: #9ff3ff;
--success: #50fa7b;
--warning: #f1fa8c;
--error: #ff5555;
}
/* ===========================
THEME: ONE DARK
=========================== */
[data-theme="one-dark"] {
--bg-primary: #282c34;
--bg-secondary: #2c313a;
--bg-tertiary: #333842;
--bg-elevated: #3e4451;
--border-primary: #3e4451;
--border-secondary: #333842;
--text-primary: #abb2bf;
--text-secondary: #9ca3af;
--text-muted: #5c6370;
--accent-primary: #61afef;
--accent-hover: #75bdf5;
--success: #98c379;
--warning: #e5c07b;
--error: #e06c75;
}
/* ===========================
THEME: SOLARIZED DARK
=========================== */
[data-theme="solarized-dark"] {
--bg-primary: #002b36;
--bg-secondary: #073642;
--bg-tertiary: #094454;
--bg-elevated: #0e5261;
--border-primary: #0e5261;
--border-secondary: #094454;
--text-primary: #839496;
--text-secondary: #93a1a1;
--text-muted: #586e75;
--accent-primary: #268bd2;
--accent-hover: #4098d9;
--success: #859900;
--warning: #b58900;
--error: #dc322f;
}
/* ===========================
THEME: NORD
=========================== */
[data-theme="nord"] {
--bg-primary: #2e3440;
--bg-secondary: #3b4252;
--bg-tertiary: #434c5e;
--bg-elevated: #4c566a;
--border-primary: #4c566a;
--border-secondary: #434c5e;
--text-primary: #eceff4;
--text-secondary: #d8dee9;
--text-muted: #616e88;
--accent-primary: #88c0d0;
--accent-hover: #9dcadb;
--success: #a3be8c;
--warning: #ebcb8b;
--error: #bf616a;
}
/* ===========================
THEME: CATPPUCCIN MOCHA
=========================== */
[data-theme="catppuccin"] {
--bg-primary: #1e1e2e;
--bg-secondary: #181825;
--bg-tertiary: #313244;
--bg-elevated: #45475a;
--border-primary: #45475a;
--border-secondary: #313244;
--text-primary: #cdd6f4;
--text-secondary: #bac2de;
--text-muted: #6c7086;
--accent-primary: #89b4fa;
--accent-hover: #a6c8ff;
--success: #a6e3a1;
--warning: #f9e2af;
--error: #f38ba8;
}
/* ===========================
THEME: EVERFOREST DARK
=========================== */
[data-theme="everforest"] {
--bg-primary: #2d353b;
--bg-secondary: #272e33;
--bg-tertiary: #343f44;
--bg-elevated: #3d484d;
--border-primary: #3d484d;
--border-secondary: #343f44;
--text-primary: #d3c6aa;
--text-secondary: #b4a990;
--text-muted: #7a8478;
--accent-primary: #7fbbb3;
--accent-hover: #93c9c1;
--success: #a7c080;
--warning: #dbbc7f;
--error: #e67e80;
}
/* ===========================
BOUTONS D'ACTION DE LA SIDEBAR
=========================== */
.sidebar-action-btn {
width: 100%;
background: var(--bg-tertiary);
border: 1px solid var(--border-primary);
border-radius: var(--radius-md);
color: var(--text-primary);
padding: var(--spacing-md);
cursor: pointer;
transition: all var(--transition-fast);
display: flex;
align-items: center;
justify-content: center;
gap: var(--spacing-sm);
font-size: 0.9rem;
font-weight: 500;
margin-top: var(--spacing-sm);
}
.sidebar-action-btn:hover {
background: var(--bg-elevated);
border-color: var(--accent-primary);
color: var(--accent-primary);
transform: translateY(-1px);
box-shadow: var(--shadow-sm);
}
/* Style spécifique pour le bouton paramètres (avec animation) */
.theme-settings-btn {
width: 100%;
background: var(--bg-tertiary);
border: 1px solid var(--border-primary);
border-radius: var(--radius-md);
color: var(--text-primary);
padding: var(--spacing-md);
cursor: pointer;
transition: all var(--transition-fast);
display: flex;
align-items: center;
justify-content: center;
gap: var(--spacing-sm);
font-size: 0.9rem;
font-weight: 500;
margin-top: var(--spacing-sm);
}
.theme-settings-btn:hover {
background: var(--bg-elevated);
border-color: var(--accent-primary);
color: var(--accent-primary);
transform: translateY(-1px);
box-shadow: var(--shadow-sm);
}
.theme-settings-btn svg {
animation: rotate 2s linear infinite;
animation-play-state: paused;
}
.theme-settings-btn:hover svg {
animation-play-state: running;
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* ===========================
MODALE DE SÉLECTION DE THÈME
=========================== */
#theme-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
}
.theme-modal-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(4px);
}
.theme-modal-content {
position: relative;
background: var(--bg-secondary);
border: 1px solid var(--accent-primary);
border-radius: var(--radius-lg);
padding: var(--spacing-xl);
max-width: 600px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
box-shadow: var(--shadow-lg), var(--shadow-glow);
animation: slideUp 0.3s ease;
z-index: 1;
}
.theme-modal-content h2 {
margin: 0 0 var(--spacing-lg) 0;
color: var(--text-primary);
font-size: 1.4rem;
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.theme-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: var(--spacing-md);
margin-bottom: var(--spacing-lg);
}
.theme-card {
background: var(--bg-tertiary);
border: 2px solid var(--border-primary);
border-radius: var(--radius-md);
padding: var(--spacing-md);
cursor: pointer;
transition: all var(--transition-fast);
position: relative;
}
.theme-card:hover {
border-color: var(--accent-primary);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.theme-card.active {
border-color: var(--accent-primary);
box-shadow: 0 0 0 3px rgba(66, 165, 245, 0.2);
}
.theme-card.active::before {
content: '✓';
position: absolute;
top: var(--spacing-xs);
right: var(--spacing-xs);
background: var(--accent-primary);
color: white;
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 0.8rem;
}
.theme-card-header {
display: flex;
align-items: center;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-md);
}
.theme-card-icon {
font-size: 1.5rem;
}
.theme-card-name {
font-weight: 600;
color: var(--text-primary);
font-size: 1rem;
}
.theme-preview {
display: flex;
gap: var(--spacing-xs);
margin-bottom: var(--spacing-sm);
height: 40px;
border-radius: var(--radius-sm);
overflow: hidden;
}
.theme-preview-color {
flex: 1;
transition: all var(--transition-fast);
}
.theme-card:hover .theme-preview-color {
transform: scaleY(1.1);
}
.theme-description {
font-size: 0.85rem;
color: var(--text-secondary);
line-height: 1.4;
}
/* Couleurs de prévisualisation pour chaque thème */
.theme-card[data-theme="material-dark"] .theme-preview-color:nth-child(1) { background: #1e1e1e; }
.theme-card[data-theme="material-dark"] .theme-preview-color:nth-child(2) { background: #42a5f5; }
.theme-card[data-theme="material-dark"] .theme-preview-color:nth-child(3) { background: #29b6f6; }
.theme-card[data-theme="material-dark"] .theme-preview-color:nth-child(4) { background: #e0e0e0; }
.theme-card[data-theme="monokai-dark"] .theme-preview-color:nth-child(1) { background: #272822; }
.theme-card[data-theme="monokai-dark"] .theme-preview-color:nth-child(2) { background: #66d9ef; }
.theme-card[data-theme="monokai-dark"] .theme-preview-color:nth-child(3) { background: #88c070; }
.theme-card[data-theme="monokai-dark"] .theme-preview-color:nth-child(4) { background: #f8f8f2; }
.theme-card[data-theme="dracula"] .theme-preview-color:nth-child(1) { background: #282a36; }
.theme-card[data-theme="dracula"] .theme-preview-color:nth-child(2) { background: #8be9fd; }
.theme-card[data-theme="dracula"] .theme-preview-color:nth-child(3) { background: #bd93f9; }
.theme-card[data-theme="dracula"] .theme-preview-color:nth-child(4) { background: #f8f8f2; }
.theme-card[data-theme="one-dark"] .theme-preview-color:nth-child(1) { background: #282c34; }
.theme-card[data-theme="one-dark"] .theme-preview-color:nth-child(2) { background: #61afef; }
.theme-card[data-theme="one-dark"] .theme-preview-color:nth-child(3) { background: #c678dd; }
.theme-card[data-theme="one-dark"] .theme-preview-color:nth-child(4) { background: #abb2bf; }
.theme-card[data-theme="solarized-dark"] .theme-preview-color:nth-child(1) { background: #002b36; }
.theme-card[data-theme="solarized-dark"] .theme-preview-color:nth-child(2) { background: #268bd2; }
.theme-card[data-theme="solarized-dark"] .theme-preview-color:nth-child(3) { background: #2aa198; }
.theme-card[data-theme="solarized-dark"] .theme-preview-color:nth-child(4) { background: #839496; }
.theme-card[data-theme="nord"] .theme-preview-color:nth-child(1) { background: #2e3440; }
.theme-card[data-theme="nord"] .theme-preview-color:nth-child(2) { background: #88c0d0; }
.theme-card[data-theme="nord"] .theme-preview-color:nth-child(3) { background: #81a1c1; }
.theme-card[data-theme="nord"] .theme-preview-color:nth-child(4) { background: #eceff4; }
.theme-card[data-theme="catppuccin"] .theme-preview-color:nth-child(1) { background: #1e1e2e; }
.theme-card[data-theme="catppuccin"] .theme-preview-color:nth-child(2) { background: #89b4fa; }
.theme-card[data-theme="catppuccin"] .theme-preview-color:nth-child(3) { background: #f5c2e7; }
.theme-card[data-theme="catppuccin"] .theme-preview-color:nth-child(4) { background: #cdd6f4; }
.theme-card[data-theme="everforest"] .theme-preview-color:nth-child(1) { background: #2d353b; }
.theme-card[data-theme="everforest"] .theme-preview-color:nth-child(2) { background: #7fbbb3; }
.theme-card[data-theme="everforest"] .theme-preview-color:nth-child(3) { background: #a7c080; }
.theme-card[data-theme="everforest"] .theme-preview-color:nth-child(4) { background: #d3c6aa; }
.theme-modal-footer {
display: flex;
justify-content: flex-end;
gap: var(--spacing-sm);
margin-top: var(--spacing-lg);
padding-top: var(--spacing-md);
border-top: 1px solid var(--border-primary);
}
/* Onglets de paramètres */
.settings-tabs {
display: flex;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-lg);
border-bottom: 2px solid var(--border-primary);
}
.settings-tab {
background: transparent;
border: none;
padding: var(--spacing-sm) var(--spacing-md);
color: var(--text-secondary);
font-size: 0.95rem;
font-weight: 500;
cursor: pointer;
transition: all var(--transition-fast);
border-bottom: 2px solid transparent;
margin-bottom: -2px;
}
.settings-tab:hover {
color: var(--text-primary);
background: var(--bg-tertiary);
}
.settings-tab.active {
color: var(--accent-primary);
border-bottom-color: var(--accent-primary);
}
.settings-section {
animation: fadeIn 0.3s ease;
}
/* Grille de polices */
.font-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: var(--spacing-md);
margin-bottom: var(--spacing-lg);
}
.font-card {
background: var(--bg-tertiary);
border: 2px solid var(--border-primary);
border-radius: var(--radius-md);
padding: var(--spacing-md);
cursor: pointer;
transition: all var(--transition-fast);
position: relative;
}
.font-card:hover {
border-color: var(--accent-primary);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.font-card.active {
border-color: var(--accent-primary);
box-shadow: 0 0 0 3px rgba(66, 165, 245, 0.2);
}
.font-card.active::before {
content: '✓';
position: absolute;
top: var(--spacing-xs);
right: var(--spacing-xs);
background: var(--accent-primary);
color: white;
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 0.8rem;
}
.font-card-header {
display: flex;
align-items: center;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-md);
}
.font-card-icon {
font-size: 1.3rem;
}
.font-card-name {
font-weight: 600;
color: var(--text-primary);
font-size: 0.95rem;
}
.font-preview {
background: var(--bg-primary);
border: 1px solid var(--border-primary);
border-radius: var(--radius-sm);
padding: var(--spacing-md);
margin-bottom: var(--spacing-sm);
font-size: 1.1rem;
text-align: center;
color: var(--text-primary);
min-height: 50px;
display: flex;
align-items: center;
justify-content: center;
}
.font-description {
font-size: 0.8rem;
color: var(--text-secondary);
line-height: 1.4;
}
/* Sélecteur de taille de police */
.font-size-selector {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
gap: var(--spacing-sm);
}
.font-size-option {
background: var(--bg-tertiary);
border: 2px solid var(--border-primary);
border-radius: var(--radius-md);
padding: var(--spacing-md);
cursor: pointer;
transition: all var(--transition-fast);
display: flex;
flex-direction: column;
align-items: center;
gap: var(--spacing-xs);
}
.font-size-option:hover {
border-color: var(--accent-primary);
transform: translateY(-2px);
box-shadow: var(--shadow-sm);
}
.font-size-option.active {
border-color: var(--accent-primary);
background: rgba(66, 165, 245, 0.1);
box-shadow: 0 0 0 3px rgba(66, 165, 245, 0.2);
}
.size-label {
font-size: 0.85rem;
color: var(--text-secondary);
font-weight: 500;
}
.font-size-option.active .size-label {
color: var(--accent-primary);
}
.size-preview {
font-weight: 600;
color: var(--text-primary);
line-height: 1;
}
/* ===========================
TOGGLE SWITCH (pour Mode Vim)
=========================== */
.toggle-switch {
position: relative;
display: inline-block;
width: 50px;
height: 26px;
flex-shrink: 0;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--bg-tertiary);
transition: 0.3s;
border-radius: 26px;
border: 1px solid var(--border-primary);
}
.toggle-slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 4px;
bottom: 3px;
background-color: var(--text-muted);
transition: 0.3s;
border-radius: 50%;
}
.toggle-switch input:checked + .toggle-slider {
background-color: var(--accent-primary);
border-color: var(--accent-primary);
}
.toggle-switch input:checked + .toggle-slider:before {
transform: translateX(24px);
background-color: white;
}
.toggle-switch input:focus + .toggle-slider {
box-shadow: 0 0 0 3px rgba(66, 165, 245, 0.2);
}

View File

@ -17,35 +17,33 @@
--text-secondary: #b0b0b0; --text-secondary: #b0b0b0;
--text-muted: #6e6e6e; --text-muted: #6e6e6e;
/* Accent colors - Blue focused */ /* Accent color - Single blue accent for consistency */
--accent-primary: #42a5f5; --accent-primary: #42a5f5;
--accent-primary-hover: #64b5f6; --accent-hover: #64b5f6;
--accent-secondary: #29b6f6;
--accent-secondary-hover: #4fc3f7;
/* Semantic colors */ /* Semantic colors */
--success: #66bb6a; --success: #66bb6a;
--warning: #ffa726; --warning: #ffa726;
--error: #ef5350; --error: #ef5350;
/* Spacing */ /* Spacing - 8px base unit system */
--spacing-xs: 0.25rem; --spacing-xs: 0.5rem; /* 8px - 1 unit */
--spacing-sm: 0.5rem; --spacing-sm: 1rem; /* 16px - 2 units */
--spacing-md: 1rem; --spacing-md: 1.5rem; /* 24px - 3 units */
--spacing-lg: 1.5rem; --spacing-lg: 2rem; /* 32px - 4 units */
--spacing-xl: 2rem; --spacing-xl: 3rem; /* 48px - 6 units */
/* Sidebar compact spacing */
--sidebar-item-gap: 0.05rem;
--sidebar-padding-v: 0.3rem;
--sidebar-padding-h: 0.75rem;
--sidebar-indent: 1rem;
/* Shadows */ /* Sidebar spacing - aligned to 8px grid */
--shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.4); --sidebar-item-gap: 0.125rem; /* 2px - minimal spacing between items */
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -2px rgba(0, 0, 0, 0.3); --sidebar-padding-v: 0.25rem; /* 4px - compact vertical padding */
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.4), 0 4px 6px -4px rgba(0, 0, 0, 0.3); --sidebar-padding-h: 1rem; /* 16px */
--shadow-glow: 0 0 20px rgba(66, 165, 245, 0.2); --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 */ /* Border radius */
--radius-sm: 4px; --radius-sm: 4px;
@ -55,8 +53,59 @@
/* Transitions */ /* Transitions */
--transition-fast: 150ms ease; --transition-fast: 150ms ease;
--transition-normal: 250ms 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 */ /* Base styles */
html { html {
font-size: 16px; font-size: 16px;
@ -90,7 +139,7 @@ header h1 {
font-size: 1.25rem; font-size: 1.25rem;
font-weight: 600; font-weight: 600;
color: var(--text-primary); color: var(--text-primary);
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); background: var(--accent-primary);
-webkit-background-clip: text; -webkit-background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
background-clip: text; background-clip: text;
@ -257,25 +306,7 @@ aside hr {
margin: var(--spacing-sm) 0; margin: var(--spacing-sm) 0;
} }
/* File tree and search results */ /* File tree and search results - styles now handled by .file-item class */
#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);
}
/* Search results header */ /* Search results header */
.search-results-header { .search-results-header {
@ -411,7 +442,7 @@ aside hr {
} }
.search-no-results-text strong { .search-no-results-text strong {
color: var(--accent-secondary); color: var(--accent-primary);
} }
.search-no-results-hint { .search-no-results-hint {
@ -455,7 +486,7 @@ aside hr {
.search-help-example code { .search-help-example code {
background: var(--bg-primary); background: var(--bg-primary);
color: var(--accent-secondary); color: var(--accent-primary);
padding: 0.2rem 0.4rem; padding: 0.2rem 0.4rem;
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
font-size: 0.8rem; font-size: 0.8rem;
@ -720,7 +751,7 @@ main::-webkit-scrollbar-thumb:hover {
.preview h2 { .preview h2 {
font-size: 1.5em; font-size: 1.5em;
color: var(--accent-secondary); color: var(--accent-primary);
margin-bottom: 0.9em; margin-bottom: 0.9em;
} }
@ -771,7 +802,7 @@ main::-webkit-scrollbar-thumb:hover {
} }
.preview ol > li::marker { .preview ol > li::marker {
color: var(--accent-secondary); color: var(--accent-primary);
font-weight: 600; font-weight: 600;
} }
@ -797,7 +828,7 @@ main::-webkit-scrollbar-thumb:hover {
.preview a:hover { .preview a:hover {
text-decoration: underline; text-decoration: underline;
color: var(--accent-primary-hover); color: var(--accent-hover);
} }
.preview strong, .preview b { .preview strong, .preview b {
@ -815,7 +846,7 @@ main::-webkit-scrollbar-thumb:hover {
padding: 0.2em 0.4em; padding: 0.2em 0.4em;
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
font-size: 0.85em; font-size: 0.85em;
color: var(--accent-secondary); color: var(--accent-primary);
font-weight: 500; font-weight: 500;
} }
@ -853,7 +884,7 @@ main::-webkit-scrollbar-thumb:hover {
} }
.preview table thead { .preview table thead {
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); background: var(--accent-primary);
} }
.preview table thead th { .preview table thead th {
@ -895,7 +926,7 @@ main::-webkit-scrollbar-thumb:hover {
button, button,
[type="submit"], [type="submit"],
[type="button"] { [type="button"] {
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); background: var(--accent-primary);
color: white; color: white;
border: none; border: none;
border-radius: var(--radius-md); border-radius: var(--radius-md);
@ -984,7 +1015,7 @@ button.secondary:hover {
} }
#slash-commands-palette li[style*="background-color"] { #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; color: white !important;
font-weight: 500; font-weight: 500;
transform: translateX(2px); transform: translateX(2px);
@ -1016,7 +1047,7 @@ progress::-webkit-progress-bar {
} }
progress::-webkit-progress-value { progress::-webkit-progress-value {
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary)); background: var(--accent-primary);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
} }
@ -1236,39 +1267,26 @@ body, html {
} }
.folder-item.drag-over .folder-header { .folder-item.drag-over .folder-header {
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); background: var(--accent-primary);
color: white; color: white;
box-shadow: var(--shadow-glow); box-shadow: var(--shadow-glow);
border: 2px solid var(--accent-primary); border: 2px solid var(--accent-primary);
border-radius: var(--radius-md); 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 { .file-item.drag-over {
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); background: var(--accent-primary);
color: white; color: white;
box-shadow: var(--shadow-glow); box-shadow: var(--shadow-glow);
} }
/* Style pour la racine en drag-over */ /* Style pour la racine en drag-over */
.sidebar-section-header.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; color: white !important;
box-shadow: var(--shadow-glow); box-shadow: var(--shadow-glow);
border: 2px solid var(--accent-primary); border: 2px solid var(--accent-primary);
border-radius: var(--radius-md); border-radius: var(--radius-md);
animation: pulse 1s ease-in-out infinite;
} }
.sidebar-section-header.drag-over .folder-name, .sidebar-section-header.drag-over .folder-name,
@ -1401,11 +1419,10 @@ body, html {
/* Quand on drag au-dessus de la racine */ /* Quand on drag au-dessus de la racine */
.root-drop-zone.drag-over .root-folder-header { .root-drop-zone.drag-over .root-folder-header {
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); background: var(--accent-primary);
color: white; color: white;
border-color: var(--accent-primary); border-color: var(--accent-primary);
box-shadow: var(--shadow-glow); box-shadow: var(--shadow-glow);
animation: pulse 1s ease-in-out infinite;
} }
.root-drop-zone.drag-over .root-folder-header .folder-name, .root-drop-zone.drag-over .root-folder-header .folder-name,
@ -2053,7 +2070,7 @@ body, html {
} }
.search-modal-result-item.selected { .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); border-color: var(--accent-primary);
} }
@ -2074,7 +2091,7 @@ body, html {
} }
.search-modal-result-title mark { .search-modal-result-title mark {
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); background: var(--accent-primary);
color: white; color: white;
padding: 2px 4px; padding: 2px 4px;
border-radius: 3px; border-radius: 3px;
@ -2200,11 +2217,6 @@ body, html {
border: 3px solid var(--border-primary); border: 3px solid var(--border-primary);
border-top-color: var(--accent-primary); border-top-color: var(--accent-primary);
border-radius: 50%; border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
} }
.search-modal-loading p { .search-modal-loading p {
@ -2566,12 +2578,12 @@ body, html {
/* Today */ /* Today */
.calendar-day-today { .calendar-day-today {
border-color: var(--accent-secondary); border-color: var(--accent-primary);
background: linear-gradient(135deg, rgba(130, 170, 255, 0.1), rgba(199, 146, 234, 0.1)); background: var(--bg-secondary);
} }
.calendar-day-today .calendar-day-number { .calendar-day-today .calendar-day-number {
color: var(--accent-secondary); color: var(--accent-primary);
font-weight: 700; font-weight: 700;
} }
@ -2596,7 +2608,7 @@ body, html {
width: 100%; width: 100%;
margin-top: var(--spacing-sm); margin-top: var(--spacing-sm);
padding: var(--spacing-sm); padding: var(--spacing-sm);
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); background: var(--accent-primary);
color: white; color: white;
border: none; border: none;
border-radius: var(--radius-md); border-radius: var(--radius-md);
@ -2681,11 +2693,12 @@ body, html {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0; padding: 0;
margin: var(--sidebar-item-gap) 0; margin: 0; /* No margin on wrapper - use same spacing as folders */
} }
.file-item-wrapper .file-item { .file-item-wrapper .file-item {
flex: 1; flex: 1;
margin: 0 !important; /* Force remove margin to avoid double spacing */
} }
/* Bouton de mode sélection */ /* Bouton de mode sélection */
@ -2715,8 +2728,8 @@ body, html {
} }
.icon-button.active:hover { .icon-button.active:hover {
background: var(--accent-primary-hover); background: var(--accent-hover);
border-color: var(--accent-primary-hover); border-color: var(--accent-hover);
} }
/* Toolbar de sélection flottante */ /* Toolbar de sélection flottante */
@ -2784,7 +2797,7 @@ body, html {
background: #ff5370; background: #ff5370;
border-color: #ff5370; border-color: #ff5370;
transform: translateY(-1px); 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 { .danger-button:active {
@ -2974,30 +2987,38 @@ body, html {
/* Bouton d'ajout aux favoris (dans le file tree) */ /* Bouton d'ajout aux favoris (dans le file tree) */
.add-to-favorites { .add-to-favorites {
opacity: 0; opacity: 0.4; /* Always slightly visible */
background: transparent; background: transparent;
border: none; border: none;
color: var(--text-muted); color: var(--text-muted);
cursor: pointer; cursor: pointer;
font-size: 0.9rem; font-size: 1rem;
padding: 0 0.2rem; padding: 0.25rem;
transition: all var(--transition-fast); transition: all var(--transition-fast);
margin-left: auto; margin-left: auto;
border-radius: var(--radius-sm);
} }
.folder-header:hover .add-to-favorites, .folder-header:hover .add-to-favorites,
.file-item:hover .add-to-favorites { .file-item:hover .add-to-favorites {
opacity: 1; opacity: 1;
background: var(--bg-tertiary);
} }
.add-to-favorites:hover { .add-to-favorites:hover {
color: var(--warning); 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 { .add-to-favorites.is-favorite {
opacity: 1; opacity: 1 !important; /* Always fully visible when favorited */
color: var(--warning); 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 */ /* Responsive - Hauteurs adaptatives pour #favorites-list */
@ -3162,7 +3183,7 @@ body, html {
} }
.link-inserter-result-item.selected { .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); border-color: var(--accent-primary);
} }
@ -3184,7 +3205,7 @@ body, html {
} }
.link-inserter-result-title mark { .link-inserter-result-title mark {
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); background: var(--accent-primary);
color: white; color: white;
padding: 2px 4px; padding: 2px 4px;
border-radius: 3px; border-radius: 3px;
@ -3241,7 +3262,6 @@ body, html {
border: 3px solid var(--bg-tertiary); border: 3px solid var(--bg-tertiary);
border-top-color: var(--accent-primary); border-top-color: var(--accent-primary);
border-radius: 50%; border-radius: 50%;
animation: spin 0.8s linear infinite;
} }
.link-inserter-loading p { .link-inserter-loading p {
@ -3575,7 +3595,7 @@ body, html {
.breadcrumb-link:hover { .breadcrumb-link:hover {
background: var(--bg-tertiary); background: var(--bg-tertiary);
color: var(--accent-secondary); color: var(--accent-primary);
} }
.breadcrumb-separator { .breadcrumb-separator {
@ -3624,3 +3644,20 @@ body, html {
color: var(--accent-primary); 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; --text-muted: #6e6e6e;
--accent-primary: #42a5f5; --accent-primary: #42a5f5;
--accent-primary-hover: #5ab3f7; --accent-hover: #5ab3f7;
--accent-secondary: #29b6f6;
--accent-secondary-hover: #4fc3f7;
--success: #66bb6a; --success: #66bb6a;
--warning: #ffa726; --warning: #ffa726;
@ -47,9 +45,7 @@
--text-muted: #75715e; --text-muted: #75715e;
--accent-primary: #66d9ef; --accent-primary: #66d9ef;
--accent-primary-hover: #7ee5f7; --accent-hover: #7ee5f7;
--accent-secondary: #88c070;
--accent-secondary-hover: #9acc84;
--success: #88c070; --success: #88c070;
--warning: #e6db74; --warning: #e6db74;
@ -73,9 +69,7 @@
--text-muted: #6272a4; --text-muted: #6272a4;
--accent-primary: #8be9fd; --accent-primary: #8be9fd;
--accent-primary-hover: #9ff3ff; --accent-hover: #9ff3ff;
--accent-secondary: #bd93f9;
--accent-secondary-hover: #cba6ff;
--success: #50fa7b; --success: #50fa7b;
--warning: #f1fa8c; --warning: #f1fa8c;
@ -99,9 +93,7 @@
--text-muted: #5c6370; --text-muted: #5c6370;
--accent-primary: #61afef; --accent-primary: #61afef;
--accent-primary-hover: #75bdf5; --accent-hover: #75bdf5;
--accent-secondary: #c678dd;
--accent-secondary-hover: #d48ae9;
--success: #98c379; --success: #98c379;
--warning: #e5c07b; --warning: #e5c07b;
@ -125,9 +117,7 @@
--text-muted: #586e75; --text-muted: #586e75;
--accent-primary: #268bd2; --accent-primary: #268bd2;
--accent-primary-hover: #4098d9; --accent-hover: #4098d9;
--accent-secondary: #2aa198;
--accent-secondary-hover: #3eb3a8;
--success: #859900; --success: #859900;
--warning: #b58900; --warning: #b58900;
@ -151,9 +141,7 @@
--text-muted: #616e88; --text-muted: #616e88;
--accent-primary: #88c0d0; --accent-primary: #88c0d0;
--accent-primary-hover: #9dcadb; --accent-hover: #9dcadb;
--accent-secondary: #81a1c1;
--accent-secondary-hover: #94b0cc;
--success: #a3be8c; --success: #a3be8c;
--warning: #ebcb8b; --warning: #ebcb8b;
@ -177,9 +165,7 @@
--text-muted: #6c7086; --text-muted: #6c7086;
--accent-primary: #89b4fa; --accent-primary: #89b4fa;
--accent-primary-hover: #a6c8ff; --accent-hover: #a6c8ff;
--accent-secondary: #f5c2e7;
--accent-secondary-hover: #f9d5ee;
--success: #a6e3a1; --success: #a6e3a1;
--warning: #f9e2af; --warning: #f9e2af;
@ -203,9 +189,7 @@
--text-muted: #7a8478; --text-muted: #7a8478;
--accent-primary: #7fbbb3; --accent-primary: #7fbbb3;
--accent-primary-hover: #93c9c1; --accent-hover: #93c9c1;
--accent-secondary: #a7c080;
--accent-secondary-hover: #b8cc94;
--success: #a7c080; --success: #a7c080;
--warning: #dbbc7f; --warning: #dbbc7f;

View File

@ -1,7 +1,7 @@
<div id="about-content" style="padding: 3rem; max-width: 900px; margin: 0 auto;"> <div id="about-content" style="padding: 3rem; max-width: 900px; margin: 0 auto;">
<div style="text-align: center; margin-bottom: 3rem;"> <div style="text-align: center; margin-bottom: 3rem;">
<h1 style="font-size: 2.5rem; color: #c792ea; margin-bottom: 1rem;"> <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> </h1>
<p style="font-size: 1.2rem; color: var(--text-secondary); margin-bottom: 2rem;"> <p style="font-size: 1.2rem; color: var(--text-secondary); margin-bottom: 2rem;">
Un gestionnaire de notes Markdown moderne et puissant Un gestionnaire de notes Markdown moderne et puissant
@ -10,23 +10,23 @@
<div style="margin-top: 3rem;"> <div style="margin-top: 3rem;">
<h2 style="color: #82aaff; font-size: 1.5rem; margin-bottom: 1.5rem;"> <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> </h2>
<div style="display: grid; gap: 1.5rem; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));"> <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;"> <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;"> <p style="color: var(--text-secondary); font-size: 0.9rem; margin-bottom: 1rem;">
Explorez vos notes dans l'arborescence à gauche Explorez vos notes dans l'arborescence à gauche
</p> </p>
</div> </div>
<div style="background: var(--bg-secondary); border: 1px solid var(--border-primary); border-radius: var(--radius-md); padding: 1.5rem;"> <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;"> <p style="color: var(--text-secondary); font-size: 0.9rem; margin-bottom: 1rem;">
Utilisez la barre de recherche en haut pour trouver vos notes Utilisez la barre de recherche en haut pour trouver vos notes
</p> </p>
</div> </div>
<div style="background: var(--bg-secondary); border: 1px solid var(--border-primary); border-radius: var(--radius-md); padding: 1.5rem;"> <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;"> <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 Tapez <code style="color: #f07178;">/</code> dans l'éditeur pour insérer du Markdown
</p> </p>
@ -36,7 +36,7 @@
<div style="margin-top: 3rem;"> <div style="margin-top: 3rem;">
<h2 style="color: #82aaff; font-size: 1.5rem; margin-bottom: 1.5rem;"> <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> </h2>
<ul style="color: var(--text-secondary); line-height: 2; list-style: none; padding: 0;"> <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);"> <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);"> <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;"> <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> </p>
</div> </div>
<div style="margin-top: 3rem;"> <div style="margin-top: 3rem;">
<h2 style="color: #82aaff; font-size: 1.5rem; margin-bottom: 1.5rem;"> <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> </h2>
<div style="display: grid; gap: 0.75rem; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));"> <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;"> <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>
</div> </div>
<p style="color: var(--text-muted); font-size: 0.85rem; text-align: center; margin-top: 1.5rem;"> <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> </p>
</div> </div>
</div> </div>

View File

@ -57,7 +57,7 @@
data-i18n="calendar.today" data-i18n="calendar.today"
title="Ouvrir la note du jour (Ctrl/Cmd+D)" title="Ouvrir la note du jour (Ctrl/Cmd+D)"
style="flex: 1; padding: 0.5rem; font-size: 0.85rem;"> 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>
<button class="daily-today-btn" <button class="daily-today-btn"
hx-get="/api/daily/calendar/{{.CurrentMonth}}" hx-get="/api/daily/calendar/{{.CurrentMonth}}"
@ -66,7 +66,7 @@
data-i18n="calendar.thisMonth" data-i18n="calendar.thisMonth"
title="Revenir au mois actuel" title="Revenir au mois actuel"
style="flex: 1; padding: 0.5rem; font-size: 0.85rem;"> style="flex: 1; padding: 0.5rem; font-size: 0.85rem;">
🗓️ Ce mois <i data-lucide="calendar-days" class="icon-sm"></i> Ce mois
</button> </button>
</div> </div>
</div> </div>

View File

@ -19,7 +19,7 @@
</label> </label>
{{if .IsHome}} {{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"> <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> </button>
{{else}} {{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)"> <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> </div>
{{if .Backlinks}} {{if .Backlinks}}
<div id="backlinks-section" class="backlinks-section"> <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"> <ul class="backlinks-list">
{{range .Backlinks}} {{range .Backlinks}}
<li class="backlink-item"> <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"> <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> </a>
</li> </li>
{{end}} {{end}}
@ -54,6 +54,17 @@
<div class="editor-actions"> <div class="editor-actions">
<div class="editor-actions-primary"> <div class="editor-actions-primary">
<button type="submit" data-i18n="editor.save">Enregistrer</button> <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 <button
hx-delete="/api/notes/{{.Filename}}" hx-delete="/api/notes/{{.Filename}}"
hx-confirm="Êtes-vous sûr de vouloir supprimer cette note ({{.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}}"> <div class="favorite-item" data-path="{{.Path}}">
{{if .IsDir}} {{if .IsDir}}
<div class="favorite-folder" data-path="{{.Path}}"> <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-folder-icon">{{.Icon}}</span>
<span class="favorite-name">{{.Title}}</span> <span class="favorite-name">{{.Title}}</span>
<button class="favorite-remove" <button class="favorite-remove"
onclick="removeFavorite('{{.Path}}')" onclick="removeFavorite('{{.Path}}')"
title="Retirer des favoris">×</button> title="Retirer des favoris"><i data-lucide="x" class="icon-xs"></i></button>
</div> </div>
{{else}} {{else}}
<a href="#" <a href="#"
@ -18,12 +18,12 @@
hx-target="#editor-container" hx-target="#editor-container"
hx-swap="innerHTML" hx-swap="innerHTML"
hx-push-url="true"> 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-file-icon">{{.Icon}}</span>
<span class="favorite-name">{{.Title}}</span> <span class="favorite-name">{{.Title}}</span>
<button class="favorite-remove" <button class="favorite-remove"
onclick="event.preventDefault(); event.stopPropagation(); removeFavorite('{{.Path}}')" onclick="event.preventDefault(); event.stopPropagation(); removeFavorite('{{.Path}}')"
title="Retirer des favoris">×</button> title="Retirer des favoris"><i data-lucide="x" class="icon-xs"></i></button>
</a> </a>
{{end}} {{end}}
</div> </div>
@ -32,7 +32,7 @@
<p class="favorites-empty"> <p class="favorites-empty">
Aucun favori.<br> Aucun favori.<br>
<span style="font-size: 0.75rem; color: var(--text-muted);"> <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> </span>
</p> </p>
{{end}} {{end}}

View File

@ -1,7 +1,7 @@
<!-- Indicateur de racine (maintenant cliquable et rétractable) --> <!-- 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;"> <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="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="folder-name">Racine</span>
<span class="root-hint">(notes/)</span> <span class="root-hint">(notes/)</span>
</div> </div>
@ -28,7 +28,7 @@
<div class="folder-header"> <div class="folder-header">
<input type="checkbox" class="selection-checkbox folder-checkbox" data-path="{{.Path}}" data-is-dir="true" style="display: none;"> <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-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> <span class="folder-name">{{.Name}}</span>
</div> </div>
<div class="folder-children" style="display: none;"> <div class="folder-children" style="display: none;">
@ -49,7 +49,7 @@
hx-swap="innerHTML" hx-swap="innerHTML"
hx-push-url="true" hx-push-url="true"
draggable="true"> draggable="true">
📄 {{.Name}} <i data-lucide="file-text" class="icon-sm"></i> {{.Name}}
</a> </a>
</div> </div>
{{end}} {{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/marked/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify/dist/purify.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> <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 src="/static/sidebar-resize.js"></script>
<script type="module" src="/static/dist/personotes-frontend.es.js"></script> <script type="module" src="/static/dist/personotes-frontend.es.js"></script>
</head> </head>
<body> <body>
<header> <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;"> <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> </button>
<div style="display: flex; align-items: center; gap: 0.75rem;"> <div style="display: flex; align-items: center; gap: 0.75rem;">
<img src="/static/images/logo.svg" alt="Logo" style="width: 40px; height: 40px;"> <img src="/static/images/logo.svg" alt="Logo" style="width: 40px; height: 40px;">
@ -46,10 +48,10 @@
style="white-space: nowrap;" style="white-space: nowrap;"
data-i18n="menu.home" data-i18n="menu.home"
title="Retour à la page d'accueil (Ctrl/Cmd+H)"> title="Retour à la page d'accueil (Ctrl/Cmd+H)">
🏠 Accueil <i data-lucide="home" class="icon-sm"></i> Accueil
</button> </button>
<button onclick="showNewNoteModal()" style="white-space: nowrap;" data-i18n="menu.newNote" title="Créer une nouvelle note (Ctrl/Cmd+N)"> <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> </button>
</header> </header>
@ -57,7 +59,7 @@
<div id="new-note-modal" style="display: none;"> <div id="new-note-modal" style="display: none;">
<div class="modal-overlay" onclick="hideNewNoteModal()"></div> <div class="modal-overlay" onclick="hideNewNoteModal()"></div>
<div class="modal-content"> <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)"> <form onsubmit="handleNewNote(event)">
<label for="note-name">Nom de la note</label> <label for="note-name">Nom de la note</label>
<input <input
@ -69,7 +71,7 @@
required required
/> />
<p style="color: var(--text-muted); font-size: 0.85rem; margin-top: 0.5rem;"> <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> </p>
<div style="display: flex; gap: 0.5rem; margin-top: 1.5rem;"> <div style="display: flex; gap: 0.5rem; margin-top: 1.5rem;">
<button type="submit">Créer / Ouvrir</button> <button type="submit">Créer / Ouvrir</button>
@ -83,7 +85,7 @@
<div id="new-folder-modal" style="display: none;"> <div id="new-folder-modal" style="display: none;">
<div class="modal-overlay" onclick="hideNewFolderModal()"></div> <div class="modal-overlay" onclick="hideNewFolderModal()"></div>
<div class="modal-content"> <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)"> <form onsubmit="handleNewFolder(event)">
<label for="folder-name">Nom du dossier</label> <label for="folder-name">Nom du dossier</label>
<input <input
@ -95,7 +97,7 @@
required required
/> />
<p style="color: var(--text-muted); font-size: 0.85rem; margin-top: 0.5rem;"> <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> </p>
<div style="display: flex; gap: 0.5rem; margin-top: 1.5rem;"> <div style="display: flex; gap: 0.5rem; margin-top: 1.5rem;">
<button type="submit">Créer</button> <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> <span id="selection-count" class="selection-count">0 élément(s) sélectionné(s)</span>
<div class="toolbar-actions"> <div class="toolbar-actions">
<button onclick="deleteSelected()" class="danger-button"> <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"> <i data-lucide="trash-2" class="icon-sm"></i>
<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>
Supprimer Supprimer
</button> </button>
<button onclick="cancelSelection()" class="secondary"> <button onclick="cancelSelection()" class="secondary">
@ -131,13 +130,13 @@
<div id="delete-confirmation-modal" style="display: none;"> <div id="delete-confirmation-modal" style="display: none;">
<div class="modal-overlay" onclick="hideDeleteConfirmationModal()"></div> <div class="modal-overlay" onclick="hideDeleteConfirmationModal()"></div>
<div class="modal-content"> <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> <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);"> <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 --> <!-- Liste des éléments à supprimer -->
</div> </div>
<p style="color: var(--warning); font-size: 0.9rem;"> <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> </p>
<div style="display: flex; gap: 0.5rem; margin-top: 1.5rem;"> <div style="display: flex; gap: 0.5rem; margin-top: 1.5rem;">
<button onclick="confirmDelete()" class="danger-button">Confirmer la suppression</button> <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-overlay" onclick="closeThemeModal()"></div>
<div class="theme-modal-content"> <div class="theme-modal-content">
<h2> <h2>
⚙️ Paramètres d'apparence <i data-lucide="settings" class="icon-md"></i> Paramètres d'apparence
</h2> </h2>
<!-- Onglets --> <!-- Onglets -->
<div class="settings-tabs"> <div class="settings-tabs">
<button class="settings-tab active" onclick="switchSettingsTab('themes')"> <button class="settings-tab active" onclick="switchSettingsTab('themes')">
🎨 Thèmes <i data-lucide="palette" class="icon-sm"></i> Thèmes
</button> </button>
<button class="settings-tab" onclick="switchSettingsTab('fonts')"> <button class="settings-tab" onclick="switchSettingsTab('fonts')">
🔤 Polices <i data-lucide="type" class="icon-sm"></i> Polices
</button> </button>
<button class="settings-tab" onclick="switchSettingsTab('editor')"> <button class="settings-tab" onclick="switchSettingsTab('editor')">
⌨️ Éditeur <i data-lucide="keyboard" class="icon-sm"></i> Éditeur
</button> </button>
<button class="settings-tab" onclick="switchSettingsTab('other')"> <button class="settings-tab" onclick="switchSettingsTab('other')">
⚙️ Autre <i data-lucide="settings" class="icon-sm"></i> Autre
</button> </button>
</div> </div>
@ -176,7 +175,7 @@
<!-- Material Dark --> <!-- Material Dark -->
<div class="theme-card active" data-theme="material-dark" onclick="selectTheme('material-dark')"> <div class="theme-card active" data-theme="material-dark" onclick="selectTheme('material-dark')">
<div class="theme-card-header"> <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> <span class="theme-card-name">Material Dark</span>
</div> </div>
<div class="theme-preview"> <div class="theme-preview">
@ -191,7 +190,7 @@
<!-- Monokai Dark --> <!-- Monokai Dark -->
<div class="theme-card" data-theme="monokai-dark" onclick="selectTheme('monokai-dark')"> <div class="theme-card" data-theme="monokai-dark" onclick="selectTheme('monokai-dark')">
<div class="theme-card-header"> <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> <span class="theme-card-name">Monokai Dark</span>
</div> </div>
<div class="theme-preview"> <div class="theme-preview">
@ -206,7 +205,7 @@
<!-- Dracula --> <!-- Dracula -->
<div class="theme-card" data-theme="dracula" onclick="selectTheme('dracula')"> <div class="theme-card" data-theme="dracula" onclick="selectTheme('dracula')">
<div class="theme-card-header"> <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> <span class="theme-card-name">Dracula</span>
</div> </div>
<div class="theme-preview"> <div class="theme-preview">
@ -221,7 +220,7 @@
<!-- One Dark --> <!-- One Dark -->
<div class="theme-card" data-theme="one-dark" onclick="selectTheme('one-dark')"> <div class="theme-card" data-theme="one-dark" onclick="selectTheme('one-dark')">
<div class="theme-card-header"> <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> <span class="theme-card-name">One Dark</span>
</div> </div>
<div class="theme-preview"> <div class="theme-preview">
@ -236,7 +235,7 @@
<!-- Solarized Dark --> <!-- Solarized Dark -->
<div class="theme-card" data-theme="solarized-dark" onclick="selectTheme('solarized-dark')"> <div class="theme-card" data-theme="solarized-dark" onclick="selectTheme('solarized-dark')">
<div class="theme-card-header"> <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> <span class="theme-card-name">Solarized Dark</span>
</div> </div>
<div class="theme-preview"> <div class="theme-preview">
@ -251,7 +250,7 @@
<!-- Nord --> <!-- Nord -->
<div class="theme-card" data-theme="nord" onclick="selectTheme('nord')"> <div class="theme-card" data-theme="nord" onclick="selectTheme('nord')">
<div class="theme-card-header"> <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> <span class="theme-card-name">Nord</span>
</div> </div>
<div class="theme-preview"> <div class="theme-preview">
@ -266,7 +265,7 @@
<!-- Catppuccin --> <!-- Catppuccin -->
<div class="theme-card" data-theme="catppuccin" onclick="selectTheme('catppuccin')"> <div class="theme-card" data-theme="catppuccin" onclick="selectTheme('catppuccin')">
<div class="theme-card-header"> <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> <span class="theme-card-name">Catppuccin</span>
</div> </div>
<div class="theme-preview"> <div class="theme-preview">
@ -281,7 +280,7 @@
<!-- Everforest --> <!-- Everforest -->
<div class="theme-card" data-theme="everforest" onclick="selectTheme('everforest')"> <div class="theme-card" data-theme="everforest" onclick="selectTheme('everforest')">
<div class="theme-card-header"> <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> <span class="theme-card-name">Everforest</span>
</div> </div>
<div class="theme-preview"> <div class="theme-preview">
@ -301,7 +300,7 @@
<!-- Fira Code --> <!-- Fira Code -->
<div class="font-card" data-font="fira-code" onclick="selectFont('fira-code')"> <div class="font-card" data-font="fira-code" onclick="selectFont('fira-code')">
<div class="font-card-header"> <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> <span class="font-card-name">Fira Code</span>
</div> </div>
<div class="font-preview" style="font-family: 'Fira Code', monospace;"> <div class="font-preview" style="font-family: 'Fira Code', monospace;">
@ -313,7 +312,7 @@
<!-- Sans-serif --> <!-- Sans-serif -->
<div class="font-card" data-font="sans-serif" onclick="selectFont('sans-serif')"> <div class="font-card" data-font="sans-serif" onclick="selectFont('sans-serif')">
<div class="font-card-header"> <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> <span class="font-card-name">Sans-serif</span>
</div> </div>
<div class="font-preview" style="font-family: -apple-system, sans-serif;"> <div class="font-preview" style="font-family: -apple-system, sans-serif;">
@ -325,7 +324,7 @@
<!-- Inter --> <!-- Inter -->
<div class="font-card" data-font="inter" onclick="selectFont('inter')"> <div class="font-card" data-font="inter" onclick="selectFont('inter')">
<div class="font-card-header"> <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> <span class="font-card-name">Inter</span>
</div> </div>
<div class="font-preview" style="font-family: 'Inter', sans-serif;"> <div class="font-preview" style="font-family: 'Inter', sans-serif;">
@ -337,7 +336,7 @@
<!-- Poppins --> <!-- Poppins -->
<div class="font-card" data-font="poppins" onclick="selectFont('poppins')"> <div class="font-card" data-font="poppins" onclick="selectFont('poppins')">
<div class="font-card-header"> <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> <span class="font-card-name">Poppins</span>
</div> </div>
<div class="font-preview" style="font-family: 'Poppins', sans-serif;"> <div class="font-preview" style="font-family: 'Poppins', sans-serif;">
@ -349,7 +348,7 @@
<!-- Public Sans --> <!-- Public Sans -->
<div class="font-card" data-font="public-sans" onclick="selectFont('public-sans')"> <div class="font-card" data-font="public-sans" onclick="selectFont('public-sans')">
<div class="font-card-header"> <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> <span class="font-card-name">Public Sans</span>
</div> </div>
<div class="font-preview" style="font-family: 'Public Sans', sans-serif;"> <div class="font-preview" style="font-family: 'Public Sans', sans-serif;">
@ -361,7 +360,7 @@
<!-- JetBrains Mono --> <!-- JetBrains Mono -->
<div class="font-card active" data-font="jetbrains-mono" onclick="selectFont('jetbrains-mono')"> <div class="font-card active" data-font="jetbrains-mono" onclick="selectFont('jetbrains-mono')">
<div class="font-card-header"> <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> <span class="font-card-name">JetBrains Mono</span>
</div> </div>
<div class="font-preview" style="font-family: 'JetBrains Mono', monospace;"> <div class="font-preview" style="font-family: 'JetBrains Mono', monospace;">
@ -373,7 +372,7 @@
<!-- Cascadia Code --> <!-- Cascadia Code -->
<div class="font-card" data-font="cascadia-code" onclick="selectFont('cascadia-code')"> <div class="font-card" data-font="cascadia-code" onclick="selectFont('cascadia-code')">
<div class="font-card-header"> <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> <span class="font-card-name">Cascadia Code</span>
</div> </div>
<div class="font-preview" style="font-family: 'Cascadia Code', monospace;"> <div class="font-preview" style="font-family: 'Cascadia Code', monospace;">
@ -385,7 +384,7 @@
<!-- Source Code Pro --> <!-- Source Code Pro -->
<div class="font-card" data-font="source-code-pro" onclick="selectFont('source-code-pro')"> <div class="font-card" data-font="source-code-pro" onclick="selectFont('source-code-pro')">
<div class="font-card-header"> <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> <span class="font-card-name">Source Code Pro</span>
</div> </div>
<div class="font-preview" style="font-family: 'Source Code Pro', monospace;"> <div class="font-preview" style="font-family: 'Source Code Pro', monospace;">
@ -397,7 +396,7 @@
<!-- Sélecteur de taille de police --> <!-- 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);"> <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"> <div class="font-size-selector">
<button class="font-size-option" data-size="small" onclick="selectFontSize('small')"> <button class="font-size-option" data-size="small" onclick="selectFontSize('small')">
<span class="size-label">Petite</span> <span class="size-label">Petite</span>
@ -421,7 +420,7 @@
<!-- Section Éditeur --> <!-- Section Éditeur -->
<div id="editor-section" class="settings-section" style="display: none;"> <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 --> <!-- 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);"> <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);"> <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;"> <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> </p>
</div> </div>
</div> </div>
<!-- Section Autre (Langue) --> <!-- Section Autre (Langue) -->
<div id="other-section" class="settings-section" style="display: none;"> <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);"> <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;"> <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;"> <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="flex: 1;">
<div style="font-weight: 500; color: var(--text-primary); margin-bottom: 0.25rem; font-size: 1rem;"> <div style="font-weight: 500; color: var(--text-primary); margin-bottom: 0.25rem; font-size: 1rem;">
🇬🇧 English English
</div> </div>
<div style="font-size: 0.85rem; color: var(--text-secondary);"> <div style="font-size: 0.85rem; color: var(--text-secondary);">
English interface 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;"> <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="flex: 1;">
<div style="font-weight: 500; color: var(--text-primary); margin-bottom: 0.25rem; font-size: 1rem;"> <div style="font-weight: 500; color: var(--text-primary); margin-bottom: 0.25rem; font-size: 1rem;">
🇫🇷 Français Français
</div> </div>
<div style="font-size: 0.85rem; color: var(--text-secondary);"> <div style="font-size: 0.85rem; color: var(--text-secondary);">
Interface en français Interface en français
@ -489,10 +488,10 @@
<div class="main-layout"> <div class="main-layout">
<aside id="sidebar"> <aside id="sidebar">
<button class="sidebar-close-btn" onclick="toggleSidebar()" title="Fermer le menu"> <button class="sidebar-close-btn" onclick="toggleSidebar()" title="Fermer le menu">
<i data-lucide="x" class="icon-md"></i>
</button> </button>
<section> <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"> <div id="search-results">
<!-- Les résultats de la recherche apparaîtront ici --> <!-- Les résultats de la recherche apparaîtront ici -->
</div> </div>
@ -503,7 +502,7 @@
<section> <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;"> <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> <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>
<div class="sidebar-section-content" id="favorites-content" style="display: block;"> <div class="sidebar-section-content" id="favorites-content" style="display: block;">
<div id="favorites-list" <div id="favorites-list"
@ -521,7 +520,7 @@
<section> <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;"> <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> <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>
<div class="sidebar-section-content" id="daily-notes-content" style="display: block;"> <div class="sidebar-section-content" id="daily-notes-content" style="display: block;">
<div id="daily-calendar-container" <div id="daily-calendar-container"
@ -546,12 +545,9 @@
<section> <section>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--spacing-sm);"> <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"> <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"> <i data-lucide="check-square" style="width: 18px; height: 18px;"></i>
<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>
</button> </button>
</div> </div>
<div id="file-tree" hx-get="/api/tree" hx-trigger="load once delay:250ms" hx-swap="innerHTML"> <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 --> <!-- 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)"> <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> </button>
<!-- Boutons du bas de la sidebar --> <!-- Boutons du bas de la sidebar -->
<div style="display: flex; gap: 0.5rem; align-items: stretch;"> <div style="display: flex; gap: 0.5rem; align-items: stretch;">
<!-- Bouton Paramètres (thèmes) --> <!-- 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;"> <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"> <i data-lucide="settings" style="width: 16px; height: 16px;"></i>
<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>
<span data-i18n="settings.title">Paramètres</span> <span data-i18n="settings.title">Paramètres</span>
</button> </button>
<!-- Bouton À propos --> <!-- Bouton À propos -->
<button <button
class="sidebar-action-btn" class="sidebar-action-btn"
title="À propos de PersoNotes" title="À propos de PersoNotes"
hx-get="/api/about" hx-get="/api/about"
hx-target="#editor-container" hx-target="#editor-container"
hx-swap="innerHTML" hx-swap="innerHTML"
style="display: flex; align-items: center; justify-content: center; padding: 0.6rem; min-width: auto; opacity: 0.7;"> 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"> <i data-lucide="info" style="width: 16px; height: 16px;"></i>
<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>
</button> </button>
</div> </div>
</aside> </aside>
@ -598,9 +587,7 @@
hx-trigger="load once" hx-trigger="load once"
hx-swap="innerHTML"> 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);"> <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"> <i data-lucide="file-text" style="width: 64px; height: 64px; margin-bottom: 1rem; opacity: 0.5;"></i>
<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>
<p style="font-size: 1.1rem; margin: 0;">Chargement...</p> <p style="font-size: 1.1rem; margin: 0;">Chargement...</p>
</div> </div>
</div> </div>
@ -695,5 +682,33 @@
} }
}); });
</script> </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> </body>
</html> </html>

View File

@ -12,7 +12,7 @@
hx-target="#editor-container" hx-target="#editor-container"
hx-swap="innerHTML" hx-swap="innerHTML"
hx-push-url="true"> 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-content">
<div class="search-result-header"> <div class="search-result-header">
<span class="search-result-title">{{.Title}}</span> <span class="search-result-title">{{.Title}}</span>
@ -40,14 +40,14 @@
</ul> </ul>
{{else}} {{else}}
<div class="search-no-results"> <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-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> <p class="search-no-results-hint">Essayez d'autres mots-clés ou utilisez les filtres</p>
</div> </div>
{{end}} {{end}}
{{else}} {{else}}
<div class="search-help"> <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> <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-examples">
<div class="search-help-example"> <div class="search-help-example">