@ -10,7 +10,9 @@
|
||||
"Bash(/home/mathieu/git/project-notes/notes/test-delete-1.md)",
|
||||
"Bash(/home/mathieu/git/project-notes/notes/test-delete-2.md)",
|
||||
"Bash(/home/mathieu/git/project-notes/notes/test-delete-folder/test.md)",
|
||||
"Bash(npm install)"
|
||||
"Bash(npm install)",
|
||||
"Bash(go get:*)",
|
||||
"Bash(ls:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
||||
377
API.md
377
API.md
@ -6,6 +6,7 @@ Base URL: `http://localhost:8080/api/v1`
|
||||
## Table des matières
|
||||
|
||||
- [Vue d'ensemble](#vue-densemble)
|
||||
- [Commandes CLI](#commandes-cli)
|
||||
- [Authentification](#authentification)
|
||||
- [Formats de données](#formats-de-données)
|
||||
- [Endpoints](#endpoints)
|
||||
@ -13,6 +14,9 @@ Base URL: `http://localhost:8080/api/v1`
|
||||
- [Récupérer une note](#récupérer-une-note)
|
||||
- [Créer/Mettre à jour une note](#créermettre-à-jour-une-note)
|
||||
- [Supprimer une note](#supprimer-une-note)
|
||||
- [Lister les notes publiques](#lister-les-notes-publiques)
|
||||
- [Basculer le statut public d'une note](#basculer-le-statut-public-dune-note)
|
||||
- [Notes publiques](#notes-publiques)
|
||||
- [Codes de statut HTTP](#codes-de-statut-http)
|
||||
- [Exemples d'utilisation](#exemples-dutilisation)
|
||||
|
||||
@ -38,6 +42,69 @@ L'API REST de PersoNotes permet de gérer vos notes Markdown via HTTP. Elle supp
|
||||
|
||||
---
|
||||
|
||||
## Commandes CLI
|
||||
|
||||
Le serveur inclut des commandes CLI intégrées pour gérer les notes publiques sans avoir à lancer le serveur HTTP.
|
||||
|
||||
### Lister les notes publiques
|
||||
|
||||
Affiche toutes les notes qui ont été exportées en HTML public.
|
||||
|
||||
**Commande** :
|
||||
```bash
|
||||
./server list-public [notes-dir]
|
||||
```
|
||||
|
||||
**Arguments** :
|
||||
- `notes-dir` (optionnel) : Chemin vers le répertoire des notes (défaut: `./notes`)
|
||||
|
||||
**Exemple de sortie** :
|
||||
```
|
||||
📚 Notes publiques (4):
|
||||
|
||||
• 2025 Learning Goals
|
||||
Source: personal/learning-goals.md
|
||||
Public: public/learning-goals.html
|
||||
Date: 2025-11-13 20:06:21
|
||||
|
||||
• AI Writing Assistant
|
||||
Source: archive/ai-assistant.md
|
||||
Public: public/ai-assistant.html
|
||||
Date: 2025-11-13 19:43:28
|
||||
|
||||
• API Endpoints Reference
|
||||
Source: documentation/api/endpoints.md
|
||||
Public: public/endpoints.html
|
||||
Date: 2025-11-13 19:36:57
|
||||
|
||||
• Product Backlog
|
||||
Source: tasks/backlog.md
|
||||
Public: public/backlog.html
|
||||
Date: 2025-11-13 20:13:05
|
||||
```
|
||||
|
||||
**Cas particuliers** :
|
||||
- Si aucune note n'est publique : affiche "Aucune note publique."
|
||||
- Si le fichier `.public.json` n'existe pas : affiche "Aucune note publique trouvée."
|
||||
- Erreur si le répertoire n'existe pas
|
||||
|
||||
**Utilisation typique** :
|
||||
```bash
|
||||
# Lister les notes publiques
|
||||
./server list-public
|
||||
|
||||
# Avec un répertoire personnalisé
|
||||
./server list-public /path/to/notes
|
||||
|
||||
# Compter les notes publiques (Linux/macOS)
|
||||
./server list-public | grep -c "^•"
|
||||
|
||||
# Exporter la liste dans un fichier
|
||||
./server list-public > public-notes-list.txt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Authentification
|
||||
|
||||
**Version actuelle : Aucune authentification requise**
|
||||
@ -364,6 +431,313 @@ curl -X DELETE http://localhost:8080/api/v1/notes/projet/old-note.md
|
||||
|
||||
---
|
||||
|
||||
### Lister les notes publiques
|
||||
|
||||
Récupère la liste des notes qui ont été publiées dans l'espace public.
|
||||
|
||||
**Endpoint** : `GET /api/public/list`
|
||||
|
||||
**Paramètres** : Aucun
|
||||
|
||||
**Réponse** : `200 OK`
|
||||
|
||||
```json
|
||||
{
|
||||
"notes": [
|
||||
{
|
||||
"path": "projet/backend.md",
|
||||
"title": "Backend API",
|
||||
"published_at": "2025-11-13T14:30:00Z"
|
||||
},
|
||||
{
|
||||
"path": "personal/guide.md",
|
||||
"title": "Guide d'utilisation",
|
||||
"published_at": "2025-11-13T10:15:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Champs retournés** :
|
||||
- `path` : Chemin relatif de la note source
|
||||
- `title` : Titre de la note
|
||||
- `published_at` : Date/heure de publication (format ISO 8601)
|
||||
|
||||
**Notes** :
|
||||
- Les notes sont triées par date de publication (plus récentes d'abord)
|
||||
- Les fichiers HTML générés se trouvent dans `public/{nom-de-la-note}.html`
|
||||
- Liste vide si aucune note n'est publique
|
||||
|
||||
**Exemple curl** :
|
||||
|
||||
```bash
|
||||
# Lister toutes les notes publiques
|
||||
curl http://localhost:8080/api/public/list
|
||||
|
||||
# Avec formatage jq
|
||||
curl -s http://localhost:8080/api/public/list | jq '.notes[] | "\(.title) -> public/\(.path | split("/")[-1] | sub(".md$"; ".html"))"'
|
||||
```
|
||||
|
||||
**Exemple de sortie formatée** :
|
||||
|
||||
```bash
|
||||
$ curl -s http://localhost:8080/api/public/list | jq '.notes[] | .title'
|
||||
"Backend API"
|
||||
"Guide d'utilisation"
|
||||
"Documentation projet"
|
||||
```
|
||||
|
||||
**Compter les notes publiques** :
|
||||
|
||||
```bash
|
||||
curl -s http://localhost:8080/api/public/list | jq '.notes | length'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Basculer le statut public d'une note
|
||||
|
||||
Publie ou retire une note de l'espace public. Génère automatiquement un fichier HTML statique exportable.
|
||||
|
||||
**Endpoint** : `POST /api/public/toggle`
|
||||
|
||||
**Content-Type** : `application/x-www-form-urlencoded`
|
||||
|
||||
**Paramètres** :
|
||||
- `path` (form) : Chemin relatif de la note (ex: `projet/backend.md`)
|
||||
|
||||
**Réponse** : `200 OK`
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "public",
|
||||
"path": "projet/backend.md"
|
||||
}
|
||||
```
|
||||
|
||||
Valeurs possibles pour `status` :
|
||||
- `"public"` : La note est maintenant publique (HTML généré)
|
||||
- `"private"` : La note est maintenant privée (HTML supprimé)
|
||||
|
||||
**Comportement** :
|
||||
- ✅ **Génère du HTML statique** dans `public/nom-de-la-note.html`
|
||||
- ✅ **Copie les CSS** nécessaires dans `public/static/`
|
||||
- ✅ **Met à jour l'index** dans `public/index.html`
|
||||
- ✅ **Structure plate** : Tous les fichiers dans `public/`, pas de sous-dossiers
|
||||
- ✅ **Portable** : Les fichiers HTML peuvent être copiés sur n'importe quel serveur web
|
||||
|
||||
**Fichiers générés** :
|
||||
```
|
||||
public/
|
||||
├── index.html # Liste de toutes les notes publiques
|
||||
├── backend.md.html # Note convertie en HTML standalone
|
||||
├── autre-note.html
|
||||
└── static/
|
||||
├── theme.css # Styles copiés
|
||||
└── themes.css
|
||||
```
|
||||
|
||||
**Erreurs** :
|
||||
- `400 Bad Request` : Chemin manquant ou invalide
|
||||
- `404 Not Found` : Note inexistante
|
||||
- `405 Method Not Allowed` : Méthode autre que POST
|
||||
- `500 Internal Server Error` : Erreur de génération HTML
|
||||
|
||||
**Exemple curl** :
|
||||
|
||||
```bash
|
||||
# Publier une note
|
||||
curl -X POST http://localhost:8080/api/public/toggle \
|
||||
-d "path=projet/backend.md"
|
||||
|
||||
# La note est maintenant accessible à :
|
||||
# http://localhost:8080/public/backend.html
|
||||
```
|
||||
|
||||
**Exemple JavaScript** :
|
||||
|
||||
```javascript
|
||||
// Publier une note
|
||||
const response = await fetch('/api/public/toggle', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: new URLSearchParams({ path: 'projet/backend.md' }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
console.log(data.status); // "public" ou "private"
|
||||
```
|
||||
|
||||
**Scripts utiles** :
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Lister les notes publiques avec leurs URLs
|
||||
|
||||
echo "📚 Notes publiques:"
|
||||
curl -s http://localhost:8080/api/public/list | jq -r '.notes[] | "• \(.title)\n → http://localhost:8080/public/\(.path | split("/")[-1] | sub(".md$"; ".html"))\n"'
|
||||
```
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Exporter toutes les notes en HTML public
|
||||
|
||||
echo "Exporting all notes to public HTML..."
|
||||
curl -s http://localhost:8080/api/v1/notes | jq -r '.notes[].path' | while read path; do
|
||||
curl -X POST http://localhost:8080/api/public/toggle -d "path=$path" > /dev/null 2>&1
|
||||
echo "✓ Published: $path"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "✅ Export terminé!"
|
||||
echo "📊 Total: $(curl -s http://localhost:8080/api/public/list | jq '.notes | length') notes publiques"
|
||||
```
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Retirer toutes les notes du public
|
||||
|
||||
echo "Unpublishing all public notes..."
|
||||
curl -s http://localhost:8080/api/public/list | jq -r '.notes[].path' | while read path; do
|
||||
curl -X POST http://localhost:8080/api/public/toggle -d "path=$path" > /dev/null 2>&1
|
||||
echo "✓ Unpublished: $path"
|
||||
done
|
||||
|
||||
echo "✅ Toutes les notes sont maintenant privées"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notes publiques
|
||||
|
||||
Le système de notes publiques génère des fichiers HTML statiques exportables. Cette section explique comment utiliser ces fichiers.
|
||||
|
||||
### Accès aux notes publiques
|
||||
|
||||
**Sur le serveur Personotes** :
|
||||
```
|
||||
http://localhost:8080/public/
|
||||
```
|
||||
|
||||
**Fichiers générés** :
|
||||
- `public/index.html` : Liste de toutes les notes publiques
|
||||
- `public/*.html` : Notes converties en HTML standalone
|
||||
- `public/static/` : CSS et assets
|
||||
|
||||
### Déploiement
|
||||
|
||||
Les fichiers HTML sont complètement autonomes et peuvent être déployés sur :
|
||||
|
||||
#### 1. Serveur web classique
|
||||
|
||||
```bash
|
||||
# Copier sur un serveur Apache/Nginx
|
||||
scp -r public/ user@server.com:/var/www/html/notes/
|
||||
|
||||
# Ou avec rsync
|
||||
rsync -av public/ user@server.com:/var/www/html/notes/
|
||||
```
|
||||
|
||||
**Configuration Nginx** :
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name notes.example.com;
|
||||
root /var/www/html/notes;
|
||||
index index.html;
|
||||
}
|
||||
```
|
||||
|
||||
**Configuration Apache** :
|
||||
```apache
|
||||
<VirtualHost *:80>
|
||||
ServerName notes.example.com
|
||||
DocumentRoot /var/www/html/notes
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
#### 2. GitHub Pages (gratuit)
|
||||
|
||||
```bash
|
||||
cd public/
|
||||
git init
|
||||
git add .
|
||||
git commit -m "Public notes"
|
||||
git remote add origin https://github.com/username/notes-public.git
|
||||
git push -u origin main
|
||||
|
||||
# Activer GitHub Pages dans Settings → Pages → main branch
|
||||
# Vos notes seront accessibles à :
|
||||
# https://username.github.io/notes-public/
|
||||
```
|
||||
|
||||
#### 3. Netlify Drop
|
||||
|
||||
1. Allez sur https://app.netlify.com/drop
|
||||
2. Glissez-déposez le dossier `public/`
|
||||
3. Netlify génère automatiquement une URL
|
||||
|
||||
#### 4. Vercel
|
||||
|
||||
```bash
|
||||
cd public/
|
||||
npx vercel
|
||||
```
|
||||
|
||||
### Automatisation de l'export
|
||||
|
||||
**Script de synchronisation** :
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# sync-public.sh - Synchroniser les notes publiques vers un serveur
|
||||
|
||||
REMOTE_USER="user"
|
||||
REMOTE_HOST="server.com"
|
||||
REMOTE_PATH="/var/www/html/notes"
|
||||
|
||||
rsync -av --delete public/ ${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_PATH}
|
||||
|
||||
echo "✅ Notes publiques synchronisées !"
|
||||
```
|
||||
|
||||
**Git Hook automatique** :
|
||||
|
||||
```bash
|
||||
# .git/hooks/post-commit
|
||||
#!/bin/bash
|
||||
|
||||
# Si le dossier public/ a changé, synchroniser
|
||||
if git diff --name-only HEAD~1 | grep -q "^public/"; then
|
||||
./sync-public.sh
|
||||
fi
|
||||
```
|
||||
|
||||
### Avantages
|
||||
|
||||
- ✅ **Performance** : HTML pré-généré = chargement instantané
|
||||
- ✅ **Sécurité** : Fichiers statiques = surface d'attaque minimale
|
||||
- ✅ **Portabilité** : Fonctionne sur n'importe quel serveur web
|
||||
- ✅ **Gratuit** : Hébergement possible sur GitHub Pages, Netlify
|
||||
- ✅ **SEO** : HTML pré-rendu = indexation optimale par Google
|
||||
- ✅ **Pas de backend** : Pas besoin de Go sur le serveur de destination
|
||||
|
||||
### Limitations
|
||||
|
||||
- ⚠️ **Noms uniques** : Si deux notes dans différents dossiers ont le même nom (ex: `personal/test.md` et `work/test.md`), elles s'écraseront car la structure est plate
|
||||
- ⚠️ **Republication manuelle** : Si vous modifiez une note déjà publique, vous devez la republier pour régénérer le HTML
|
||||
- ⚠️ **Pas de recherche** : Les fichiers HTML n'incluent pas de fonction de recherche (uniquement consultables)
|
||||
|
||||
### Documentation complète
|
||||
|
||||
Pour plus d'informations sur l'export des notes publiques :
|
||||
- **QUICK_START_PUBLIC.md** : Guide de démarrage rapide
|
||||
- **EXPORT_GUIDE.md** : Guide complet de déploiement
|
||||
|
||||
---
|
||||
|
||||
## Codes de statut HTTP
|
||||
|
||||
| Code | Signification | Description |
|
||||
@ -556,13 +930,14 @@ echo "$STATS" | jq -r '.notes[].tags[]' | sort | uniq -c | sort -rn | head -5
|
||||
|
||||
## Changelog
|
||||
|
||||
### v1 (2025-11-10)
|
||||
### v1 (2025-11-13)
|
||||
- ✨ Première version de l'API REST
|
||||
- ✅ Endpoints: LIST, GET, PUT, DELETE
|
||||
- ✅ Content negotiation JSON/Markdown
|
||||
- ✅ Support sous-dossiers
|
||||
- ✅ Gestion automatique du front matter
|
||||
- ✅ Ré-indexation automatique
|
||||
- ✅ Export de notes publiques en HTML statique (POST /api/public/toggle)
|
||||
|
||||
---
|
||||
|
||||
|
||||
155
CLAUDE.md
155
CLAUDE.md
@ -9,11 +9,13 @@ A lightweight web-based Markdown note-taking application with a Go backend and m
|
||||
**Key Features**:
|
||||
- **Daily Notes**: Quick daily journaling with interactive calendar, keyboard shortcuts (Ctrl/Cmd+D), and structured templates
|
||||
- **Favorites System**: Star important notes and folders for quick access from the sidebar
|
||||
- **Public Notes**: Share selected notes publicly without authentication via server-rendered HTML pages
|
||||
- **Note Linking**: Create links between notes with `/link` command and fuzzy search modal
|
||||
- **Vim Mode**: Full Vim keybindings support (hjkl navigation, modes, commands) for power users
|
||||
- **Multiple Themes**: 8 dark themes (Material Dark, Monokai, Dracula, One Dark, Solarized, Nord, Catppuccin, Everforest)
|
||||
- **Font Customization**: 8 fonts (JetBrains Mono, Fira Code, Inter, etc.) with 4 size options
|
||||
- **Keyboard Shortcuts**: 10+ global shortcuts for navigation, editing, and productivity
|
||||
- **Internationalization**: Full i18n support with English and French translations, automatic language detection
|
||||
|
||||
**Recent Modernization**: The project has been migrated from a simple textarea editor to CodeMirror 6, with a Vite build system for frontend modules. The backend remains unchanged, maintaining the same Go architecture with htmx for dynamic interactions.
|
||||
|
||||
@ -21,31 +23,41 @@ A lightweight web-based Markdown note-taking application with a Go backend and m
|
||||
|
||||
### Backend (Go)
|
||||
|
||||
Four main packages under `internal/`:
|
||||
Five main packages under `internal/`:
|
||||
- **indexer**: Maintains an in-memory index mapping tags to note files. Parses YAML front matter from `.md` files to build the index. Thread-safe with RWMutex.
|
||||
- **watcher**: Uses `fsnotify` to monitor filesystem changes and trigger re-indexing with 200ms debounce. Recursively watches all subdirectories.
|
||||
- **i18n**: Internationalization support with JSON-based translations. Loads translations at startup and provides `T()` function for translation lookups with variable interpolation.
|
||||
- **api**: HTTP handlers that serve templates and handle CRUD operations on notes. Updates front matter automatically on save.
|
||||
- `handler.go` - Main HTML endpoints for the web interface
|
||||
- `rest_handler.go` - REST API endpoints (v1)
|
||||
- `daily_notes.go` - Daily note creation and calendar functionality
|
||||
- `favorites.go` - Favorites management (star/unstar notes and folders)
|
||||
- `public.go` - Public notes sharing with server-side HTML rendering
|
||||
|
||||
The server (`cmd/server/main.go`) coordinates these components:
|
||||
1. Loads initial index from notes directory
|
||||
2. Starts filesystem watcher for automatic re-indexing
|
||||
3. Pre-parses HTML templates from `templates/`
|
||||
4. Serves routes:
|
||||
2. Loads translations from `locales/` directory (JSON files for each language)
|
||||
3. Starts filesystem watcher for automatic re-indexing
|
||||
4. Pre-parses HTML templates from `templates/`
|
||||
5. Serves routes:
|
||||
- `/` (main page)
|
||||
- `/api/v1/notes` and `/api/v1/notes/*` (REST API - JSON responses)
|
||||
- `/api/i18n/{lang}` (Translation JSON endpoint)
|
||||
- `/api/search` (HTML search results)
|
||||
- `/api/notes/*` (HTML editor for notes)
|
||||
- `/api/tree` (HTML file tree)
|
||||
- `/api/folders/create` (Folder management)
|
||||
- `/api/files/move` (File/folder moving)
|
||||
- `/api/home` (Home page)
|
||||
- `/api/daily-notes/*` (Daily note creation and calendar)
|
||||
- `/api/favorites/*` (Favorites management)
|
||||
5. Handles static files from `static/` directory
|
||||
- `/api/about` (About page)
|
||||
- `/api/daily` and `/api/daily/*` (Daily note creation and calendar)
|
||||
- `/api/favorites` (Favorites management)
|
||||
- `/api/folder/*` (Folder view)
|
||||
- `/public` (Public notes list - no auth required)
|
||||
- `/public/view/*` (View public note - no auth required)
|
||||
- `/api/public/toggle` (Toggle public status - requires auth in production)
|
||||
- `/api/public/list` (List public notes JSON)
|
||||
6. Handles static files from `static/` directory and `public/` directory (for public HTML exports)
|
||||
|
||||
### Frontend
|
||||
|
||||
@ -69,9 +81,14 @@ frontend/src/
|
||||
├── file-tree.js # Drag-and-drop file organization
|
||||
├── favorites.js # Favorites system (star/unstar functionality)
|
||||
├── daily-notes.js # Daily notes creation and calendar widget
|
||||
├── public-toggle.js # Public/private status toggle for notes
|
||||
├── keyboard-shortcuts.js # Global keyboard shortcuts management
|
||||
├── theme-manager.js # Theme switching and persistence
|
||||
├── font-manager.js # Font selection and size management
|
||||
├── i18n.js # i18n client with translation function and language detection
|
||||
├── language-manager.js # Language selector UI and page translation
|
||||
├── sidebar-sections.js # Sidebar sections management (Recent/Favorites/All notes)
|
||||
├── debug.js # Debug utilities
|
||||
└── ui.js # Sidebar toggle functionality
|
||||
```
|
||||
|
||||
@ -289,6 +306,33 @@ The favorites system allows quick access to frequently used notes and folders:
|
||||
|
||||
Favorites are loaded on server startup and updated in real-time via htmx.
|
||||
|
||||
### Public Notes
|
||||
|
||||
**Implementation**: `internal/api/public.go` and `frontend/src/public-toggle.js`
|
||||
|
||||
The public notes feature allows sharing selected notes publicly without requiring authentication:
|
||||
|
||||
**Features**:
|
||||
- **Toggle Button**: Click 🔒 Privé/🌐 Public button in the editor to publish/unpublish notes
|
||||
- **Server-side Rendering**: Notes converted to HTML using `goldmark` (Go Markdown library)
|
||||
- **Public Routes**: `/public` (list) and `/public/view/{path}` (individual note) - no auth required
|
||||
- **Persistence**: Public notes list stored in `notes/.public.json`
|
||||
- **GitHub Flavored Markdown**: Supports tables, strikethrough, task lists, syntax highlighting
|
||||
- **SEO-friendly**: Pre-rendered HTML with proper meta tags
|
||||
- **Internationalized**: Full i18n support for public pages
|
||||
|
||||
**Security Model**:
|
||||
- Public routes (`/public*`) are accessible without authentication
|
||||
- Admin routes (`/api/public/toggle`) should be protected by reverse proxy auth (Authelia, Authentik, Basic Auth)
|
||||
- In production, use nginx/Caddy with auth to protect all routes except `/public*`
|
||||
|
||||
**Command-line tool**:
|
||||
```bash
|
||||
go run ./cmd/server list-public # List all published notes
|
||||
```
|
||||
|
||||
**Documentation**: See `PUBLIC_NOTES.md` for complete security recommendations and usage guide.
|
||||
|
||||
### Note Format
|
||||
|
||||
Notes have YAML front matter with these fields:
|
||||
@ -372,6 +416,7 @@ go mod tidy
|
||||
Key backend dependencies:
|
||||
- `github.com/fsnotify/fsnotify` - Filesystem watcher
|
||||
- `gopkg.in/yaml.v3` - YAML parsing for front matter
|
||||
- `github.com/yuin/goldmark` - Markdown to HTML conversion for public notes (with GFM extensions)
|
||||
|
||||
### Vite Build System
|
||||
|
||||
@ -510,6 +555,65 @@ Themes are applied via CSS custom properties and persist in localStorage.
|
||||
|
||||
Font settings apply to both the editor and preview pane.
|
||||
|
||||
### Internationalization (i18n)
|
||||
|
||||
**Implementation**: `internal/i18n/i18n.go`, `frontend/src/i18n.js`, `frontend/src/language-manager.js`
|
||||
|
||||
The application provides full internationalization support:
|
||||
|
||||
#### Backend (Go)
|
||||
- **Package**: `internal/i18n/i18n.go` - Translation engine with JSON-based translation files
|
||||
- **Storage**: Translation files in `locales/` directory (e.g., `en.json`, `fr.json`)
|
||||
- **Loading**: Translations loaded once at server startup from `locales/` directory
|
||||
- **Translation Function**: `T(lang, key, vars...)` - Looks up translation keys with optional variable interpolation
|
||||
- **API Endpoint**: `/api/i18n/{lang}` - Serves translation JSON for frontend consumption
|
||||
- **Helper Functions**: `getLanguage(r)` and `t(r, key, vars...)` in handler.go for request-scoped translations
|
||||
|
||||
#### Frontend (JavaScript)
|
||||
- **Module**: `frontend/src/i18n.js` - Client-side translation with automatic language detection
|
||||
- **Detection Order**:
|
||||
1. localStorage (`language` key)
|
||||
2. Browser language (navigator.language)
|
||||
3. Default to English
|
||||
- **Translation Function**: `t(key, vars)` - Looks up translations with variable interpolation
|
||||
- **Page Translation**: `translatePage()` - Automatically translates elements with `data-i18n` attributes
|
||||
- **Language Manager**: `frontend/src/language-manager.js` - UI for language selection in Settings modal
|
||||
- **Persistence**: Language preference stored in localStorage
|
||||
- **Reload**: Automatic page translation on language change
|
||||
|
||||
#### Available Languages
|
||||
- **English** (en) - Default language
|
||||
- **French** (fr) - Full translation
|
||||
|
||||
#### Adding New Languages
|
||||
1. Create new JSON file in `locales/` (e.g., `locales/es.json`)
|
||||
2. Copy structure from `en.json` with 200+ translation keys
|
||||
3. Translate all keys maintaining the nested structure
|
||||
4. Add language option to Settings modal in templates
|
||||
5. Restart server to load new translations
|
||||
|
||||
#### Translation Key Structure
|
||||
```
|
||||
app.name → "Personotes"
|
||||
menu.home → "Home" / "Accueil"
|
||||
editor.save → "Save" / "Sauvegarder"
|
||||
editor.confirmDelete → "Are you sure...?" (supports {{filename}} interpolation)
|
||||
errors.methodNotAllowed → "Method not allowed" / "Méthode non supportée"
|
||||
```
|
||||
|
||||
#### Variable Interpolation
|
||||
```javascript
|
||||
// JavaScript
|
||||
t('editor.confirmDelete', { filename: 'test.md' })
|
||||
// → "Are you sure you want to delete this note (test.md)?"
|
||||
|
||||
// Go
|
||||
h.t(r, "editor.confirmDelete", map[string]string{"filename": "test.md"})
|
||||
// → "Êtes-vous sûr de vouloir supprimer cette note (test.md) ?"
|
||||
```
|
||||
|
||||
**Documentation**: See `I18N_IMPLEMENTATION.md` for complete implementation details.
|
||||
|
||||
### Vim Mode
|
||||
|
||||
**Implementation**: `frontend/src/vim-mode-manager.js` using `@replit/codemirror-vim`
|
||||
@ -565,6 +669,13 @@ File path validation in `handler.go` and `rest_handler.go`:
|
||||
- CORS not configured (same-origin only)
|
||||
- No rate limiting (add middleware if needed)
|
||||
|
||||
**Public Notes Security**:
|
||||
- Routes `/public` and `/public/view/*` are intentionally public (no auth required)
|
||||
- Route `/api/public/toggle` MUST be protected in production (requires auth to publish/unpublish)
|
||||
- Use reverse proxy (nginx with Authelia/Authentik, or Basic Auth) to protect admin routes
|
||||
- Public HTML is server-rendered with goldmark (prevents XSS, safe rendering)
|
||||
- See `PUBLIC_NOTES.md` for complete security setup guide
|
||||
|
||||
### Template System
|
||||
|
||||
Templates are pre-parsed at startup. The API handler returns HTML fragments that htmx inserts into the page. Out-of-band swaps update the file tree sidebar without full page reload.
|
||||
@ -752,10 +863,14 @@ personotes/
|
||||
│ │ ├── handler.go # HTTP handlers for CRUD operations
|
||||
│ │ ├── rest_handler.go # REST API v1 endpoints
|
||||
│ │ ├── daily_notes.go # Daily notes functionality
|
||||
│ │ └── favorites.go # Favorites management
|
||||
│ │ ├── favorites.go # Favorites management
|
||||
│ │ └── public.go # Public notes sharing
|
||||
│ ├── indexer/
|
||||
│ │ ├── indexer.go # Note indexing and search
|
||||
│ │ └── indexer_test.go # Indexer tests
|
||||
│ ├── i18n/
|
||||
│ │ ├── i18n.go # Internationalization engine
|
||||
│ │ └── i18n_test.go # i18n tests
|
||||
│ └── watcher/
|
||||
│ └── watcher.go # Filesystem watcher with fsnotify
|
||||
├── frontend/ # Frontend build system
|
||||
@ -767,9 +882,15 @@ personotes/
|
||||
│ │ ├── file-tree.js # Drag-and-drop file tree
|
||||
│ │ ├── favorites.js # Favorites system
|
||||
│ │ ├── daily-notes.js # Daily notes and calendar widget
|
||||
│ │ ├── public-toggle.js # Public/private status toggle
|
||||
│ │ ├── keyboard-shortcuts.js # Global keyboard shortcuts
|
||||
│ │ ├── theme-manager.js # Theme switching
|
||||
│ │ ├── font-manager.js # Font customization
|
||||
│ │ ├── i18n.js # i18n client and translation
|
||||
│ │ ├── language-manager.js # Language selector UI
|
||||
│ │ ├── link-inserter.js # Note linking modal
|
||||
│ │ ├── sidebar-sections.js # Sidebar sections management
|
||||
│ │ ├── debug.js # Debug utilities
|
||||
│ │ └── ui.js # Sidebar toggle
|
||||
│ ├── package.json # NPM dependencies
|
||||
│ ├── package-lock.json
|
||||
@ -785,11 +906,20 @@ personotes/
|
||||
│ ├── file-tree.html # File tree sidebar
|
||||
│ ├── search-results.html # Search results
|
||||
│ └── new-note-prompt.html # New note modal
|
||||
├── locales/ # i18n translation files
|
||||
│ ├── en.json # English translations (200+ keys)
|
||||
│ ├── fr.json # French translations (200+ keys)
|
||||
│ └── README.md # Translation guide
|
||||
├── notes/ # Note storage directory
|
||||
│ ├── *.md # Markdown files with YAML front matter
|
||||
│ ├── daily/ # Daily notes (YYYY-MM-DD.md)
|
||||
│ ├── .favorites.json # Favorites list (auto-generated)
|
||||
│ ├── .public.json # Public notes list (auto-generated)
|
||||
│ └── daily-note-template.md # Optional daily note template
|
||||
├── public/ # Public HTML exports (auto-generated)
|
||||
│ ├── index.html # Public notes list page
|
||||
│ ├── *.html # Individual public notes (HTML)
|
||||
│ └── static/ # Static assets for public pages
|
||||
├── docs/ # Documentation
|
||||
│ ├── KEYBOARD_SHORTCUTS.md # Keyboard shortcuts reference
|
||||
│ ├── DAILY_NOTES.md # Daily notes guide
|
||||
@ -798,6 +928,8 @@ personotes/
|
||||
├── go.mod # Go dependencies
|
||||
├── go.sum
|
||||
├── API.md # REST API documentation
|
||||
├── I18N_IMPLEMENTATION.md # i18n implementation guide
|
||||
├── PUBLIC_NOTES.md # Public notes documentation and security guide
|
||||
└── CLAUDE.md # This file
|
||||
```
|
||||
|
||||
@ -809,8 +941,10 @@ personotes/
|
||||
- `internal/api/rest_handler.go` - REST API v1 endpoints
|
||||
- `internal/api/daily_notes.go` - Daily notes and calendar functionality
|
||||
- `internal/api/favorites.go` - Favorites management
|
||||
- `internal/api/public.go` - Public notes sharing and HTML export
|
||||
- `internal/indexer/indexer.go` - Search and indexing logic
|
||||
- `internal/watcher/watcher.go` - Filesystem monitoring
|
||||
- `internal/i18n/i18n.go` - Internationalization engine
|
||||
|
||||
**Frontend Development**:
|
||||
- `frontend/src/editor.js` - CodeMirror editor, preview, slash commands
|
||||
@ -820,12 +954,17 @@ personotes/
|
||||
- `frontend/src/file-tree.js` - File tree interactions and drag-and-drop
|
||||
- `frontend/src/favorites.js` - Favorites system
|
||||
- `frontend/src/daily-notes.js` - Daily notes creation and calendar widget
|
||||
- `frontend/src/public-toggle.js` - Public/private status toggle
|
||||
- `frontend/src/keyboard-shortcuts.js` - Global keyboard shortcuts
|
||||
- `frontend/src/theme-manager.js` - Theme switching logic
|
||||
- `frontend/src/font-manager.js` - Font customization logic
|
||||
- `frontend/src/i18n.js` - i18n client and translation
|
||||
- `frontend/src/language-manager.js` - Language selector UI
|
||||
- `frontend/src/sidebar-sections.js` - Sidebar sections management
|
||||
- `frontend/src/ui.js` - UI utilities (sidebar toggle)
|
||||
- `static/theme.css` - Styling and theming (8 themes)
|
||||
- `templates/*.html` - HTML templates (Go template syntax)
|
||||
- `locales/*.json` - Translation files (en.json, fr.json)
|
||||
|
||||
**Configuration**:
|
||||
- `frontend/vite.config.js` - Frontend build configuration
|
||||
|
||||
360
EXPORT_GUIDE.md
Normal file
360
EXPORT_GUIDE.md
Normal 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
188
PUBLIC_NOTES.md
Normal 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
119
QUICK_START_PUBLIC.md
Normal 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.
|
||||
@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
@ -10,6 +11,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@ -20,21 +22,82 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Vérifier si une sous-commande est demandée
|
||||
if len(os.Args) > 1 && os.Args[1] == "list-public" {
|
||||
cmdListPublic()
|
||||
return
|
||||
}
|
||||
|
||||
addr := flag.String("addr", ":8080", "Adresse d ecoute HTTP")
|
||||
notesDir := flag.String("notes-dir", "./notes", "Repertoire contenant les notes Markdown")
|
||||
flag.Parse()
|
||||
|
||||
logger := log.New(os.Stdout, "[server] ", log.LstdFlags)
|
||||
|
||||
runServer(*addr, *notesDir, logger)
|
||||
}
|
||||
|
||||
// cmdListPublic liste les notes publiques
|
||||
func cmdListPublic() {
|
||||
notesDir := "./notes"
|
||||
if len(os.Args) > 2 && os.Args[2] != "" {
|
||||
notesDir = os.Args[2]
|
||||
}
|
||||
|
||||
publicFile := filepath.Join(notesDir, ".public.json")
|
||||
|
||||
// Lire le fichier .public.json
|
||||
data, err := os.ReadFile(publicFile)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
fmt.Println("Aucune note publique trouvée.")
|
||||
fmt.Printf("Le fichier %s n'existe pas.\n", publicFile)
|
||||
os.Exit(0)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "Erreur de lecture: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var publicNotes struct {
|
||||
Notes []struct {
|
||||
Path string `json:"path"`
|
||||
Title string `json:"title"`
|
||||
PublishedAt time.Time `json:"published_at"`
|
||||
} `json:"notes"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &publicNotes); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Erreur de parsing JSON: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if len(publicNotes.Notes) == 0 {
|
||||
fmt.Println("Aucune note publique.")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
fmt.Printf("\n📚 Notes publiques (%d):\n\n", len(publicNotes.Notes))
|
||||
for _, note := range publicNotes.Notes {
|
||||
filename := filepath.Base(note.Path)
|
||||
htmlFile := filename[:len(filename)-3] + ".html"
|
||||
|
||||
fmt.Printf("• %s\n", note.Title)
|
||||
fmt.Printf(" Source: %s\n", note.Path)
|
||||
fmt.Printf(" Public: public/%s\n", htmlFile)
|
||||
fmt.Printf(" Date: %s\n\n", note.PublishedAt.Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
}
|
||||
|
||||
func runServer(addr, notesDir string, logger *log.Logger) {
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
if err := ensureDir(*notesDir); err != nil {
|
||||
if err := ensureDir(notesDir); err != nil {
|
||||
logger.Fatalf("repertoire notes invalide: %v", err)
|
||||
}
|
||||
|
||||
idx := indexer.New()
|
||||
if err := idx.Load(*notesDir); err != nil {
|
||||
if err := idx.Load(notesDir); err != nil {
|
||||
logger.Fatalf("echec de l indexation initiale: %v", err)
|
||||
}
|
||||
|
||||
@ -45,7 +108,7 @@ func main() {
|
||||
}
|
||||
logger.Printf("traductions chargees: %v", translator.GetAvailableLanguages())
|
||||
|
||||
w, err := watcher.Start(ctx, *notesDir, idx, logger)
|
||||
w, err := watcher.Start(ctx, notesDir, idx, logger)
|
||||
if err != nil {
|
||||
logger.Fatalf("echec du watcher: %v", err)
|
||||
}
|
||||
@ -63,6 +126,12 @@ func main() {
|
||||
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static"))))
|
||||
mux.Handle("/frontend/", http.StripPrefix("/frontend/", http.FileServer(http.Dir("./frontend"))))
|
||||
|
||||
// Servir les fichiers HTML publics (notes exportées)
|
||||
publicDir := filepath.Join(notesDir, "..", "public")
|
||||
if _, err := os.Stat(publicDir); err == nil {
|
||||
mux.Handle("/public/", http.StripPrefix("/public/", http.FileServer(http.Dir(publicDir))))
|
||||
}
|
||||
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" {
|
||||
http.NotFound(w, r)
|
||||
@ -77,7 +146,7 @@ func main() {
|
||||
}
|
||||
})
|
||||
|
||||
apiHandler := api.NewHandler(*notesDir, idx, templates, logger, translator)
|
||||
apiHandler := api.NewHandler(notesDir, idx, templates, logger, translator)
|
||||
mux.Handle("/api/i18n/", apiHandler) // I18n translations
|
||||
mux.Handle("/api/v1/notes", apiHandler) // REST API v1
|
||||
mux.Handle("/api/v1/notes/", apiHandler) // REST API v1
|
||||
@ -93,16 +162,18 @@ func main() {
|
||||
mux.Handle("/api/folder/", apiHandler) // Folder view
|
||||
mux.Handle("/api/notes/", apiHandler)
|
||||
mux.Handle("/api/tree", apiHandler)
|
||||
mux.Handle("/api/public/list", apiHandler) // List public notes
|
||||
mux.Handle("/api/public/toggle", apiHandler) // Toggle public status (génère HTML statique)
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: *addr,
|
||||
Addr: addr,
|
||||
Handler: mux,
|
||||
ReadTimeout: 15 * time.Second,
|
||||
WriteTimeout: 15 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
}
|
||||
|
||||
logger.Printf("demarrage du serveur sur %s", *addr)
|
||||
logger.Printf("demarrage du serveur sur %s", addr)
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
|
||||
360
docs/EXPORT_GUIDE.md
Normal file
360
docs/EXPORT_GUIDE.md
Normal 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.
|
||||
@ -133,13 +133,13 @@ class FavoritesManager {
|
||||
attachFavoriteButtons() {
|
||||
debug('attachFavoriteButtons: Début...');
|
||||
|
||||
// Supprimer tous les boutons favoris existants pour les recréer avec le bon état
|
||||
document.querySelectorAll('.add-to-favorites').forEach(btn => btn.remove());
|
||||
|
||||
// Ajouter des boutons étoile aux éléments du file tree
|
||||
this.getFavoritesPaths().then(favoritePaths => {
|
||||
debug('Chemins favoris:', favoritePaths);
|
||||
|
||||
// Supprimer tous les boutons favoris existants APRÈS avoir récupéré la liste
|
||||
document.querySelectorAll('.add-to-favorites').forEach(btn => btn.remove());
|
||||
|
||||
// Dossiers
|
||||
const folderHeaders = document.querySelectorAll('.folder-header');
|
||||
debug('Nombre de folder-header trouvés:', folderHeaders.length);
|
||||
@ -153,7 +153,7 @@ class FavoritesManager {
|
||||
if (path) {
|
||||
const button = document.createElement('button');
|
||||
button.className = 'add-to-favorites';
|
||||
button.innerHTML = '⭐';
|
||||
button.innerHTML = '<i data-lucide="star" class="icon-sm"></i>';
|
||||
button.title = 'Ajouter aux favoris';
|
||||
|
||||
// Extraire le nom avant d'ajouter le bouton
|
||||
@ -191,11 +191,11 @@ class FavoritesManager {
|
||||
if (path) {
|
||||
const button = document.createElement('button');
|
||||
button.className = 'add-to-favorites';
|
||||
button.innerHTML = '⭐';
|
||||
button.innerHTML = '<i data-lucide="star" class="icon-sm"></i>';
|
||||
button.title = 'Ajouter aux favoris';
|
||||
|
||||
// Extraire le nom avant d'ajouter le bouton
|
||||
const name = fileItem.textContent.trim().replace('📄', '').trim().replace('.md', '');
|
||||
const name = fileItem.textContent.trim().replace('.md', '');
|
||||
|
||||
button.onclick = (e) => {
|
||||
e.preventDefault();
|
||||
@ -220,6 +220,11 @@ class FavoritesManager {
|
||||
});
|
||||
|
||||
debug('attachFavoriteButtons: Terminé');
|
||||
|
||||
// Initialiser les icônes Lucide après création des boutons
|
||||
if (typeof lucide !== 'undefined') {
|
||||
lucide.createIcons();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,6 +13,9 @@ class FileTree {
|
||||
init() {
|
||||
this.setupEventListeners();
|
||||
|
||||
// Restaurer l'état des dossiers au démarrage
|
||||
setTimeout(() => this.restoreFolderStates(), 500);
|
||||
|
||||
debug('FileTree initialized with event delegation');
|
||||
}
|
||||
|
||||
@ -67,17 +70,60 @@ class FileTree {
|
||||
const children = folderItem.querySelector('.folder-children');
|
||||
const toggle = header.querySelector('.folder-toggle');
|
||||
const icon = header.querySelector('.folder-icon');
|
||||
const folderPath = folderItem.getAttribute('data-path');
|
||||
|
||||
if (children.style.display === 'none') {
|
||||
// Ouvrir le dossier
|
||||
children.style.display = 'block';
|
||||
toggle.classList.add('expanded');
|
||||
icon.textContent = '📂';
|
||||
icon.innerHTML = '<i data-lucide="folder-open" class="icon-sm"></i>';
|
||||
this.saveFolderState(folderPath, true);
|
||||
} else {
|
||||
// Fermer le dossier
|
||||
children.style.display = 'none';
|
||||
toggle.classList.remove('expanded');
|
||||
icon.textContent = '📁';
|
||||
icon.innerHTML = '<i data-lucide="folder" class="icon-sm"></i>';
|
||||
this.saveFolderState(folderPath, false);
|
||||
}
|
||||
}
|
||||
|
||||
saveFolderState(folderPath, isExpanded) {
|
||||
if (!folderPath) return;
|
||||
const expandedFolders = this.getExpandedFolders();
|
||||
if (isExpanded) {
|
||||
expandedFolders.add(folderPath);
|
||||
} else {
|
||||
expandedFolders.delete(folderPath);
|
||||
}
|
||||
localStorage.setItem('expanded-folders', JSON.stringify([...expandedFolders]));
|
||||
}
|
||||
|
||||
getExpandedFolders() {
|
||||
const saved = localStorage.getItem('expanded-folders');
|
||||
return saved ? new Set(JSON.parse(saved)) : new Set();
|
||||
}
|
||||
|
||||
restoreFolderStates() {
|
||||
const expandedFolders = this.getExpandedFolders();
|
||||
document.querySelectorAll('.folder-item').forEach(folderItem => {
|
||||
const folderPath = folderItem.getAttribute('data-path');
|
||||
if (folderPath && expandedFolders.has(folderPath)) {
|
||||
const header = folderItem.querySelector('.folder-header');
|
||||
const children = folderItem.querySelector('.folder-children');
|
||||
const toggle = header?.querySelector('.folder-toggle');
|
||||
const icon = header?.querySelector('.folder-icon');
|
||||
|
||||
if (children && toggle && icon) {
|
||||
children.style.display = 'block';
|
||||
toggle.classList.add('expanded');
|
||||
icon.innerHTML = '<i data-lucide="folder-open" class="icon-sm"></i>';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Réinitialiser les icônes Lucide
|
||||
if (typeof lucide !== 'undefined') {
|
||||
lucide.createIcons();
|
||||
}
|
||||
}
|
||||
|
||||
@ -159,8 +205,9 @@ class FileTree {
|
||||
// Vérifier si le swap concerne le file-tree
|
||||
const target = event.detail?.target;
|
||||
if (target && (target.id === 'file-tree' || target.closest('#file-tree'))) {
|
||||
debug('FileTree: afterSwap detected, updating attributes...');
|
||||
debug('FileTree: afterSwap detected, updating attributes and restoring folder states...');
|
||||
this.updateDraggableAttributes();
|
||||
setTimeout(() => this.restoreFolderStates(), 50);
|
||||
}
|
||||
});
|
||||
|
||||
@ -169,8 +216,9 @@ class FileTree {
|
||||
const target = event.detail?.target;
|
||||
// Ignorer les swaps de statut (auto-save-status, save-status)
|
||||
if (target && target.id === 'file-tree') {
|
||||
debug('FileTree: oobAfterSwap detected, updating attributes...');
|
||||
debug('FileTree: oobAfterSwap detected, updating attributes and restoring folder states...');
|
||||
this.updateDraggableAttributes();
|
||||
setTimeout(() => this.restoreFolderStates(), 50);
|
||||
}
|
||||
});
|
||||
|
||||
@ -181,6 +229,7 @@ class FileTree {
|
||||
setTimeout(() => {
|
||||
this.setupEventListeners();
|
||||
this.updateDraggableAttributes();
|
||||
this.restoreFolderStates();
|
||||
}, 50);
|
||||
});
|
||||
}
|
||||
@ -678,7 +727,7 @@ class SelectionManager {
|
||||
const checkbox = document.querySelector(`.selection-checkbox[data-path="${path}"]`);
|
||||
const isDir = checkbox?.dataset.isDir === 'true';
|
||||
|
||||
li.innerHTML = `${isDir ? '📁' : '📄'} <code>${path}</code>`;
|
||||
li.innerHTML = `${isDir ? '<i data-lucide="folder" class="icon-sm"></i>' : '<i data-lucide="file-text" class="icon-sm"></i>'} <code>${path}</code>`;
|
||||
ul.appendChild(li);
|
||||
});
|
||||
|
||||
|
||||
@ -231,8 +231,10 @@ class I18n {
|
||||
// Create singleton instance
|
||||
export const i18n = new I18n();
|
||||
|
||||
// Export convenience function
|
||||
// Export convenience functions
|
||||
export const t = (key, args) => i18n.t(key, args);
|
||||
export const loadTranslations = () => i18n.loadTranslations();
|
||||
export const translatePage = () => i18n.translatePage();
|
||||
|
||||
// Initialize on import
|
||||
i18n.init().then(() => {
|
||||
|
||||
@ -181,12 +181,12 @@ class LanguageManager {
|
||||
// Header buttons
|
||||
const homeButton = document.querySelector('button[hx-get="/api/home"]');
|
||||
if (homeButton && !homeButton.hasAttribute('data-i18n')) {
|
||||
homeButton.innerHTML = `🏠 ${t('menu.home')}`;
|
||||
homeButton.innerHTML = `<i data-lucide="home" class="icon-sm"></i> ${t('menu.home')}`;
|
||||
}
|
||||
|
||||
const newNoteButton = document.querySelector('header button[onclick="showNewNoteModal()"]');
|
||||
if (newNoteButton && !newNoteButton.hasAttribute('data-i18n')) {
|
||||
newNoteButton.innerHTML = `✨ ${t('menu.newNote')}`;
|
||||
newNoteButton.innerHTML = `<i data-lucide="file-plus" class="icon-sm"></i> ${t('menu.newNote')}`;
|
||||
}
|
||||
|
||||
// Search placeholder
|
||||
@ -199,7 +199,7 @@ class LanguageManager {
|
||||
const newNoteModal = document.getElementById('new-note-modal');
|
||||
if (newNoteModal) {
|
||||
const title = newNoteModal.querySelector('h2');
|
||||
if (title) title.textContent = `📝 ${t('newNoteModal.title')}`;
|
||||
if (title) title.innerHTML = `<i data-lucide="file-text" class="icon-sm"></i> ${t('newNoteModal.title')}`;
|
||||
|
||||
const label = newNoteModal.querySelector('label[for="note-name"]');
|
||||
if (label) label.textContent = t('newNoteModal.label');
|
||||
@ -218,7 +218,7 @@ class LanguageManager {
|
||||
const newFolderModal = document.getElementById('new-folder-modal');
|
||||
if (newFolderModal) {
|
||||
const title = newFolderModal.querySelector('h2');
|
||||
if (title) title.textContent = `📁 ${t('newFolderModal.title')}`;
|
||||
if (title) title.innerHTML = `<i data-lucide="folder-plus" class="icon-sm"></i> ${t('newFolderModal.title')}`;
|
||||
|
||||
const label = newFolderModal.querySelector('label[for="folder-name"]');
|
||||
if (label) label.textContent = t('newFolderModal.label');
|
||||
@ -256,16 +256,16 @@ class LanguageManager {
|
||||
// Theme modal
|
||||
const modalTitle = document.querySelector('.theme-modal-content h2');
|
||||
if (modalTitle) {
|
||||
modalTitle.textContent = `⚙️ ${t('settings.title')}`;
|
||||
modalTitle.innerHTML = `<i data-lucide="settings" class="icon-sm"></i> ${t('settings.title')}`;
|
||||
}
|
||||
|
||||
// Translate tabs
|
||||
const tabs = document.querySelectorAll('.settings-tab');
|
||||
if (tabs.length >= 4) {
|
||||
tabs[0].innerHTML = `🎨 ${t('tabs.themes')}`;
|
||||
tabs[1].innerHTML = `🔤 ${t('tabs.fonts')}`;
|
||||
tabs[2].innerHTML = `⌨️ ${t('tabs.shortcuts')}`;
|
||||
tabs[3].innerHTML = `⚙️ ${t('tabs.other')}`;
|
||||
tabs[0].innerHTML = `<i data-lucide="palette" class="icon-sm"></i> ${t('tabs.themes')}`;
|
||||
tabs[1].innerHTML = `<i data-lucide="type" class="icon-sm"></i> ${t('tabs.fonts')}`;
|
||||
tabs[2].innerHTML = `<i data-lucide="keyboard" class="icon-sm"></i> ${t('tabs.shortcuts')}`;
|
||||
tabs[3].innerHTML = `<i data-lucide="settings" class="icon-sm"></i> ${t('tabs.other')}`;
|
||||
}
|
||||
|
||||
// Translate close button in settings
|
||||
@ -281,20 +281,20 @@ class LanguageManager {
|
||||
if (langSection) {
|
||||
const heading = langSection.querySelector('h3');
|
||||
if (heading) {
|
||||
heading.textContent = `🌍 ${t('languages.title')}`;
|
||||
heading.innerHTML = `<i data-lucide="languages" class="icon-sm"></i> ${t('languages.title')}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Sidebar sections
|
||||
const searchSectionTitle = document.querySelector('.sidebar-section-title');
|
||||
if (searchSectionTitle && searchSectionTitle.textContent.includes('🔍')) {
|
||||
searchSectionTitle.textContent = `🔍 ${t('search.title') || 'Recherche'}`;
|
||||
if (searchSectionTitle && (searchSectionTitle.textContent.includes('🔍') || searchSectionTitle.querySelector('[data-lucide="search"]'))) {
|
||||
searchSectionTitle.innerHTML = `<i data-lucide="search" class="icon-sm"></i> ${t('search.title') || 'Recherche'}`;
|
||||
}
|
||||
|
||||
// Sidebar "Nouveau dossier" button
|
||||
const newFolderBtn = document.querySelector('.folder-create-btn');
|
||||
if (newFolderBtn && !newFolderBtn.hasAttribute('data-i18n')) {
|
||||
newFolderBtn.innerHTML = `📁 ${t('fileTree.newFolder')}`;
|
||||
newFolderBtn.innerHTML = `<i data-lucide="folder-plus" class="icon-sm"></i> ${t('fileTree.newFolder')}`;
|
||||
}
|
||||
|
||||
// Sidebar "Paramètres" button span
|
||||
|
||||
@ -309,7 +309,7 @@ class LinkInserter {
|
||||
this.results = [];
|
||||
this.resultsContainer.innerHTML = `
|
||||
<div class="link-inserter-no-results">
|
||||
<div class="link-inserter-no-results-icon">🔍</div>
|
||||
<div class="link-inserter-no-results-icon"><i data-lucide="search" class="icon-lg"></i></div>
|
||||
<p>Aucune note trouvée pour « <strong>${this.escapeHtml(query)}</strong> »</p>
|
||||
</div>
|
||||
`;
|
||||
@ -318,7 +318,7 @@ class LinkInserter {
|
||||
showError() {
|
||||
this.resultsContainer.innerHTML = `
|
||||
<div class="link-inserter-error">
|
||||
<div class="link-inserter-error-icon">⚠️</div>
|
||||
<div class="link-inserter-error-icon"><i data-lucide="alert-triangle" class="icon-lg"></i></div>
|
||||
<p>Erreur lors de la recherche</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@ -12,3 +12,7 @@ import './vim-mode-manager.js';
|
||||
import './favorites.js';
|
||||
import './sidebar-sections.js';
|
||||
import './keyboard-shortcuts.js';
|
||||
import { initPublicToggle } from './public-toggle.js';
|
||||
|
||||
// Initialiser le toggle public
|
||||
initPublicToggle();
|
||||
|
||||
145
frontend/src/public-toggle.js
Normal file
145
frontend/src/public-toggle.js
Normal 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);
|
||||
}
|
||||
@ -51,7 +51,7 @@ class SearchModal {
|
||||
<div class="search-modal-body">
|
||||
<div class="search-modal-results">
|
||||
<div class="search-modal-help">
|
||||
<div class="search-modal-help-title">💡 Recherche avancée</div>
|
||||
<div class="search-modal-help-title"><i data-lucide="lightbulb" class="icon-sm"></i> Recherche avancée</div>
|
||||
<div class="search-modal-help-items">
|
||||
<div class="search-modal-help-item">
|
||||
<code>tag:projet</code>
|
||||
@ -289,7 +289,7 @@ class SearchModal {
|
||||
this.results = [];
|
||||
this.resultsContainer.innerHTML = `
|
||||
<div class="search-modal-help">
|
||||
<div class="search-modal-help-title">💡 Recherche avancée</div>
|
||||
<div class="search-modal-help-title"><i data-lucide="lightbulb" class="icon-sm"></i> Recherche avancée</div>
|
||||
<div class="search-modal-help-items">
|
||||
<div class="search-modal-help-item">
|
||||
<code>tag:projet</code>
|
||||
@ -325,7 +325,7 @@ class SearchModal {
|
||||
this.results = [];
|
||||
this.resultsContainer.innerHTML = `
|
||||
<div class="search-modal-no-results">
|
||||
<div class="search-modal-no-results-icon">🔍</div>
|
||||
<div class="search-modal-no-results-icon"><i data-lucide="search" class="icon-lg"></i></div>
|
||||
<p class="search-modal-no-results-text">Aucun résultat pour « <strong>${this.escapeHtml(query)}</strong> »</p>
|
||||
<p class="search-modal-no-results-hint">Essayez d'autres mots-clés ou utilisez les filtres</p>
|
||||
</div>
|
||||
@ -335,7 +335,7 @@ class SearchModal {
|
||||
showError() {
|
||||
this.resultsContainer.innerHTML = `
|
||||
<div class="search-modal-error">
|
||||
<div class="search-modal-error-icon">⚠️</div>
|
||||
<div class="search-modal-error-icon"><i data-lucide="alert-triangle" class="icon-lg"></i></div>
|
||||
<p>Une erreur s'est produite lors de la recherche</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@ -9,49 +9,49 @@ class ThemeManager {
|
||||
{
|
||||
id: 'material-dark',
|
||||
name: 'Material Dark',
|
||||
icon: '🌙',
|
||||
icon: 'moon',
|
||||
description: 'Thème professionnel inspiré de Material Design'
|
||||
},
|
||||
{
|
||||
id: 'monokai-dark',
|
||||
name: 'Monokai Dark',
|
||||
icon: '🎨',
|
||||
icon: 'palette',
|
||||
description: 'Palette Monokai classique pour les développeurs'
|
||||
},
|
||||
{
|
||||
id: 'dracula',
|
||||
name: 'Dracula',
|
||||
icon: '🧛',
|
||||
icon: 'moon-star',
|
||||
description: 'Thème sombre élégant avec des accents violets et cyan'
|
||||
},
|
||||
{
|
||||
id: 'one-dark',
|
||||
name: 'One Dark',
|
||||
icon: '⚡',
|
||||
icon: 'zap',
|
||||
description: 'Thème populaire d\'Atom avec des couleurs douces'
|
||||
},
|
||||
{
|
||||
id: 'solarized-dark',
|
||||
name: 'Solarized Dark',
|
||||
icon: '☀️',
|
||||
icon: 'sun',
|
||||
description: 'Palette scientifiquement optimisée pour réduire la fatigue oculaire'
|
||||
},
|
||||
{
|
||||
id: 'nord',
|
||||
name: 'Nord',
|
||||
icon: '❄️',
|
||||
icon: 'snowflake',
|
||||
description: 'Palette arctique apaisante avec des tons bleus froids'
|
||||
},
|
||||
{
|
||||
id: 'catppuccin',
|
||||
name: 'Catppuccin',
|
||||
icon: '🌸',
|
||||
icon: 'flower-2',
|
||||
description: 'Thème pastel doux et chaleureux avec des accents roses et bleus'
|
||||
},
|
||||
{
|
||||
id: 'everforest',
|
||||
name: 'Everforest',
|
||||
icon: '🌲',
|
||||
icon: 'tree-pine',
|
||||
description: 'Palette naturelle inspirée de la forêt avec des tons verts et beiges'
|
||||
}
|
||||
];
|
||||
|
||||
1
go.mod
1
go.mod
@ -4,6 +4,7 @@ go 1.22
|
||||
|
||||
require (
|
||||
github.com/fsnotify/fsnotify v1.7.0
|
||||
github.com/yuin/goldmark v1.7.13
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
|
||||
2
go.sum
2
go.sum
@ -1,5 +1,7 @@
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
|
||||
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
|
||||
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
|
||||
@ -71,6 +71,16 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Public endpoints
|
||||
if path == "/api/public/list" {
|
||||
h.handlePublicList(w, r)
|
||||
return
|
||||
}
|
||||
if path == "/api/public/toggle" {
|
||||
h.handlePublicToggle(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Legacy/HTML endpoints
|
||||
if strings.HasPrefix(path, "/api/search") {
|
||||
h.handleSearch(w, r)
|
||||
@ -295,12 +305,14 @@ func (h *Handler) handleHome(w http.ResponseWriter, r *http.Request) {
|
||||
Filename string
|
||||
Content string
|
||||
IsHome bool
|
||||
IsPublic bool
|
||||
Backlinks []BacklinkInfo
|
||||
Breadcrumb template.HTML
|
||||
}{
|
||||
Filename: "🏠 Accueil - Index",
|
||||
Content: content,
|
||||
IsHome: true,
|
||||
IsPublic: false,
|
||||
Backlinks: nil, // Pas de backlinks pour la page d'accueil
|
||||
Breadcrumb: h.generateBreadcrumb(""),
|
||||
}
|
||||
@ -351,7 +363,10 @@ func (h *Handler) generateHomeMarkdown(r *http.Request) string {
|
||||
// Section des favoris (après les tags)
|
||||
h.generateFavoritesSection(&sb, r)
|
||||
|
||||
// Section des notes récemment modifiées (après les favoris)
|
||||
// Section des notes publiques (après les favoris)
|
||||
h.generatePublicNotesSection(&sb, r)
|
||||
|
||||
// Section des notes récemment modifiées (après les notes publiques)
|
||||
h.generateRecentNotesSection(&sb, r)
|
||||
|
||||
// Section de toutes les notes avec accordéon
|
||||
@ -380,7 +395,7 @@ func (h *Handler) generateTagsSection(sb *strings.Builder) {
|
||||
|
||||
sb.WriteString("<div class=\"home-section\">\n")
|
||||
sb.WriteString(" <div class=\"home-section-header\" onclick=\"toggleFolder('tags')\">\n")
|
||||
sb.WriteString(" <h2 class=\"home-section-title\">🏷️ Tags</h2>\n")
|
||||
sb.WriteString(" <h2 class=\"home-section-title\"><i data-lucide=\"tags\" class=\"icon-sm\"></i> Tags</h2>\n")
|
||||
sb.WriteString(" </div>\n")
|
||||
sb.WriteString(" <div class=\"home-section-content\" id=\"folder-tags\">\n")
|
||||
sb.WriteString(" <div class=\"tags-cloud\">\n")
|
||||
@ -408,7 +423,7 @@ func (h *Handler) generateFavoritesSection(sb *strings.Builder, r *http.Request)
|
||||
|
||||
sb.WriteString("<div class=\"home-section\">\n")
|
||||
sb.WriteString(" <div class=\"home-section-header\" onclick=\"toggleFolder('favorites')\">\n")
|
||||
sb.WriteString(" <h2 class=\"home-section-title\">⭐ " + h.t(r, "favorites.title") + "</h2>\n")
|
||||
sb.WriteString(" <h2 class=\"home-section-title\"><i data-lucide=\"star\" class=\"icon-sm\"></i> " + h.t(r, "favorites.title") + "</h2>\n")
|
||||
sb.WriteString(" </div>\n")
|
||||
sb.WriteString(" <div class=\"home-section-content\" id=\"folder-favorites\">\n")
|
||||
sb.WriteString(" <div class=\"note-tree favorites-tree\">\n")
|
||||
@ -420,7 +435,7 @@ func (h *Handler) generateFavoritesSection(sb *strings.Builder, r *http.Request)
|
||||
// Dossier - avec accordéon
|
||||
sb.WriteString(fmt.Sprintf(" <div class=\"folder indent-level-1\">\n"))
|
||||
sb.WriteString(fmt.Sprintf(" <div class=\"folder-header\" onclick=\"toggleFolder('%s')\">\n", safeID))
|
||||
sb.WriteString(fmt.Sprintf(" <span class=\"folder-icon\" id=\"icon-%s\">📁</span>\n", safeID))
|
||||
sb.WriteString(fmt.Sprintf(" <span class=\"folder-icon\" id=\"icon-%s\"><i data-lucide=\"folder\" class=\"icon-sm\"></i></span>\n", safeID))
|
||||
sb.WriteString(fmt.Sprintf(" <strong>%s</strong>\n", fav.Title))
|
||||
sb.WriteString(fmt.Sprintf(" </div>\n"))
|
||||
sb.WriteString(fmt.Sprintf(" <div class=\"folder-content\" id=\"folder-%s\">\n", safeID))
|
||||
@ -445,6 +460,44 @@ func (h *Handler) generateFavoritesSection(sb *strings.Builder, r *http.Request)
|
||||
sb.WriteString("</div>\n\n")
|
||||
}
|
||||
|
||||
// generatePublicNotesSection génère la section des notes publiques
|
||||
func (h *Handler) generatePublicNotesSection(sb *strings.Builder, r *http.Request) {
|
||||
publicNotes, err := h.loadPublicNotes()
|
||||
if err != nil || len(publicNotes.Notes) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
sb.WriteString("<div class=\"home-section\">\n")
|
||||
sb.WriteString(" <div class=\"home-section-header\" onclick=\"toggleFolder('public-notes')\">\n")
|
||||
sb.WriteString(fmt.Sprintf(" <h2 class=\"home-section-title\"><i data-lucide=\"globe\" class=\"icon-sm\"></i> %s (%d)</h2>\n", h.t(r, "publicNotes.title"), len(publicNotes.Notes)))
|
||||
sb.WriteString(" </div>\n")
|
||||
sb.WriteString(" <div class=\"home-section-content\" id=\"folder-public-notes\">\n")
|
||||
sb.WriteString(" <div class=\"public-notes-list\">\n")
|
||||
|
||||
for _, note := range publicNotes.Notes {
|
||||
filename := filepath.Base(note.Path)
|
||||
htmlFile := filename[:len(filename)-3] + ".html"
|
||||
publicURL := fmt.Sprintf("/public/%s", htmlFile)
|
||||
|
||||
sb.WriteString(" <div class=\"public-note-card\">\n")
|
||||
sb.WriteString(fmt.Sprintf(" <div class=\"public-note-header\">\n"))
|
||||
sb.WriteString(fmt.Sprintf(" <a href=\"#\" class=\"public-note-edit\" hx-get=\"/api/notes/%s\" hx-target=\"#editor-container\" hx-swap=\"innerHTML\" hx-push-url=\"true\" title=\"%s\">", note.Path, h.t(r, "publicNotes.editNote")))
|
||||
sb.WriteString(fmt.Sprintf(" <span class=\"public-note-title\">%s</span>", note.Title))
|
||||
sb.WriteString(" </a>\n")
|
||||
sb.WriteString(fmt.Sprintf(" <a href=\"%s\" target=\"_blank\" class=\"public-note-view\" title=\"%s\">🌐</a>\n", publicURL, h.t(r, "publicNotes.viewPublic")))
|
||||
sb.WriteString(" </div>\n")
|
||||
sb.WriteString(fmt.Sprintf(" <div class=\"public-note-meta\">\n"))
|
||||
sb.WriteString(fmt.Sprintf(" <span class=\"public-note-source\">📄 %s</span>\n", note.Path))
|
||||
sb.WriteString(fmt.Sprintf(" <span class=\"public-note-date\"><i data-lucide=\"calendar\" class=\"icon-sm\"></i> %s</span>\n", note.PublishedAt.Format("02/01/2006")))
|
||||
sb.WriteString(" </div>\n")
|
||||
sb.WriteString(" </div>\n")
|
||||
}
|
||||
|
||||
sb.WriteString(" </div>\n")
|
||||
sb.WriteString(" </div>\n")
|
||||
sb.WriteString("</div>\n\n")
|
||||
}
|
||||
|
||||
// generateRecentNotesSection génère la section des notes récemment modifiées
|
||||
func (h *Handler) generateRecentNotesSection(sb *strings.Builder, r *http.Request) {
|
||||
recentDocs := h.idx.GetRecentDocuments(5)
|
||||
@ -477,7 +530,7 @@ func (h *Handler) generateRecentNotesSection(sb *strings.Builder, r *http.Reques
|
||||
sb.WriteString(fmt.Sprintf(" <a href=\"#\" class=\"recent-note-link\" hx-get=\"/api/notes/%s\" hx-target=\"#editor-container\" hx-swap=\"innerHTML\" hx-push-url=\"true\">\n", doc.Path))
|
||||
sb.WriteString(fmt.Sprintf(" <div class=\"recent-note-title\">%s</div>\n", doc.Title))
|
||||
sb.WriteString(fmt.Sprintf(" <div class=\"recent-note-meta\">\n"))
|
||||
sb.WriteString(fmt.Sprintf(" <span class=\"recent-note-date\">📅 %s</span>\n", dateStr))
|
||||
sb.WriteString(fmt.Sprintf(" <span class=\"recent-note-date\"><i data-lucide=\"calendar\" class=\"icon-sm\"></i> %s</span>\n", dateStr))
|
||||
if len(doc.Tags) > 0 {
|
||||
sb.WriteString(fmt.Sprintf(" <span class=\"recent-note-tags\">"))
|
||||
for i, tag := range doc.Tags {
|
||||
@ -523,7 +576,7 @@ func (h *Handler) generateFavoriteFolderContent(sb *strings.Builder, folderPath
|
||||
// Sous-dossier
|
||||
sb.WriteString(fmt.Sprintf("%s<div class=\"folder %s\">\n", indent, indentClass))
|
||||
sb.WriteString(fmt.Sprintf("%s <div class=\"folder-header\" onclick=\"toggleFolder('%s')\">\n", indent, safeID))
|
||||
sb.WriteString(fmt.Sprintf("%s <span class=\"folder-icon\" id=\"icon-%s\">📁</span>\n", indent, safeID))
|
||||
sb.WriteString(fmt.Sprintf("%s <span class=\"folder-icon\" id=\"icon-%s\"><i data-lucide=\"folder\" class=\"icon-sm\"></i></span>\n", indent, safeID))
|
||||
sb.WriteString(fmt.Sprintf("%s <strong>%s</strong>\n", indent, name))
|
||||
sb.WriteString(fmt.Sprintf("%s </div>\n", indent))
|
||||
sb.WriteString(fmt.Sprintf("%s <div class=\"folder-content\" id=\"folder-%s\">\n", indent, safeID))
|
||||
@ -579,7 +632,7 @@ func (h *Handler) generateMarkdownTree(sb *strings.Builder, node *TreeNode, dept
|
||||
indentClass := fmt.Sprintf("indent-level-%d", depth)
|
||||
sb.WriteString(fmt.Sprintf("%s<div class=\"folder %s\">\n", indent, indentClass))
|
||||
sb.WriteString(fmt.Sprintf("%s <div class=\"folder-header\" onclick=\"toggleFolder('%s')\">\n", indent, safeID))
|
||||
sb.WriteString(fmt.Sprintf("%s <span class=\"folder-icon\" id=\"icon-%s\">📁</span>\n", indent, safeID))
|
||||
sb.WriteString(fmt.Sprintf("%s <span class=\"folder-icon\" id=\"icon-%s\"><i data-lucide=\"folder\" class=\"icon-sm\"></i></span>\n", indent, safeID))
|
||||
sb.WriteString(fmt.Sprintf("%s <strong>%s</strong>\n", indent, node.Name))
|
||||
sb.WriteString(fmt.Sprintf("%s </div>\n", indent))
|
||||
sb.WriteString(fmt.Sprintf("%s <div class=\"folder-content\" id=\"folder-%s\">\n", indent, safeID))
|
||||
@ -688,11 +741,13 @@ func (h *Handler) createAndRenderNote(w http.ResponseWriter, r *http.Request, fi
|
||||
Filename string
|
||||
Content string
|
||||
IsHome bool
|
||||
IsPublic bool
|
||||
Backlinks []BacklinkInfo
|
||||
}{
|
||||
Filename: filename,
|
||||
Content: initialContent,
|
||||
IsHome: false,
|
||||
IsPublic: false, // Nouvelle note, pas publique par défaut
|
||||
Backlinks: nil, // Pas de backlinks pour une nouvelle note
|
||||
}
|
||||
|
||||
@ -832,12 +887,14 @@ func (h *Handler) handleGetNote(w http.ResponseWriter, r *http.Request, filename
|
||||
Filename string
|
||||
Content string
|
||||
IsHome bool
|
||||
IsPublic bool
|
||||
Backlinks []BacklinkInfo
|
||||
Breadcrumb template.HTML
|
||||
}{
|
||||
Filename: filename,
|
||||
Content: string(content),
|
||||
IsHome: false,
|
||||
IsPublic: h.isPublic(filename),
|
||||
Backlinks: backlinkData,
|
||||
Breadcrumb: h.generateBreadcrumb(filename),
|
||||
}
|
||||
@ -1350,12 +1407,14 @@ func (h *Handler) handleFolderView(w http.ResponseWriter, r *http.Request) {
|
||||
Filename string
|
||||
Content string
|
||||
IsHome bool
|
||||
IsPublic bool
|
||||
Backlinks []BacklinkInfo
|
||||
Breadcrumb template.HTML
|
||||
}{
|
||||
Filename: cleanPath,
|
||||
Content: content,
|
||||
IsHome: true, // Pas d'édition pour une vue de dossier
|
||||
IsPublic: false,
|
||||
Backlinks: nil,
|
||||
Breadcrumb: h.generateBreadcrumb(cleanPath),
|
||||
}
|
||||
@ -1370,7 +1429,7 @@ func (h *Handler) handleFolderView(w http.ResponseWriter, r *http.Request) {
|
||||
// generateBreadcrumb génère un fil d'Ariane HTML cliquable
|
||||
func (h *Handler) generateBreadcrumb(path string) template.HTML {
|
||||
if path == "" {
|
||||
return template.HTML(`<strong>📁 Racine</strong>`)
|
||||
return template.HTML(`<strong><i data-lucide="folder" class="icon-sm"></i> Racine</strong>`)
|
||||
}
|
||||
|
||||
parts := strings.Split(filepath.ToSlash(path), "/")
|
||||
@ -1379,7 +1438,7 @@ func (h *Handler) generateBreadcrumb(path string) template.HTML {
|
||||
sb.WriteString(`<span class="breadcrumb">`)
|
||||
|
||||
// Lien racine
|
||||
sb.WriteString(`<a href="#" hx-get="/api/home" hx-target="#editor-container" hx-swap="innerHTML" hx-push-url="true" class="breadcrumb-link">📁 Racine</a>`)
|
||||
sb.WriteString(`<a href="#" hx-get="/api/home" hx-target="#editor-container" hx-swap="innerHTML" hx-push-url="true" class="breadcrumb-link"><i data-lucide="folder" class="icon-sm"></i> Racine</a>`)
|
||||
|
||||
// Construire les liens pour chaque partie
|
||||
currentPath := ""
|
||||
|
||||
607
internal/api/public.go
Normal file
607
internal/api/public.go
Normal 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)
|
||||
}
|
||||
@ -260,5 +260,25 @@
|
||||
"nextMonth": "Next month",
|
||||
"noNote": "No note",
|
||||
"noteOf": "Note of"
|
||||
},
|
||||
"public": {
|
||||
"buttonPrivate": "Private",
|
||||
"buttonPublic": "Public",
|
||||
"titlePrivate": "This note is private - Click to make it public",
|
||||
"titlePublic": "This note is public - Click to make it private",
|
||||
"listTitle": "Public Notes",
|
||||
"listSubtitle": "Discover my shared notes",
|
||||
"noPublicNotes": "No public notes yet",
|
||||
"publishedOn": "Published on",
|
||||
"backToList": "Back to public notes",
|
||||
"loading": "Loading...",
|
||||
"notificationPublished": "✅ Note published! It is now visible at /public",
|
||||
"notificationUnpublished": "✅ Note removed from public space",
|
||||
"notificationError": "❌ Error changing status"
|
||||
},
|
||||
"publicNotes": {
|
||||
"title": "Public Notes",
|
||||
"editNote": "Edit note",
|
||||
"viewPublic": "View public page"
|
||||
}
|
||||
}
|
||||
|
||||
@ -260,5 +260,25 @@
|
||||
"nextMonth": "Mois suivant",
|
||||
"noNote": "Pas de note",
|
||||
"noteOf": "Note du"
|
||||
},
|
||||
"public": {
|
||||
"buttonPrivate": "Privé",
|
||||
"buttonPublic": "Public",
|
||||
"titlePrivate": "Cette note est privée - Cliquer pour la rendre publique",
|
||||
"titlePublic": "Cette note est publique - Cliquer pour la rendre privée",
|
||||
"listTitle": "Notes Publiques",
|
||||
"listSubtitle": "Découvrez mes notes partagées",
|
||||
"noPublicNotes": "Aucune note publique pour le moment",
|
||||
"publishedOn": "Publié le",
|
||||
"backToList": "Retour aux notes publiques",
|
||||
"loading": "Chargement...",
|
||||
"notificationPublished": "✅ Note publiée ! Elle est maintenant visible sur /public",
|
||||
"notificationUnpublished": "✅ Note retirée de l'espace public",
|
||||
"notificationError": "❌ Erreur lors de la modification du statut"
|
||||
},
|
||||
"publicNotes": {
|
||||
"title": "Notes Publiques",
|
||||
"editNote": "Éditer la note",
|
||||
"viewPublic": "Voir la page publique"
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,40 +7,68 @@
|
||||
"added_at": "2025-11-11T13:55:49.371541279+01:00",
|
||||
"order": 0
|
||||
},
|
||||
{
|
||||
"path": "research/design/ui-inspiration.md",
|
||||
"is_dir": false,
|
||||
"title": "ui-inspiration",
|
||||
"added_at": "2025-11-11T14:20:49.985321698+01:00",
|
||||
"order": 1
|
||||
},
|
||||
{
|
||||
"path": "ideas/client-feedback.md",
|
||||
"is_dir": false,
|
||||
"title": "client-feedback",
|
||||
"added_at": "2025-11-11T14:22:16.497953232+01:00",
|
||||
"order": 1
|
||||
},
|
||||
{
|
||||
"path": "personal/learning-goals.md",
|
||||
"is_dir": false,
|
||||
"title": "learning-goals",
|
||||
"added_at": "2025-12-24T13:17:21.123080299+01:00",
|
||||
"order": 2
|
||||
},
|
||||
{
|
||||
"path": "ideas/collaboration.md",
|
||||
"is_dir": false,
|
||||
"title": "collaboration",
|
||||
"added_at": "2025-11-11T14:22:18.012032002+01:00",
|
||||
"path": "archive",
|
||||
"is_dir": true,
|
||||
"title": "archive",
|
||||
"added_at": "2025-12-24T15:48:42.323990909+01:00",
|
||||
"order": 3
|
||||
},
|
||||
{
|
||||
"path": "ideas/mobile-app.md",
|
||||
"path": "archive/ai-assistant.md",
|
||||
"is_dir": false,
|
||||
"title": "mobile-app",
|
||||
"added_at": "2025-11-11T14:22:19.048311608+01:00",
|
||||
"title": "ai-assistant",
|
||||
"added_at": "2025-12-24T15:49:08.265811752+01:00",
|
||||
"order": 4
|
||||
},
|
||||
{
|
||||
"path": "documentation/guides",
|
||||
"is_dir": true,
|
||||
"title": "guides",
|
||||
"added_at": "2025-11-12T18:18:20.53353467+01:00",
|
||||
"path": "meetings/2025/sprint-planning.md",
|
||||
"is_dir": false,
|
||||
"title": "sprint-planning",
|
||||
"added_at": "2025-12-24T15:55:04.58786532+01:00",
|
||||
"order": 5
|
||||
},
|
||||
{
|
||||
"path": "meetings",
|
||||
"is_dir": true,
|
||||
"title": "meetings",
|
||||
"added_at": "2025-12-24T15:56:40.332077313+01:00",
|
||||
"order": 6
|
||||
},
|
||||
{
|
||||
"path": "Myfolder.txt",
|
||||
"is_dir": true,
|
||||
"title": "Myfolder.txt",
|
||||
"added_at": "2025-12-24T15:57:09.512148418+01:00",
|
||||
"order": 7
|
||||
},
|
||||
{
|
||||
"path": "projets",
|
||||
"is_dir": true,
|
||||
"title": "projets",
|
||||
"added_at": "2025-12-24T15:59:24.938636283+01:00",
|
||||
"order": 8
|
||||
},
|
||||
{
|
||||
"path": "documentation/bienvenue.md",
|
||||
"is_dir": false,
|
||||
"title": "bienvenue",
|
||||
"added_at": "2025-12-24T16:30:46.322365652+01:00",
|
||||
"order": 9
|
||||
}
|
||||
]
|
||||
}
|
||||
39
notes/.public.json
Normal file
39
notes/.public.json
Normal 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
25
notes/daily/2025/12/24.md
Normal 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
|
||||
-
|
||||
@ -1,7 +1,7 @@
|
||||
---
|
||||
title: Bienvenue dans PersoNotes
|
||||
date: 08-11-2025
|
||||
last_modified: 09-11-2025:01:13
|
||||
last_modified: 24-12-2025:16:28
|
||||
tags:
|
||||
- aide
|
||||
- documentation
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
---
|
||||
title: Client Feedback Session
|
||||
date: 10-11-2025
|
||||
last_modified: 11-11-2025:11:12
|
||||
last_modified: 24-12-2025:16:45
|
||||
tags:
|
||||
- meeting
|
||||
- client
|
||||
@ -28,3 +28,7 @@ Focus sur l'export PDF pour la v1.1
|
||||
|
||||
|
||||
# DERNIER EDIT
|
||||
|
||||
[Progressive Web App](projets/mobile/pwa.md)
|
||||
|
||||
`This is a `
|
||||
@ -1,7 +1,7 @@
|
||||
---
|
||||
title: CodeMirror Integration
|
||||
date: 10-11-2025
|
||||
last_modified: 12-11-2025:09:37
|
||||
last_modified: 24-12-2025:16:46
|
||||
tags:
|
||||
- projet
|
||||
- frontend
|
||||
@ -1,8 +1,11 @@
|
||||
---
|
||||
title: "Vite Build Process"
|
||||
date: "10-11-2025"
|
||||
last_modified: "10-11-2025:19:21"
|
||||
tags: ["projet", "frontend", "build"]
|
||||
title: Vite Build Process
|
||||
date: 10-11-2025
|
||||
last_modified: 24-12-2025:16:41
|
||||
tags:
|
||||
- projet
|
||||
- frontend
|
||||
- build
|
||||
---
|
||||
|
||||
# Vite Build Process
|
||||
|
||||
108
public/authentication.html
Normal file
108
public/authentication.html
Normal 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
|
||||
{
|
||||
"username": "user",
|
||||
"password": "pass"
|
||||
}
|
||||
|
||||
Response:
|
||||
{
|
||||
"token": "eyJhbGc..."
|
||||
}
|
||||
</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
95
public/backlog.html
Normal 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
312
public/bienvenue.html
Normal 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’est mon application de prise de note</p>
|
||||
<h2 id="jespre-quelle-va-bien-marcher">J’espére qu’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’application et le format front matter.</p>
|
||||
<h2 id="quest-ce-que-le-front-matter-">Qu’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’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)
|
||||

|
||||
</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("Hello World!");
|
||||
}
|
||||
```
|
||||
</code></pre>
|
||||
<p>Rendu :</p>
|
||||
<pre><code class="language-javascript">function hello() {
|
||||
console.log("Hello World!");
|
||||
}
|
||||
</code></pre>
|
||||
<h3 id="citations">Citations</h3>
|
||||
<pre><code class="language-markdown">> Ceci est une citation
|
||||
> 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’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’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’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’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
|
||||
|
||||
> Utilisation de JWT pour l'authentification
|
||||
|
||||
Code d'exemple :
|
||||
|
||||
```go
|
||||
func generateToken(userID string) (string, error) {
|
||||
// Implementation
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Prochaines étapes
|
||||
|
||||
- [ ] Implémenter les endpoints
|
||||
- [ ] Écrire les tests
|
||||
- [ ] Documentation API
|
||||
|
||||
## Actions
|
||||
|
||||
| Qui | Action | Deadline |
|
||||
|---------|---------------------|------------|
|
||||
| Bob | Endpoints API | 15-11-2025 |
|
||||
| Charlie | Interface Frontend | 20-11-2025 |
|
||||
| Alice | Review & Deploy | 25-11-2025 |
|
||||
</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
89
public/collaboration.html
Normal 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
110
public/index.html
Normal 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
93
public/mobile-app.html
Normal 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’édition de notes.</p>
|
||||
<h2 id="tech-stack">Tech Stack</h2>
|
||||
<ul>
|
||||
<li>React Native ou Flutter</li>
|
||||
<li>Sync avec l’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>
|
||||
94
public/notes/personal/learning-goals.html
Normal file
94
public/notes/personal/learning-goals.html
Normal 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
3663
public/static/theme.css
Normal file
File diff suppressed because it is too large
Load Diff
679
public/static/themes.css
Normal file
679
public/static/themes.css
Normal 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);
|
||||
}
|
||||
221
static/theme.css
221
static/theme.css
@ -17,35 +17,33 @@
|
||||
--text-secondary: #b0b0b0;
|
||||
--text-muted: #6e6e6e;
|
||||
|
||||
/* Accent colors - Blue focused */
|
||||
/* Accent color - Single blue accent for consistency */
|
||||
--accent-primary: #42a5f5;
|
||||
--accent-primary-hover: #64b5f6;
|
||||
--accent-secondary: #29b6f6;
|
||||
--accent-secondary-hover: #4fc3f7;
|
||||
--accent-hover: #64b5f6;
|
||||
|
||||
/* Semantic colors */
|
||||
--success: #66bb6a;
|
||||
--warning: #ffa726;
|
||||
--error: #ef5350;
|
||||
|
||||
/* Spacing */
|
||||
--spacing-xs: 0.25rem;
|
||||
--spacing-sm: 0.5rem;
|
||||
--spacing-md: 1rem;
|
||||
--spacing-lg: 1.5rem;
|
||||
--spacing-xl: 2rem;
|
||||
/* Spacing - 8px base unit system */
|
||||
--spacing-xs: 0.5rem; /* 8px - 1 unit */
|
||||
--spacing-sm: 1rem; /* 16px - 2 units */
|
||||
--spacing-md: 1.5rem; /* 24px - 3 units */
|
||||
--spacing-lg: 2rem; /* 32px - 4 units */
|
||||
--spacing-xl: 3rem; /* 48px - 6 units */
|
||||
|
||||
/* Sidebar compact spacing */
|
||||
--sidebar-item-gap: 0.05rem;
|
||||
--sidebar-padding-v: 0.3rem;
|
||||
--sidebar-padding-h: 0.75rem;
|
||||
--sidebar-indent: 1rem;
|
||||
/* Sidebar spacing - aligned to 8px grid */
|
||||
--sidebar-item-gap: 0.125rem; /* 2px - minimal spacing between items */
|
||||
--sidebar-padding-v: 0.25rem; /* 4px - compact vertical padding */
|
||||
--sidebar-padding-h: 1rem; /* 16px */
|
||||
--sidebar-indent: 1.5rem; /* 24px */
|
||||
|
||||
/* Shadows */
|
||||
--shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.4);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -2px rgba(0, 0, 0, 0.3);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.4), 0 4px 6px -4px rgba(0, 0, 0, 0.3);
|
||||
--shadow-glow: 0 0 20px rgba(66, 165, 245, 0.2);
|
||||
/* Shadows - reduced opacity for minimal look */
|
||||
--shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.08);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.12), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
|
||||
--shadow-glow: 0 0 20px rgba(66, 165, 245, 0.1);
|
||||
|
||||
/* Border radius */
|
||||
--radius-sm: 4px;
|
||||
@ -55,8 +53,59 @@
|
||||
/* Transitions */
|
||||
--transition-fast: 150ms ease;
|
||||
--transition-normal: 250ms ease;
|
||||
|
||||
/* Typography scale - consistent font sizes */
|
||||
--text-xs: 0.75rem; /* 12px */
|
||||
--text-sm: 0.875rem; /* 14px */
|
||||
--text-base: 1rem; /* 16px - default */
|
||||
--text-md: 1.125rem; /* 18px */
|
||||
--text-lg: 1.25rem; /* 20px */
|
||||
--text-xl: 1.5rem; /* 24px */
|
||||
--text-2xl: 2rem; /* 32px */
|
||||
--text-3xl: 3rem; /* 48px */
|
||||
|
||||
/* Touch targets - minimum sizes for accessibility */
|
||||
--touch-sm: 2rem; /* 32px - compact */
|
||||
--touch-md: 2.75rem; /* 44px - standard mobile minimum */
|
||||
--touch-lg: 3rem; /* 48px - comfortable */
|
||||
|
||||
/* Lucide Icons - Professional SVG icons */
|
||||
--icon-xs: 14px;
|
||||
--icon-sm: 16px;
|
||||
--icon-md: 20px;
|
||||
--icon-lg: 24px;
|
||||
--icon-xl: 32px;
|
||||
}
|
||||
|
||||
/* Lucide Icons Styling */
|
||||
.lucide {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
stroke-width: 2;
|
||||
stroke: currentColor;
|
||||
fill: none;
|
||||
}
|
||||
|
||||
/* Icon size variants */
|
||||
.icon-xs { width: var(--icon-xs); height: var(--icon-xs); }
|
||||
.icon-sm { width: var(--icon-sm); height: var(--icon-sm); }
|
||||
.icon-md { width: var(--icon-md); height: var(--icon-md); }
|
||||
.icon-lg { width: var(--icon-lg); height: var(--icon-lg); }
|
||||
.icon-xl { width: var(--icon-xl); height: var(--icon-xl); }
|
||||
|
||||
/* Icon with text alignment */
|
||||
.icon-text {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Icon color variants */
|
||||
.icon-primary { color: var(--text-primary); }
|
||||
.icon-secondary { color: var(--text-secondary); }
|
||||
.icon-muted { color: var(--text-muted); }
|
||||
.icon-accent { color: var(--accent-primary); }
|
||||
|
||||
/* Base styles */
|
||||
html {
|
||||
font-size: 16px;
|
||||
@ -90,7 +139,7 @@ header h1 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
|
||||
background: var(--accent-primary);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
@ -257,25 +306,7 @@ aside hr {
|
||||
margin: var(--spacing-sm) 0;
|
||||
}
|
||||
|
||||
/* File tree and search results */
|
||||
#file-tree a,
|
||||
#search-results a {
|
||||
display: block;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
border-radius: var(--radius-sm);
|
||||
transition: all var(--transition-fast);
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
#file-tree a:hover,
|
||||
#search-results a:hover {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--accent-primary);
|
||||
transform: translateX(2px);
|
||||
}
|
||||
/* File tree and search results - styles now handled by .file-item class */
|
||||
|
||||
/* Search results header */
|
||||
.search-results-header {
|
||||
@ -411,7 +442,7 @@ aside hr {
|
||||
}
|
||||
|
||||
.search-no-results-text strong {
|
||||
color: var(--accent-secondary);
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.search-no-results-hint {
|
||||
@ -455,7 +486,7 @@ aside hr {
|
||||
|
||||
.search-help-example code {
|
||||
background: var(--bg-primary);
|
||||
color: var(--accent-secondary);
|
||||
color: var(--accent-primary);
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.8rem;
|
||||
@ -720,7 +751,7 @@ main::-webkit-scrollbar-thumb:hover {
|
||||
|
||||
.preview h2 {
|
||||
font-size: 1.5em;
|
||||
color: var(--accent-secondary);
|
||||
color: var(--accent-primary);
|
||||
margin-bottom: 0.9em;
|
||||
}
|
||||
|
||||
@ -771,7 +802,7 @@ main::-webkit-scrollbar-thumb:hover {
|
||||
}
|
||||
|
||||
.preview ol > li::marker {
|
||||
color: var(--accent-secondary);
|
||||
color: var(--accent-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@ -797,7 +828,7 @@ main::-webkit-scrollbar-thumb:hover {
|
||||
|
||||
.preview a:hover {
|
||||
text-decoration: underline;
|
||||
color: var(--accent-primary-hover);
|
||||
color: var(--accent-hover);
|
||||
}
|
||||
|
||||
.preview strong, .preview b {
|
||||
@ -815,7 +846,7 @@ main::-webkit-scrollbar-thumb:hover {
|
||||
padding: 0.2em 0.4em;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.85em;
|
||||
color: var(--accent-secondary);
|
||||
color: var(--accent-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@ -853,7 +884,7 @@ main::-webkit-scrollbar-thumb:hover {
|
||||
}
|
||||
|
||||
.preview table thead {
|
||||
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
|
||||
background: var(--accent-primary);
|
||||
}
|
||||
|
||||
.preview table thead th {
|
||||
@ -895,7 +926,7 @@ main::-webkit-scrollbar-thumb:hover {
|
||||
button,
|
||||
[type="submit"],
|
||||
[type="button"] {
|
||||
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
|
||||
background: var(--accent-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
@ -984,7 +1015,7 @@ button.secondary:hover {
|
||||
}
|
||||
|
||||
#slash-commands-palette li[style*="background-color"] {
|
||||
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)) !important;
|
||||
background: var(--accent-primary) !important;
|
||||
color: white !important;
|
||||
font-weight: 500;
|
||||
transform: translateX(2px);
|
||||
@ -1016,7 +1047,7 @@ progress::-webkit-progress-bar {
|
||||
}
|
||||
|
||||
progress::-webkit-progress-value {
|
||||
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
|
||||
background: var(--accent-primary);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
@ -1236,39 +1267,26 @@ body, html {
|
||||
}
|
||||
|
||||
.folder-item.drag-over .folder-header {
|
||||
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
|
||||
background: var(--accent-primary);
|
||||
color: white;
|
||||
box-shadow: var(--shadow-glow);
|
||||
border: 2px solid var(--accent-primary);
|
||||
border-radius: var(--radius-md);
|
||||
animation: pulse 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
box-shadow: var(--shadow-glow);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 0 30px rgba(88, 166, 255, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
.file-item.drag-over {
|
||||
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
|
||||
background: var(--accent-primary);
|
||||
color: white;
|
||||
box-shadow: var(--shadow-glow);
|
||||
}
|
||||
|
||||
/* Style pour la racine en drag-over */
|
||||
.sidebar-section-header.drag-over {
|
||||
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)) !important;
|
||||
background: var(--accent-primary) !important;
|
||||
color: white !important;
|
||||
box-shadow: var(--shadow-glow);
|
||||
border: 2px solid var(--accent-primary);
|
||||
border-radius: var(--radius-md);
|
||||
animation: pulse 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.sidebar-section-header.drag-over .folder-name,
|
||||
@ -1401,11 +1419,10 @@ body, html {
|
||||
|
||||
/* Quand on drag au-dessus de la racine */
|
||||
.root-drop-zone.drag-over .root-folder-header {
|
||||
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
|
||||
background: var(--accent-primary);
|
||||
color: white;
|
||||
border-color: var(--accent-primary);
|
||||
box-shadow: var(--shadow-glow);
|
||||
animation: pulse 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.root-drop-zone.drag-over .root-folder-header .folder-name,
|
||||
@ -2053,7 +2070,7 @@ body, html {
|
||||
}
|
||||
|
||||
.search-modal-result-item.selected {
|
||||
background: linear-gradient(135deg, rgba(130, 170, 255, 0.15), rgba(199, 146, 234, 0.15));
|
||||
background: var(--bg-secondary);
|
||||
border-color: var(--accent-primary);
|
||||
}
|
||||
|
||||
@ -2074,7 +2091,7 @@ body, html {
|
||||
}
|
||||
|
||||
.search-modal-result-title mark {
|
||||
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
|
||||
background: var(--accent-primary);
|
||||
color: white;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
@ -2200,11 +2217,6 @@ body, html {
|
||||
border: 3px solid var(--border-primary);
|
||||
border-top-color: var(--accent-primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.search-modal-loading p {
|
||||
@ -2566,12 +2578,12 @@ body, html {
|
||||
|
||||
/* Today */
|
||||
.calendar-day-today {
|
||||
border-color: var(--accent-secondary);
|
||||
background: linear-gradient(135deg, rgba(130, 170, 255, 0.1), rgba(199, 146, 234, 0.1));
|
||||
border-color: var(--accent-primary);
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.calendar-day-today .calendar-day-number {
|
||||
color: var(--accent-secondary);
|
||||
color: var(--accent-primary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@ -2596,7 +2608,7 @@ body, html {
|
||||
width: 100%;
|
||||
margin-top: var(--spacing-sm);
|
||||
padding: var(--spacing-sm);
|
||||
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
|
||||
background: var(--accent-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
@ -2681,11 +2693,12 @@ body, html {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
margin: var(--sidebar-item-gap) 0;
|
||||
margin: 0; /* No margin on wrapper - use same spacing as folders */
|
||||
}
|
||||
|
||||
.file-item-wrapper .file-item {
|
||||
flex: 1;
|
||||
margin: 0 !important; /* Force remove margin to avoid double spacing */
|
||||
}
|
||||
|
||||
/* Bouton de mode sélection */
|
||||
@ -2715,8 +2728,8 @@ body, html {
|
||||
}
|
||||
|
||||
.icon-button.active:hover {
|
||||
background: var(--accent-primary-hover);
|
||||
border-color: var(--accent-primary-hover);
|
||||
background: var(--accent-hover);
|
||||
border-color: var(--accent-hover);
|
||||
}
|
||||
|
||||
/* Toolbar de sélection flottante */
|
||||
@ -2784,7 +2797,7 @@ body, html {
|
||||
background: #ff5370;
|
||||
border-color: #ff5370;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(240, 113, 120, 0.4);
|
||||
box-shadow: 0 4px 12px rgba(240, 113, 120, 0.15);
|
||||
}
|
||||
|
||||
.danger-button:active {
|
||||
@ -2974,30 +2987,38 @@ body, html {
|
||||
|
||||
/* Bouton d'ajout aux favoris (dans le file tree) */
|
||||
.add-to-favorites {
|
||||
opacity: 0;
|
||||
opacity: 0.4; /* Always slightly visible */
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
padding: 0 0.2rem;
|
||||
font-size: 1rem;
|
||||
padding: 0.25rem;
|
||||
transition: all var(--transition-fast);
|
||||
margin-left: auto;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.folder-header:hover .add-to-favorites,
|
||||
.file-item:hover .add-to-favorites {
|
||||
opacity: 1;
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.add-to-favorites:hover {
|
||||
color: var(--warning);
|
||||
transform: scale(1.2);
|
||||
transform: scale(1.15);
|
||||
background: rgba(255, 193, 7, 0.1); /* Subtle yellow background */
|
||||
}
|
||||
|
||||
.add-to-favorites.is-favorite {
|
||||
opacity: 1;
|
||||
opacity: 1 !important; /* Always fully visible when favorited */
|
||||
color: var(--warning);
|
||||
background: rgba(255, 193, 7, 0.15);
|
||||
}
|
||||
|
||||
.add-to-favorites.is-favorite:hover {
|
||||
background: rgba(255, 193, 7, 0.2);
|
||||
}
|
||||
|
||||
/* Responsive - Hauteurs adaptatives pour #favorites-list */
|
||||
@ -3162,7 +3183,7 @@ body, html {
|
||||
}
|
||||
|
||||
.link-inserter-result-item.selected {
|
||||
background: linear-gradient(135deg, rgba(130, 170, 255, 0.15), rgba(199, 146, 234, 0.15));
|
||||
background: var(--bg-secondary);
|
||||
border-color: var(--accent-primary);
|
||||
}
|
||||
|
||||
@ -3184,7 +3205,7 @@ body, html {
|
||||
}
|
||||
|
||||
.link-inserter-result-title mark {
|
||||
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
|
||||
background: var(--accent-primary);
|
||||
color: white;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
@ -3241,7 +3262,6 @@ body, html {
|
||||
border: 3px solid var(--bg-tertiary);
|
||||
border-top-color: var(--accent-primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
.link-inserter-loading p {
|
||||
@ -3575,7 +3595,7 @@ body, html {
|
||||
|
||||
.breadcrumb-link:hover {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--accent-secondary);
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.breadcrumb-separator {
|
||||
@ -3624,3 +3644,20 @@ body, html {
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Public toggle button */
|
||||
#toggle-public-btn {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
#toggle-public-btn.public-active {
|
||||
background: var(--accent-green, #2ecc71);
|
||||
color: white;
|
||||
border-color: var(--accent-green, #2ecc71);
|
||||
}
|
||||
|
||||
#toggle-public-btn.public-active:hover {
|
||||
background: var(--accent-green-dark, #27ae60);
|
||||
border-color: var(--accent-green-dark, #27ae60);
|
||||
}
|
||||
|
||||
@ -21,9 +21,7 @@
|
||||
--text-muted: #6e6e6e;
|
||||
|
||||
--accent-primary: #42a5f5;
|
||||
--accent-primary-hover: #5ab3f7;
|
||||
--accent-secondary: #29b6f6;
|
||||
--accent-secondary-hover: #4fc3f7;
|
||||
--accent-hover: #5ab3f7;
|
||||
|
||||
--success: #66bb6a;
|
||||
--warning: #ffa726;
|
||||
@ -47,9 +45,7 @@
|
||||
--text-muted: #75715e;
|
||||
|
||||
--accent-primary: #66d9ef;
|
||||
--accent-primary-hover: #7ee5f7;
|
||||
--accent-secondary: #88c070;
|
||||
--accent-secondary-hover: #9acc84;
|
||||
--accent-hover: #7ee5f7;
|
||||
|
||||
--success: #88c070;
|
||||
--warning: #e6db74;
|
||||
@ -73,9 +69,7 @@
|
||||
--text-muted: #6272a4;
|
||||
|
||||
--accent-primary: #8be9fd;
|
||||
--accent-primary-hover: #9ff3ff;
|
||||
--accent-secondary: #bd93f9;
|
||||
--accent-secondary-hover: #cba6ff;
|
||||
--accent-hover: #9ff3ff;
|
||||
|
||||
--success: #50fa7b;
|
||||
--warning: #f1fa8c;
|
||||
@ -99,9 +93,7 @@
|
||||
--text-muted: #5c6370;
|
||||
|
||||
--accent-primary: #61afef;
|
||||
--accent-primary-hover: #75bdf5;
|
||||
--accent-secondary: #c678dd;
|
||||
--accent-secondary-hover: #d48ae9;
|
||||
--accent-hover: #75bdf5;
|
||||
|
||||
--success: #98c379;
|
||||
--warning: #e5c07b;
|
||||
@ -125,9 +117,7 @@
|
||||
--text-muted: #586e75;
|
||||
|
||||
--accent-primary: #268bd2;
|
||||
--accent-primary-hover: #4098d9;
|
||||
--accent-secondary: #2aa198;
|
||||
--accent-secondary-hover: #3eb3a8;
|
||||
--accent-hover: #4098d9;
|
||||
|
||||
--success: #859900;
|
||||
--warning: #b58900;
|
||||
@ -151,9 +141,7 @@
|
||||
--text-muted: #616e88;
|
||||
|
||||
--accent-primary: #88c0d0;
|
||||
--accent-primary-hover: #9dcadb;
|
||||
--accent-secondary: #81a1c1;
|
||||
--accent-secondary-hover: #94b0cc;
|
||||
--accent-hover: #9dcadb;
|
||||
|
||||
--success: #a3be8c;
|
||||
--warning: #ebcb8b;
|
||||
@ -177,9 +165,7 @@
|
||||
--text-muted: #6c7086;
|
||||
|
||||
--accent-primary: #89b4fa;
|
||||
--accent-primary-hover: #a6c8ff;
|
||||
--accent-secondary: #f5c2e7;
|
||||
--accent-secondary-hover: #f9d5ee;
|
||||
--accent-hover: #a6c8ff;
|
||||
|
||||
--success: #a6e3a1;
|
||||
--warning: #f9e2af;
|
||||
@ -203,9 +189,7 @@
|
||||
--text-muted: #7a8478;
|
||||
|
||||
--accent-primary: #7fbbb3;
|
||||
--accent-primary-hover: #93c9c1;
|
||||
--accent-secondary: #a7c080;
|
||||
--accent-secondary-hover: #b8cc94;
|
||||
--accent-hover: #93c9c1;
|
||||
|
||||
--success: #a7c080;
|
||||
--warning: #dbbc7f;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<div id="about-content" style="padding: 3rem; max-width: 900px; margin: 0 auto;">
|
||||
<div style="text-align: center; margin-bottom: 3rem;">
|
||||
<h1 style="font-size: 2.5rem; color: #c792ea; margin-bottom: 1rem;">
|
||||
📝 About PersoNotes
|
||||
<i data-lucide="file-text" class="icon-md"></i> About PersoNotes
|
||||
</h1>
|
||||
<p style="font-size: 1.2rem; color: var(--text-secondary); margin-bottom: 2rem;">
|
||||
Un gestionnaire de notes Markdown moderne et puissant
|
||||
@ -10,23 +10,23 @@
|
||||
|
||||
<div style="margin-top: 3rem;">
|
||||
<h2 style="color: #82aaff; font-size: 1.5rem; margin-bottom: 1.5rem;">
|
||||
🚀 Démarrage rapide
|
||||
<i data-lucide="rocket" class="icon-sm"></i> Démarrage rapide
|
||||
</h2>
|
||||
<div style="display: grid; gap: 1.5rem; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));">
|
||||
<div style="background: var(--bg-secondary); border: 1px solid var(--border-primary); border-radius: var(--radius-md); padding: 1.5rem;">
|
||||
<h3 style="color: #89ddff; margin-bottom: 0.5rem;">📁 Parcourir</h3>
|
||||
<h3 style="color: #89ddff; margin-bottom: 0.5rem;"><i data-lucide="folder-open" class="icon-sm"></i> Parcourir</h3>
|
||||
<p style="color: var(--text-secondary); font-size: 0.9rem; margin-bottom: 1rem;">
|
||||
Explorez vos notes dans l'arborescence à gauche
|
||||
</p>
|
||||
</div>
|
||||
<div style="background: var(--bg-secondary); border: 1px solid var(--border-primary); border-radius: var(--radius-md); padding: 1.5rem;">
|
||||
<h3 style="color: #c3e88d; margin-bottom: 0.5rem;">🔍 Rechercher</h3>
|
||||
<h3 style="color: #c3e88d; margin-bottom: 0.5rem;"><i data-lucide="search" class="icon-sm"></i> Rechercher</h3>
|
||||
<p style="color: var(--text-secondary); font-size: 0.9rem; margin-bottom: 1rem;">
|
||||
Utilisez la barre de recherche en haut pour trouver vos notes
|
||||
</p>
|
||||
</div>
|
||||
<div style="background: var(--bg-secondary); border: 1px solid var(--border-primary); border-radius: var(--radius-md); padding: 1.5rem;">
|
||||
<h3 style="color: #ffcb6b; margin-bottom: 0.5rem;">⚡ Slash commands</h3>
|
||||
<h3 style="color: #ffcb6b; margin-bottom: 0.5rem;"><i data-lucide="zap" class="icon-sm"></i> Slash commands</h3>
|
||||
<p style="color: var(--text-secondary); font-size: 0.9rem; margin-bottom: 1rem;">
|
||||
Tapez <code style="color: #f07178;">/</code> dans l'éditeur pour insérer du Markdown
|
||||
</p>
|
||||
@ -36,7 +36,7 @@
|
||||
|
||||
<div style="margin-top: 3rem;">
|
||||
<h2 style="color: #82aaff; font-size: 1.5rem; margin-bottom: 1.5rem;">
|
||||
✨ Fonctionnalités
|
||||
<i data-lucide="sparkles" class="icon-sm"></i> Fonctionnalités
|
||||
</h2>
|
||||
<ul style="color: var(--text-secondary); line-height: 2; list-style: none; padding: 0;">
|
||||
<li style="padding: 0.5rem 0; border-bottom: 1px solid var(--border-secondary);">
|
||||
@ -62,13 +62,13 @@
|
||||
|
||||
<div style="margin-top: 3rem; text-align: center; padding: 2rem; background: var(--bg-secondary); border-radius: var(--radius-md); border: 1px solid var(--border-primary);">
|
||||
<p style="color: var(--text-muted); font-size: 0.9rem;">
|
||||
💡 Astuce : Cliquez sur une note dans l'arborescence pour commencer à éditer
|
||||
<i data-lucide="lightbulb" class="icon-sm"></i> Astuce : Cliquez sur une note dans l'arborescence pour commencer à éditer
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 3rem;">
|
||||
<h2 style="color: #82aaff; font-size: 1.5rem; margin-bottom: 1.5rem;">
|
||||
⌨️ Raccourcis clavier
|
||||
<i data-lucide="keyboard" class="icon-sm"></i> Raccourcis clavier
|
||||
</h2>
|
||||
<div style="display: grid; gap: 0.75rem; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));">
|
||||
<div style="background: var(--bg-secondary); border: 1px solid var(--border-primary); border-radius: var(--radius-sm); padding: 0.75rem; display: flex; justify-content: space-between; align-items: center;">
|
||||
@ -113,7 +113,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<p style="color: var(--text-muted); font-size: 0.85rem; text-align: center; margin-top: 1.5rem;">
|
||||
💡 Sur Mac, utilisez Cmd au lieu de Ctrl
|
||||
<i data-lucide="lightbulb" class="icon-sm"></i> Sur Mac, utilisez Cmd au lieu de Ctrl
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -57,7 +57,7 @@
|
||||
data-i18n="calendar.today"
|
||||
title="Ouvrir la note du jour (Ctrl/Cmd+D)"
|
||||
style="flex: 1; padding: 0.5rem; font-size: 0.85rem;">
|
||||
📅 Aujourd'hui
|
||||
<i data-lucide="calendar-check" class="icon-sm"></i> Aujourd'hui
|
||||
</button>
|
||||
<button class="daily-today-btn"
|
||||
hx-get="/api/daily/calendar/{{.CurrentMonth}}"
|
||||
@ -66,7 +66,7 @@
|
||||
data-i18n="calendar.thisMonth"
|
||||
title="Revenir au mois actuel"
|
||||
style="flex: 1; padding: 0.5rem; font-size: 0.85rem;">
|
||||
🗓️ Ce mois
|
||||
<i data-lucide="calendar-days" class="icon-sm"></i> Ce mois
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
</label>
|
||||
{{if .IsHome}}
|
||||
<button type="button" class="toggle-preview-btn" hx-get="/api/home" hx-target="#editor-container" hx-swap="innerHTML" hx-push-url="true" data-i18n="editor.refresh" title="Actualiser la page d'accueil">
|
||||
🔄 Actualiser
|
||||
<i data-lucide="refresh-cw" class="icon-sm"></i> Actualiser
|
||||
</button>
|
||||
{{else}}
|
||||
<button type="button" id="toggle-preview-btn" class="toggle-preview-btn" onclick="togglePreview()" data-i18n-title="editor.togglePreview" title="Mode: Éditeur + Preview (cliquer pour Éditeur seul)">
|
||||
@ -36,12 +36,12 @@
|
||||
</div>
|
||||
{{if .Backlinks}}
|
||||
<div id="backlinks-section" class="backlinks-section">
|
||||
<h3 class="backlinks-title">🔗 Référencé par</h3>
|
||||
<h3 class="backlinks-title"><i data-lucide="link" class="icon-sm"></i> Référencé par</h3>
|
||||
<ul class="backlinks-list">
|
||||
{{range .Backlinks}}
|
||||
<li class="backlink-item">
|
||||
<a href="#" onclick="return false;" hx-get="/api/notes/{{.Path}}" hx-target="#editor-container" hx-swap="innerHTML" hx-push-url="true" class="backlink-link">
|
||||
📄 {{.Title}}
|
||||
<i data-lucide="file-text" class="icon-sm"></i> {{.Title}}
|
||||
</a>
|
||||
</li>
|
||||
{{end}}
|
||||
@ -54,6 +54,17 @@
|
||||
<div class="editor-actions">
|
||||
<div class="editor-actions-primary">
|
||||
<button type="submit" data-i18n="editor.save">Enregistrer</button>
|
||||
<button
|
||||
id="toggle-public-btn"
|
||||
type="button"
|
||||
class="secondary {{if .IsPublic}}public-active{{end}}"
|
||||
data-path="{{.Filename}}"
|
||||
data-is-public="{{.IsPublic}}"
|
||||
data-i18n-title="{{if .IsPublic}}public.titlePublic{{else}}public.titlePrivate{{end}}"
|
||||
title="{{if .IsPublic}}This note is public - Click to make it private{{else}}This note is private - Click to make it public{{end}}"
|
||||
>
|
||||
{{if .IsPublic}}<span data-i18n="public.buttonPublic"><i data-lucide="globe" class="icon-sm"></i> Public</span>{{else}}<span data-i18n="public.buttonPrivate"><i data-lucide="lock" class="icon-sm"></i> Private</span>{{end}}
|
||||
</button>
|
||||
<button
|
||||
hx-delete="/api/notes/{{.Filename}}"
|
||||
hx-confirm="Êtes-vous sûr de vouloir supprimer cette note ({{.Filename}}) ?"
|
||||
|
||||
@ -3,12 +3,12 @@
|
||||
<div class="favorite-item" data-path="{{.Path}}">
|
||||
{{if .IsDir}}
|
||||
<div class="favorite-folder" data-path="{{.Path}}">
|
||||
<span class="favorite-icon">⭐</span>
|
||||
<span class="favorite-icon"><i data-lucide="star" class="icon-sm"></i></span>
|
||||
<span class="favorite-folder-icon">{{.Icon}}</span>
|
||||
<span class="favorite-name">{{.Title}}</span>
|
||||
<button class="favorite-remove"
|
||||
onclick="removeFavorite('{{.Path}}')"
|
||||
title="Retirer des favoris">×</button>
|
||||
title="Retirer des favoris"><i data-lucide="x" class="icon-xs"></i></button>
|
||||
</div>
|
||||
{{else}}
|
||||
<a href="#"
|
||||
@ -18,12 +18,12 @@
|
||||
hx-target="#editor-container"
|
||||
hx-swap="innerHTML"
|
||||
hx-push-url="true">
|
||||
<span class="favorite-icon">⭐</span>
|
||||
<span class="favorite-icon"><i data-lucide="star" class="icon-sm"></i></span>
|
||||
<span class="favorite-file-icon">{{.Icon}}</span>
|
||||
<span class="favorite-name">{{.Title}}</span>
|
||||
<button class="favorite-remove"
|
||||
onclick="event.preventDefault(); event.stopPropagation(); removeFavorite('{{.Path}}')"
|
||||
title="Retirer des favoris">×</button>
|
||||
title="Retirer des favoris"><i data-lucide="x" class="icon-xs"></i></button>
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
@ -32,7 +32,7 @@
|
||||
<p class="favorites-empty">
|
||||
Aucun favori.<br>
|
||||
<span style="font-size: 0.75rem; color: var(--text-muted);">
|
||||
Cliquez sur ⭐ à côté d'une note ou d'un dossier pour l'ajouter.
|
||||
Cliquez sur <i data-lucide="star" class="icon-sm" style="display: inline; width: 14px; height: 14px; vertical-align: middle;"></i> à côté d'une note ou d'un dossier pour l'ajouter.
|
||||
</span>
|
||||
</p>
|
||||
{{end}}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<!-- Indicateur de racine (maintenant cliquable et rétractable) -->
|
||||
<div class="sidebar-section-header" data-section="notes" data-path="" data-is-dir="true" onclick="toggleSidebarSection('notes', event)" style="cursor: pointer;">
|
||||
<span class="section-toggle expanded">▶</span>
|
||||
<span class="folder-icon">🏠</span>
|
||||
<span class="folder-icon"><i data-lucide="home" class="icon-sm"></i></span>
|
||||
<span class="folder-name">Racine</span>
|
||||
<span class="root-hint">(notes/)</span>
|
||||
</div>
|
||||
@ -28,7 +28,7 @@
|
||||
<div class="folder-header">
|
||||
<input type="checkbox" class="selection-checkbox folder-checkbox" data-path="{{.Path}}" data-is-dir="true" style="display: none;">
|
||||
<span class="folder-toggle">▶</span>
|
||||
<span class="folder-icon">📁</span>
|
||||
<span class="folder-icon"><i data-lucide="folder" class="icon-sm"></i></span>
|
||||
<span class="folder-name">{{.Name}}</span>
|
||||
</div>
|
||||
<div class="folder-children" style="display: none;">
|
||||
@ -49,7 +49,7 @@
|
||||
hx-swap="innerHTML"
|
||||
hx-push-url="true"
|
||||
draggable="true">
|
||||
📄 {{.Name}}
|
||||
<i data-lucide="file-text" class="icon-sm"></i> {{.Name}}
|
||||
</a>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
@ -16,13 +16,15 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/dompurify/dist/purify.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js"></script>
|
||||
<!-- Lucide Icons - Professional SVG icons -->
|
||||
<script src="https://unpkg.com/lucide@latest"></script>
|
||||
<script src="/static/sidebar-resize.js"></script>
|
||||
<script type="module" src="/static/dist/personotes-frontend.es.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<button id="toggle-sidebar-btn" title="Afficher/Masquer la barre latérale (Ctrl/Cmd+B)" style="background: none; border: none; padding: 0; margin-right: 1rem; cursor: pointer; color: var(--text-primary); display: flex; align-items: center;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line></svg>
|
||||
<i data-lucide="menu" style="width: 24px; height: 24px;"></i>
|
||||
</button>
|
||||
<div style="display: flex; align-items: center; gap: 0.75rem;">
|
||||
<img src="/static/images/logo.svg" alt="Logo" style="width: 40px; height: 40px;">
|
||||
@ -46,10 +48,10 @@
|
||||
style="white-space: nowrap;"
|
||||
data-i18n="menu.home"
|
||||
title="Retour à la page d'accueil (Ctrl/Cmd+H)">
|
||||
🏠 Accueil
|
||||
<i data-lucide="home" class="icon-sm"></i> Accueil
|
||||
</button>
|
||||
<button onclick="showNewNoteModal()" style="white-space: nowrap;" data-i18n="menu.newNote" title="Créer une nouvelle note (Ctrl/Cmd+N)">
|
||||
✨ Nouvelle note
|
||||
<i data-lucide="file-plus" class="icon-sm"></i> Nouvelle note
|
||||
</button>
|
||||
</header>
|
||||
|
||||
@ -57,7 +59,7 @@
|
||||
<div id="new-note-modal" style="display: none;">
|
||||
<div class="modal-overlay" onclick="hideNewNoteModal()"></div>
|
||||
<div class="modal-content">
|
||||
<h2>📝 Nouvelle note</h2>
|
||||
<h2><i data-lucide="file-text" class="icon-md"></i> Nouvelle note</h2>
|
||||
<form onsubmit="handleNewNote(event)">
|
||||
<label for="note-name">Nom de la note</label>
|
||||
<input
|
||||
@ -69,7 +71,7 @@
|
||||
required
|
||||
/>
|
||||
<p style="color: var(--text-muted); font-size: 0.85rem; margin-top: 0.5rem;">
|
||||
💡 Si la note existe déjà, elle sera ouverte.
|
||||
<i data-lucide="lightbulb" class="icon-sm"></i> Si la note existe déjà, elle sera ouverte.
|
||||
</p>
|
||||
<div style="display: flex; gap: 0.5rem; margin-top: 1.5rem;">
|
||||
<button type="submit">Créer / Ouvrir</button>
|
||||
@ -83,7 +85,7 @@
|
||||
<div id="new-folder-modal" style="display: none;">
|
||||
<div class="modal-overlay" onclick="hideNewFolderModal()"></div>
|
||||
<div class="modal-content">
|
||||
<h2>📁 Nouveau dossier</h2>
|
||||
<h2><i data-lucide="folder-plus" class="icon-md"></i> Nouveau dossier</h2>
|
||||
<form onsubmit="handleNewFolder(event)">
|
||||
<label for="folder-name">Nom du dossier</label>
|
||||
<input
|
||||
@ -95,7 +97,7 @@
|
||||
required
|
||||
/>
|
||||
<p style="color: var(--text-muted); font-size: 0.85rem; margin-top: 0.5rem;">
|
||||
💡 Vous pouvez créer des sous-dossiers avec "/", ex: projets/backend
|
||||
<i data-lucide="lightbulb" class="icon-sm"></i> Vous pouvez créer des sous-dossiers avec "/", ex: projets/backend
|
||||
</p>
|
||||
<div style="display: flex; gap: 0.5rem; margin-top: 1.5rem;">
|
||||
<button type="submit">Créer</button>
|
||||
@ -114,10 +116,7 @@
|
||||
<span id="selection-count" class="selection-count">0 élément(s) sélectionné(s)</span>
|
||||
<div class="toolbar-actions">
|
||||
<button onclick="deleteSelected()" class="danger-button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="3 6 5 6 21 6"></polyline>
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
|
||||
</svg>
|
||||
<i data-lucide="trash-2" class="icon-sm"></i>
|
||||
Supprimer
|
||||
</button>
|
||||
<button onclick="cancelSelection()" class="secondary">
|
||||
@ -131,13 +130,13 @@
|
||||
<div id="delete-confirmation-modal" style="display: none;">
|
||||
<div class="modal-overlay" onclick="hideDeleteConfirmationModal()"></div>
|
||||
<div class="modal-content">
|
||||
<h2 style="color: var(--error);">⚠️ Confirmer la suppression</h2>
|
||||
<h2 style="color: var(--error);"><i data-lucide="alert-triangle" class="icon-md"></i> Confirmer la suppression</h2>
|
||||
<p>Vous êtes sur le point de supprimer <strong id="delete-count">0</strong> élément(s) :</p>
|
||||
<div id="delete-items-list" style="max-height: 300px; overflow-y: auto; margin: 1rem 0; padding: 0.5rem; background: var(--bg-tertiary); border-radius: var(--radius-md);">
|
||||
<!-- Liste des éléments à supprimer -->
|
||||
</div>
|
||||
<p style="color: var(--warning); font-size: 0.9rem;">
|
||||
⚠️ Cette action est <strong>irréversible</strong>. Les dossiers seront supprimés avec tout leur contenu.
|
||||
<i data-lucide="alert-triangle" class="icon-sm"></i> Cette action est <strong>irréversible</strong>. Les dossiers seront supprimés avec tout leur contenu.
|
||||
</p>
|
||||
<div style="display: flex; gap: 0.5rem; margin-top: 1.5rem;">
|
||||
<button onclick="confirmDelete()" class="danger-button">Confirmer la suppression</button>
|
||||
@ -151,22 +150,22 @@
|
||||
<div class="theme-modal-overlay" onclick="closeThemeModal()"></div>
|
||||
<div class="theme-modal-content">
|
||||
<h2>
|
||||
⚙️ Paramètres d'apparence
|
||||
<i data-lucide="settings" class="icon-md"></i> Paramètres d'apparence
|
||||
</h2>
|
||||
|
||||
<!-- Onglets -->
|
||||
<div class="settings-tabs">
|
||||
<button class="settings-tab active" onclick="switchSettingsTab('themes')">
|
||||
🎨 Thèmes
|
||||
<i data-lucide="palette" class="icon-sm"></i> Thèmes
|
||||
</button>
|
||||
<button class="settings-tab" onclick="switchSettingsTab('fonts')">
|
||||
🔤 Polices
|
||||
<i data-lucide="type" class="icon-sm"></i> Polices
|
||||
</button>
|
||||
<button class="settings-tab" onclick="switchSettingsTab('editor')">
|
||||
⌨️ Éditeur
|
||||
<i data-lucide="keyboard" class="icon-sm"></i> Éditeur
|
||||
</button>
|
||||
<button class="settings-tab" onclick="switchSettingsTab('other')">
|
||||
⚙️ Autre
|
||||
<i data-lucide="settings" class="icon-sm"></i> Autre
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -176,7 +175,7 @@
|
||||
<!-- Material Dark -->
|
||||
<div class="theme-card active" data-theme="material-dark" onclick="selectTheme('material-dark')">
|
||||
<div class="theme-card-header">
|
||||
<span class="theme-card-icon">🌙</span>
|
||||
<span class="theme-card-icon"><i data-lucide="moon" class="icon-sm"></i></span>
|
||||
<span class="theme-card-name">Material Dark</span>
|
||||
</div>
|
||||
<div class="theme-preview">
|
||||
@ -191,7 +190,7 @@
|
||||
<!-- Monokai Dark -->
|
||||
<div class="theme-card" data-theme="monokai-dark" onclick="selectTheme('monokai-dark')">
|
||||
<div class="theme-card-header">
|
||||
<span class="theme-card-icon">🎨</span>
|
||||
<span class="theme-card-icon"><i data-lucide="palette" class="icon-sm"></i></span>
|
||||
<span class="theme-card-name">Monokai Dark</span>
|
||||
</div>
|
||||
<div class="theme-preview">
|
||||
@ -206,7 +205,7 @@
|
||||
<!-- Dracula -->
|
||||
<div class="theme-card" data-theme="dracula" onclick="selectTheme('dracula')">
|
||||
<div class="theme-card-header">
|
||||
<span class="theme-card-icon">🧛</span>
|
||||
<span class="theme-card-icon"><i data-lucide="moon-star" class="icon-sm"></i></span>
|
||||
<span class="theme-card-name">Dracula</span>
|
||||
</div>
|
||||
<div class="theme-preview">
|
||||
@ -221,7 +220,7 @@
|
||||
<!-- One Dark -->
|
||||
<div class="theme-card" data-theme="one-dark" onclick="selectTheme('one-dark')">
|
||||
<div class="theme-card-header">
|
||||
<span class="theme-card-icon">⚡</span>
|
||||
<span class="theme-card-icon"><i data-lucide="zap" class="icon-sm"></i></span>
|
||||
<span class="theme-card-name">One Dark</span>
|
||||
</div>
|
||||
<div class="theme-preview">
|
||||
@ -236,7 +235,7 @@
|
||||
<!-- Solarized Dark -->
|
||||
<div class="theme-card" data-theme="solarized-dark" onclick="selectTheme('solarized-dark')">
|
||||
<div class="theme-card-header">
|
||||
<span class="theme-card-icon">☀️</span>
|
||||
<span class="theme-card-icon"><i data-lucide="sun" class="icon-sm"></i></span>
|
||||
<span class="theme-card-name">Solarized Dark</span>
|
||||
</div>
|
||||
<div class="theme-preview">
|
||||
@ -251,7 +250,7 @@
|
||||
<!-- Nord -->
|
||||
<div class="theme-card" data-theme="nord" onclick="selectTheme('nord')">
|
||||
<div class="theme-card-header">
|
||||
<span class="theme-card-icon">❄️</span>
|
||||
<span class="theme-card-icon"><i data-lucide="snowflake" class="icon-sm"></i></span>
|
||||
<span class="theme-card-name">Nord</span>
|
||||
</div>
|
||||
<div class="theme-preview">
|
||||
@ -266,7 +265,7 @@
|
||||
<!-- Catppuccin -->
|
||||
<div class="theme-card" data-theme="catppuccin" onclick="selectTheme('catppuccin')">
|
||||
<div class="theme-card-header">
|
||||
<span class="theme-card-icon">🌸</span>
|
||||
<span class="theme-card-icon"><i data-lucide="flower" class="icon-sm"></i></span>
|
||||
<span class="theme-card-name">Catppuccin</span>
|
||||
</div>
|
||||
<div class="theme-preview">
|
||||
@ -281,7 +280,7 @@
|
||||
<!-- Everforest -->
|
||||
<div class="theme-card" data-theme="everforest" onclick="selectTheme('everforest')">
|
||||
<div class="theme-card-header">
|
||||
<span class="theme-card-icon">🌲</span>
|
||||
<span class="theme-card-icon"><i data-lucide="tree-deciduous" class="icon-sm"></i></span>
|
||||
<span class="theme-card-name">Everforest</span>
|
||||
</div>
|
||||
<div class="theme-preview">
|
||||
@ -301,7 +300,7 @@
|
||||
<!-- Fira Code -->
|
||||
<div class="font-card" data-font="fira-code" onclick="selectFont('fira-code')">
|
||||
<div class="font-card-header">
|
||||
<span class="font-card-icon">💻</span>
|
||||
<span class="font-card-icon"><i data-lucide="code" class="icon-sm"></i></span>
|
||||
<span class="font-card-name">Fira Code</span>
|
||||
</div>
|
||||
<div class="font-preview" style="font-family: 'Fira Code', monospace;">
|
||||
@ -313,7 +312,7 @@
|
||||
<!-- Sans-serif -->
|
||||
<div class="font-card" data-font="sans-serif" onclick="selectFont('sans-serif')">
|
||||
<div class="font-card-header">
|
||||
<span class="font-card-icon">📝</span>
|
||||
<span class="font-card-icon"><i data-lucide="type" class="icon-sm"></i></span>
|
||||
<span class="font-card-name">Sans-serif</span>
|
||||
</div>
|
||||
<div class="font-preview" style="font-family: -apple-system, sans-serif;">
|
||||
@ -325,7 +324,7 @@
|
||||
<!-- Inter -->
|
||||
<div class="font-card" data-font="inter" onclick="selectFont('inter')">
|
||||
<div class="font-card-header">
|
||||
<span class="font-card-icon">✨</span>
|
||||
<span class="font-card-icon"><i data-lucide="sparkles" class="icon-sm"></i></span>
|
||||
<span class="font-card-name">Inter</span>
|
||||
</div>
|
||||
<div class="font-preview" style="font-family: 'Inter', sans-serif;">
|
||||
@ -337,7 +336,7 @@
|
||||
<!-- Poppins -->
|
||||
<div class="font-card" data-font="poppins" onclick="selectFont('poppins')">
|
||||
<div class="font-card-header">
|
||||
<span class="font-card-icon">🎯</span>
|
||||
<span class="font-card-icon"><i data-lucide="target" class="icon-sm"></i></span>
|
||||
<span class="font-card-name">Poppins</span>
|
||||
</div>
|
||||
<div class="font-preview" style="font-family: 'Poppins', sans-serif;">
|
||||
@ -349,7 +348,7 @@
|
||||
<!-- Public Sans -->
|
||||
<div class="font-card" data-font="public-sans" onclick="selectFont('public-sans')">
|
||||
<div class="font-card-header">
|
||||
<span class="font-card-icon">🏛️</span>
|
||||
<span class="font-card-icon"><i data-lucide="building" class="icon-sm"></i></span>
|
||||
<span class="font-card-name">Public Sans</span>
|
||||
</div>
|
||||
<div class="font-preview" style="font-family: 'Public Sans', sans-serif;">
|
||||
@ -361,7 +360,7 @@
|
||||
<!-- JetBrains Mono -->
|
||||
<div class="font-card active" data-font="jetbrains-mono" onclick="selectFont('jetbrains-mono')">
|
||||
<div class="font-card-header">
|
||||
<span class="font-card-icon">⚡</span>
|
||||
<span class="font-card-icon"><i data-lucide="zap" class="icon-sm"></i></span>
|
||||
<span class="font-card-name">JetBrains Mono</span>
|
||||
</div>
|
||||
<div class="font-preview" style="font-family: 'JetBrains Mono', monospace;">
|
||||
@ -373,7 +372,7 @@
|
||||
<!-- Cascadia Code -->
|
||||
<div class="font-card" data-font="cascadia-code" onclick="selectFont('cascadia-code')">
|
||||
<div class="font-card-header">
|
||||
<span class="font-card-icon">🪟</span>
|
||||
<span class="font-card-icon"><i data-lucide="square" class="icon-sm"></i></span>
|
||||
<span class="font-card-name">Cascadia Code</span>
|
||||
</div>
|
||||
<div class="font-preview" style="font-family: 'Cascadia Code', monospace;">
|
||||
@ -385,7 +384,7 @@
|
||||
<!-- Source Code Pro -->
|
||||
<div class="font-card" data-font="source-code-pro" onclick="selectFont('source-code-pro')">
|
||||
<div class="font-card-header">
|
||||
<span class="font-card-icon">🔧</span>
|
||||
<span class="font-card-icon"><i data-lucide="wrench" class="icon-sm"></i></span>
|
||||
<span class="font-card-name">Source Code Pro</span>
|
||||
</div>
|
||||
<div class="font-preview" style="font-family: 'Source Code Pro', monospace;">
|
||||
@ -397,7 +396,7 @@
|
||||
|
||||
<!-- Sélecteur de taille de police -->
|
||||
<div style="margin-top: var(--spacing-lg); padding-top: var(--spacing-lg); border-top: 1px solid var(--border-primary);">
|
||||
<h3 style="font-size: 1rem; color: var(--text-primary); margin-bottom: var(--spacing-md);">📏 Taille de police</h3>
|
||||
<h3 style="font-size: 1rem; color: var(--text-primary); margin-bottom: var(--spacing-md);"><i data-lucide="ruler" class="icon-sm"></i> Taille de police</h3>
|
||||
<div class="font-size-selector">
|
||||
<button class="font-size-option" data-size="small" onclick="selectFontSize('small')">
|
||||
<span class="size-label">Petite</span>
|
||||
@ -421,7 +420,7 @@
|
||||
|
||||
<!-- Section Éditeur -->
|
||||
<div id="editor-section" class="settings-section" style="display: none;">
|
||||
<h3 style="font-size: 1.1rem; color: var(--text-primary); margin-bottom: var(--spacing-lg);">⌨️ Mode d'édition</h3>
|
||||
<h3 style="font-size: 1.1rem; color: var(--text-primary); margin-bottom: var(--spacing-lg);"><i data-lucide="keyboard" class="icon-sm"></i> Mode d'édition</h3>
|
||||
|
||||
<!-- Toggle Mode Vim -->
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; padding: var(--spacing-lg); background: var(--bg-secondary); border-radius: var(--radius-md); border: 1px solid var(--border-primary);">
|
||||
@ -444,21 +443,21 @@
|
||||
|
||||
<div style="margin-top: var(--spacing-md); padding: var(--spacing-md); background: var(--bg-tertiary); border-radius: var(--radius-sm); border-left: 3px solid var(--accent-primary);">
|
||||
<p style="font-size: 0.85rem; color: var(--text-muted); margin: 0;">
|
||||
💡 <strong>Astuce :</strong> Le mode Vim sera appliqué immédiatement à l'éditeur actuel. Si vous ouvrez une nouvelle note, le mode restera activé.
|
||||
<i data-lucide="lightbulb" class="icon-sm"></i> <strong>Astuce :</strong> Le mode Vim sera appliqué immédiatement à l'éditeur actuel. Si vous ouvrez une nouvelle note, le mode restera activé.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section Autre (Langue) -->
|
||||
<div id="other-section" class="settings-section" style="display: none;">
|
||||
<h3 style="font-size: 1.1rem; color: var(--text-primary); margin-bottom: var(--spacing-lg);">🌍 Langue / Language</h3>
|
||||
<h3 style="font-size: 1.1rem; color: var(--text-primary); margin-bottom: var(--spacing-lg);"><i data-lucide="languages" class="icon-sm"></i> Langue / Language</h3>
|
||||
|
||||
<div style="display: flex; flex-direction: column; gap: var(--spacing-md);">
|
||||
<label class="language-option" style="display: flex; align-items: center; padding: var(--spacing-lg); background: var(--bg-secondary); border-radius: var(--radius-md); border: 2px solid var(--border-primary); cursor: pointer; transition: all 0.2s ease;">
|
||||
<input type="radio" name="language" value="en" style="margin-right: var(--spacing-md); width: 20px; height: 20px; cursor: pointer;">
|
||||
<div style="flex: 1;">
|
||||
<div style="font-weight: 500; color: var(--text-primary); margin-bottom: 0.25rem; font-size: 1rem;">
|
||||
🇬🇧 English
|
||||
English
|
||||
</div>
|
||||
<div style="font-size: 0.85rem; color: var(--text-secondary);">
|
||||
English interface
|
||||
@ -470,7 +469,7 @@
|
||||
<input type="radio" name="language" value="fr" style="margin-right: var(--spacing-md); width: 20px; height: 20px; cursor: pointer;">
|
||||
<div style="flex: 1;">
|
||||
<div style="font-weight: 500; color: var(--text-primary); margin-bottom: 0.25rem; font-size: 1rem;">
|
||||
🇫🇷 Français
|
||||
Français
|
||||
</div>
|
||||
<div style="font-size: 0.85rem; color: var(--text-secondary);">
|
||||
Interface en français
|
||||
@ -489,10 +488,10 @@
|
||||
<div class="main-layout">
|
||||
<aside id="sidebar">
|
||||
<button class="sidebar-close-btn" onclick="toggleSidebar()" title="Fermer le menu">
|
||||
✕
|
||||
<i data-lucide="x" class="icon-md"></i>
|
||||
</button>
|
||||
<section>
|
||||
<h2 class="sidebar-section-title">🔍 Recherche</h2>
|
||||
<h2 class="sidebar-section-title"><i data-lucide="search" class="icon-sm"></i> Recherche</h2>
|
||||
<div id="search-results">
|
||||
<!-- Les résultats de la recherche apparaîtront ici -->
|
||||
</div>
|
||||
@ -503,7 +502,7 @@
|
||||
<section>
|
||||
<div class="sidebar-section-header" data-section="favorites" onclick="toggleSidebarSection('favorites', event)" style="cursor: pointer; display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem 0;">
|
||||
<span class="section-toggle expanded">▶</span>
|
||||
<h2 class="sidebar-section-title" data-i18n="sidebar.favorites" style="margin: 0; flex: 1;">⭐ Favoris</h2>
|
||||
<h2 class="sidebar-section-title" data-i18n="sidebar.favorites" style="margin: 0; flex: 1;"><i data-lucide="star" class="icon-sm"></i> Favoris</h2>
|
||||
</div>
|
||||
<div class="sidebar-section-content" id="favorites-content" style="display: block;">
|
||||
<div id="favorites-list"
|
||||
@ -521,7 +520,7 @@
|
||||
<section>
|
||||
<div class="sidebar-section-header" data-section="daily-notes" onclick="toggleSidebarSection('daily-notes', event)" style="cursor: pointer; display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem 0;">
|
||||
<span class="section-toggle expanded">▶</span>
|
||||
<h2 class="sidebar-section-title" data-i18n="sidebar.daily" style="margin: 0; flex: 1;">📅 Daily Notes</h2>
|
||||
<h2 class="sidebar-section-title" data-i18n="sidebar.daily" style="margin: 0; flex: 1;"><i data-lucide="calendar" class="icon-sm"></i> Daily Notes</h2>
|
||||
</div>
|
||||
<div class="sidebar-section-content" id="daily-notes-content" style="display: block;">
|
||||
<div id="daily-calendar-container"
|
||||
@ -546,12 +545,9 @@
|
||||
|
||||
<section>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--spacing-sm);">
|
||||
<h2 class="sidebar-section-title" style="margin: 0;">📚 Notes</h2>
|
||||
<h2 class="sidebar-section-title" style="margin: 0;"><i data-lucide="notebook" class="icon-sm"></i> Notes</h2>
|
||||
<button id="toggle-selection-mode" onclick="toggleSelectionMode()" class="icon-button" title="Mode sélection">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="9 11 12 14 22 4"></polyline>
|
||||
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path>
|
||||
</svg>
|
||||
<i data-lucide="check-square" style="width: 18px; height: 18px;"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div id="file-tree" hx-get="/api/tree" hx-trigger="load once delay:250ms" hx-swap="innerHTML">
|
||||
@ -562,17 +558,14 @@
|
||||
|
||||
<!-- Bouton Nouveau dossier avant les paramètres -->
|
||||
<button onclick="showNewFolderModal()" class="folder-create-btn sidebar-action-btn" data-i18n="fileTree.newFolder" title="Créer un nouveau dossier (Ctrl/Cmd+Shift+F)">
|
||||
📁 Nouveau dossier
|
||||
<i data-lucide="folder-plus" class="icon-sm"></i> Nouveau dossier
|
||||
</button>
|
||||
|
||||
<!-- Boutons du bas de la sidebar -->
|
||||
<div style="display: flex; gap: 0.5rem; align-items: stretch;">
|
||||
<!-- Bouton Paramètres (thèmes) -->
|
||||
<button id="theme-settings-btn" class="sidebar-action-btn" onclick="openThemeModal()" title="Ouvrir les paramètres (Ctrl/Cmd+,)" style="flex: 1; display: flex; align-items: center; gap: 0.5rem; padding: 0.6rem 0.8rem; font-size: 0.85rem;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="3"></circle>
|
||||
<path d="M12 1v6m0 6v6m-6-6h6m6 0h-6m-5.3-5.3l4.2 4.2m4.2 4.2l4.2 4.2m0-12.6l-4.2 4.2m-4.2 4.2L2.7 19.3"></path>
|
||||
</svg>
|
||||
<i data-lucide="settings" style="width: 16px; height: 16px;"></i>
|
||||
<span data-i18n="settings.title">Paramètres</span>
|
||||
</button>
|
||||
|
||||
@ -584,11 +577,7 @@
|
||||
hx-target="#editor-container"
|
||||
hx-swap="innerHTML"
|
||||
style="display: flex; align-items: center; justify-content: center; padding: 0.6rem; min-width: auto; opacity: 0.7;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<line x1="12" y1="16" x2="12" y2="12"></line>
|
||||
<line x1="12" y1="8" x2="12.01" y2="8"></line>
|
||||
</svg>
|
||||
<i data-lucide="info" style="width: 16px; height: 16px;"></i>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
@ -598,9 +587,7 @@
|
||||
hx-trigger="load once"
|
||||
hx-swap="innerHTML">
|
||||
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 50vh; text-align: center; color: var(--text-secondary);">
|
||||
<svg style="width: 64px; height: 64px; margin-bottom: 1rem; opacity: 0.5;" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
<i data-lucide="file-text" style="width: 64px; height: 64px; margin-bottom: 1rem; opacity: 0.5;"></i>
|
||||
<p style="font-size: 1.1rem; margin: 0;">Chargement...</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -695,5 +682,33 @@
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Lucide Icons Initialization & htmx Integration -->
|
||||
<script>
|
||||
// Initialize Lucide icons on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (typeof lucide !== 'undefined') {
|
||||
lucide.createIcons();
|
||||
}
|
||||
});
|
||||
|
||||
// Reinitialize Lucide icons after htmx swaps
|
||||
document.body.addEventListener('htmx:afterSwap', function(event) {
|
||||
if (typeof lucide !== 'undefined') {
|
||||
lucide.createIcons({
|
||||
nameAttr: 'data-lucide'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Reinitialize after out-of-band swaps (file-tree updates)
|
||||
document.body.addEventListener('htmx:oobAfterSwap', function(event) {
|
||||
if (typeof lucide !== 'undefined') {
|
||||
lucide.createIcons({
|
||||
nameAttr: 'data-lucide'
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -12,7 +12,7 @@
|
||||
hx-target="#editor-container"
|
||||
hx-swap="innerHTML"
|
||||
hx-push-url="true">
|
||||
<div class="search-result-icon">📄</div>
|
||||
<div class="search-result-icon"><i data-lucide="file-text" class="icon-md"></i></div>
|
||||
<div class="search-result-content">
|
||||
<div class="search-result-header">
|
||||
<span class="search-result-title">{{.Title}}</span>
|
||||
@ -40,14 +40,14 @@
|
||||
</ul>
|
||||
{{else}}
|
||||
<div class="search-no-results">
|
||||
<div class="search-no-results-icon">🔍</div>
|
||||
<div class="search-no-results-icon"><i data-lucide="search" class="icon-lg"></i></div>
|
||||
<p class="search-no-results-text">Aucun résultat pour « <strong>{{.Query}}</strong> »</p>
|
||||
<p class="search-no-results-hint">Essayez d'autres mots-clés ou utilisez les filtres</p>
|
||||
</div>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<div class="search-help">
|
||||
<p class="search-help-title">💡 Recherche avancée</p>
|
||||
<p class="search-help-title"><i data-lucide="lightbulb" class="icon-sm"></i> Recherche avancée</p>
|
||||
<p class="search-help-text">Saisissez des mots-clés pour rechercher dans vos notes</p>
|
||||
<div class="search-help-examples">
|
||||
<div class="search-help-example">
|
||||
|
||||
Reference in New Issue
Block a user