Add backlink

This commit is contained in:
2025-11-12 09:31:09 +01:00
parent 5e30a5cf5d
commit 584a4a0acd
25 changed files with 1769 additions and 79 deletions

View File

@ -170,6 +170,58 @@ User types in editor → CodeMirror EditorView.updateListener
└─ HTMX updates file tree automatically └─ HTMX updates file tree automatically
``` ```
### 6. Creating Links Between Notes (Internal Links)
```
User types /ilink → SlashCommands detects slash + query
├─ Filters commands by query
└─ Shows command palette
└─ User selects "ilink" → SlashCommands.openLinkInserter()
├─ Saves current cursor position
└─ Opens LinkInserter modal
User types query → LinkInserter searches
├─ Debounce 200ms
└─ fetch('/api/search?query=...')
├─ Go Server queries indexer
├─ Returns HTML results
└─ LinkInserter parses HTML
├─ Extracts title, path, tags
├─ Renders in modal
└─ Updates keyboard selection
User selects note → LinkInserter.selectResult()
(Enter/click) │
├─ Calls callback with {title, path}
└─ SlashCommands.openLinkInserter callback
├─ Builds HTML with HTMX: <a href="#" hx-get="/api/notes/path">title</a>
├─ Uses CodeMirror transaction
├─ Replaces /ilink with HTML link
├─ Positions cursor after link
└─ Closes modal
Preview Rendering → marked.js parses Markdown (including inline HTML)
├─ DOMPurify sanitizes (allows hx-* attributes)
├─ htmx.process() activates HTMX on links
└─ Links become clickable → load note via HTMX
```
**Key Design Decisions**:
- **No new backend code**: Reuses existing `/api/search` endpoint for search, `/api/notes/` for navigation
- **Database-free**: Leverages in-memory indexer for speed
- **Consistent UX**: Modal design matches SearchModal styling
- **Clickable links**: HTML with HTMX attributes, rendered directly by marked.js
- **HTMX integration**: Links use `hx-get` to load notes without page reload
- **Keyboard-first**: Full keyboard navigation without mouse
## Frontend Architecture ## Frontend Architecture
### Build Process (Vite) ### Build Process (Vite)
@ -224,6 +276,17 @@ Executed in browser → Initializes components
- Result highlighting - Result highlighting
- Uses HTMX search API - Uses HTMX search API
**link-inserter.js**
- LinkInserter class for internal note linking
- Modal search interface for `/ilink` command
- Fuzzy search across notes
- Keyboard navigation (↑/↓/Enter/Esc)
- Integration with SlashCommands
- Uses HTMX search API for consistency
- Inserts Markdown links into editor
**Note**: `/link` command inserts standard Markdown template `[texte](url)` for external links
**ui.js** **ui.js**
- Sidebar toggle (mobile/desktop) - Sidebar toggle (mobile/desktop)
- Simple utility functions - Simple utility functions

262
CLAUDE.md
View File

@ -6,16 +6,29 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
A lightweight web-based Markdown note-taking application with a Go backend and modern JavaScript frontend. Notes are stored as plain Markdown files with YAML front matter containing metadata (title, date, last_modified, tags). The system provides a sophisticated CodeMirror 6 editor with live preview, rich search capabilities, hierarchical organization, and automatic front matter management. A lightweight web-based Markdown note-taking application with a Go backend and modern JavaScript frontend. Notes are stored as plain Markdown files with YAML front matter containing metadata (title, date, last_modified, tags). The system provides a sophisticated CodeMirror 6 editor with live preview, rich search capabilities, hierarchical organization, and automatic front matter management.
**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
- **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
**Recent Modernization**: The project has been migrated from a simple textarea editor to CodeMirror 6, with a Vite build system for frontend modules. The backend remains unchanged, maintaining the same Go architecture with htmx for dynamic interactions. **Recent Modernization**: The project has been migrated from a simple textarea editor to CodeMirror 6, with a Vite build system for frontend modules. The backend remains unchanged, maintaining the same Go architecture with htmx for dynamic interactions.
## Architecture ## Architecture
### Backend (Go) ### Backend (Go)
Three main packages under `internal/`: Four main packages under `internal/`:
- **indexer**: Maintains an in-memory index mapping tags to note files. Parses YAML front matter from `.md` files to build the index. Thread-safe with RWMutex. - **indexer**: Maintains an in-memory index mapping tags to note files. Parses YAML front matter from `.md` files to build the index. Thread-safe with RWMutex.
- **watcher**: Uses `fsnotify` to monitor filesystem changes and trigger re-indexing with 200ms debounce. Recursively watches all subdirectories. - **watcher**: Uses `fsnotify` to monitor filesystem changes and trigger re-indexing with 200ms debounce. Recursively watches all subdirectories.
- **api**: HTTP handlers that serve templates and handle CRUD operations on notes. Updates front matter automatically on save. - **api**: HTTP handlers that serve templates and handle CRUD operations on notes. Updates front matter automatically on save.
- `handler.go` - Main HTML endpoints for the web interface
- `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)
The server (`cmd/server/main.go`) coordinates these components: The server (`cmd/server/main.go`) coordinates these components:
1. Loads initial index from notes directory 1. Loads initial index from notes directory
@ -30,6 +43,8 @@ The server (`cmd/server/main.go`) coordinates these components:
- `/api/folders/create` (Folder management) - `/api/folders/create` (Folder management)
- `/api/files/move` (File/folder moving) - `/api/files/move` (File/folder moving)
- `/api/home` (Home page) - `/api/home` (Home page)
- `/api/daily-notes/*` (Daily note creation and calendar)
- `/api/favorites/*` (Favorites management)
5. Handles static files from `static/` directory 5. Handles static files from `static/` directory
### Frontend ### Frontend
@ -48,20 +63,31 @@ The frontend uses a modern build system with Vite and CodeMirror 6:
frontend/src/ frontend/src/
├── main.js # Entry point - imports all modules ├── main.js # Entry point - imports all modules
├── editor.js # CodeMirror 6 editor implementation with slash commands ├── editor.js # CodeMirror 6 editor implementation with slash commands
├── vim-mode-manager.js # Vim mode integration for CodeMirror
├── search.js # Search modal with Ctrl/Cmd+K keyboard shortcut ├── search.js # Search modal with Ctrl/Cmd+K keyboard shortcut
├── link-inserter.js # Note linking modal for /link command
├── file-tree.js # Drag-and-drop file organization ├── file-tree.js # Drag-and-drop file organization
├── favorites.js # Favorites system (star/unstar functionality)
├── daily-notes.js # Daily notes creation and calendar widget
├── keyboard-shortcuts.js # Global keyboard shortcuts management
├── theme-manager.js # Theme switching and persistence
├── font-manager.js # Font selection and size management
└── ui.js # Sidebar toggle functionality └── ui.js # Sidebar toggle functionality
``` ```
#### CodeMirror 6 Editor Features #### CodeMirror 6 Editor Features
- **Syntax Highlighting**: Full Markdown language support (`@codemirror/lang-markdown`) - **Syntax Highlighting**: Full Markdown language support (`@codemirror/lang-markdown`)
- **Theme**: One Dark theme (`@codemirror/theme-one-dark`) - VS Code-inspired dark theme - **Theme**: One Dark theme (`@codemirror/theme-one-dark`) - VS Code-inspired dark theme
- **Vim Mode**: Optional full Vim keybindings (`@replit/codemirror-vim`) with hjkl navigation, modes, and commands
- **Live Preview**: Debounced updates (150ms) synchronized with editor scroll position - **Live Preview**: Debounced updates (150ms) synchronized with editor scroll position
- **Auto-Save**: Triggers after 2 seconds of inactivity - **Auto-Save**: Triggers after 2 seconds of inactivity
- **Keyboard Shortcuts**: - **Keyboard Shortcuts**:
- `Ctrl/Cmd+S` for manual save - `Ctrl/Cmd+S` for manual save
- `Ctrl/Cmd+D` for daily notes
- `Ctrl/Cmd+K` for search
- `Ctrl/Cmd+B` for sidebar toggle
- `Tab` for proper indentation - `Tab` for proper indentation
- Full keyboard navigation - Full keyboard navigation (see docs/KEYBOARD_SHORTCUTS.md)
- **View Modes**: Toggle between split view, editor-only, and preview-only - **View Modes**: Toggle between split view, editor-only, and preview-only
- **Slash Commands**: Type `/` to open command palette for quick Markdown insertion - **Slash Commands**: Type `/` to open command palette for quick Markdown insertion
- **Front Matter Handling**: Automatically strips YAML front matter in preview - **Front Matter Handling**: Automatically strips YAML front matter in preview
@ -71,12 +97,21 @@ frontend/src/
- **Drag & Drop**: Move files between folders with visual feedback - **Drag & Drop**: Move files between folders with visual feedback
- **Folder Creation**: Modal-based creation supporting nested paths - **Folder Creation**: Modal-based creation supporting nested paths
- **Safe Validation**: Prevents dangerous path operations - **Safe Validation**: Prevents dangerous path operations
- **Favorites**: Star notes and folders for quick access (★ icon in sidebar)
#### Rendering Pipeline #### Rendering Pipeline
- **marked.js**: Markdown to HTML conversion - **marked.js**: Markdown to HTML conversion
- **DOMPurify**: HTML sanitization to prevent XSS attacks - **DOMPurify**: HTML sanitization to prevent XSS attacks
- **Highlight.js**: Syntax highlighting for code blocks in preview - **Highlight.js**: Syntax highlighting for code blocks in preview
- **Custom Theme**: Material Darker theme in `static/theme.css` with CSS custom properties - **Custom Themes**: 8 dark themes in `static/theme.css` with CSS custom properties
- Material Dark (default)
- Monokai
- Dracula
- One Dark
- Solarized Dark
- Nord
- Catppuccin
- Everforest
### HTMX + JavaScript Coordination (Optimized Architecture) ### HTMX + JavaScript Coordination (Optimized Architecture)
@ -227,6 +262,33 @@ htmx.ajax('POST', '/api/files/move', {
- Use MutationObserver when HTMX events are available - Use MutationObserver when HTMX events are available
- Mix fetch() and htmx.ajax() for similar operations - Mix fetch() and htmx.ajax() for similar operations
### Daily Notes
**Implementation**: `internal/api/daily_notes.go` and `frontend/src/daily-notes.js`
Daily notes provide a quick journaling feature:
- **Keyboard Shortcut**: `Ctrl/Cmd+D` creates or opens today's note
- **Calendar Widget**: Interactive monthly calendar showing all daily notes
- **Template System**: Uses `daily-note-template.md` if present in notes directory
- **Auto-naming**: Creates notes as `daily/YYYY-MM-DD.md` by default
- **Visual Indicators**: Calendar highlights days with existing notes
- **One-click Access**: Click any calendar date to open or create that day's note
The calendar is implemented using htmx for dynamic month navigation and rendering.
### Favorites System
**Implementation**: `internal/api/favorites.go` and `frontend/src/favorites.js`
The favorites system allows quick access to frequently used notes and folders:
- **Star Icon**: Click ★ next to any note or folder in the file tree
- **Persistence**: Favorites stored in `.favorites.json` in the notes directory
- **Quick Access**: Starred items appear at the top of the sidebar
- **Folder Support**: Star entire folders to quickly access project areas
- **Visual Feedback**: Filled star (★) for favorites, empty star (☆) for non-favorites
Favorites are loaded on server startup and updated in real-time via htmx.
### Note Format ### Note Format
Notes have YAML front matter with these fields: Notes have YAML front matter with these fields:
@ -258,12 +320,13 @@ Output files (loaded by templates):
- `static/dist/project-notes-frontend.umd.js` (UMD format) - `static/dist/project-notes-frontend.umd.js` (UMD format)
Frontend dependencies (from `frontend/package.json`): Frontend dependencies (from `frontend/package.json`):
- `@codemirror/basic-setup` - Base editor functionality - `@codemirror/basic-setup` (^0.20.0) - Base editor functionality
- `@codemirror/lang-markdown` - Markdown language support - `@codemirror/lang-markdown` (^6.5.0) - Markdown language support
- `@codemirror/state` - Editor state management - `@codemirror/state` (^6.5.2) - Editor state management
- `@codemirror/view` - Editor view layer - `@codemirror/view` (^6.38.6) - Editor view layer
- `@codemirror/theme-one-dark` - Dark theme - `@codemirror/theme-one-dark` (^6.1.3) - Dark theme
- `vite` - Build tool - `@replit/codemirror-vim` (^6.2.2) - Vim mode integration
- `vite` (^7.2.2) - Build tool
### Running the Server ### Running the Server
@ -415,6 +478,78 @@ A modern command-palette style search modal is available:
**Styling**: Custom styles in `static/theme.css` with Material Darker theme integration. **Styling**: Custom styles in `static/theme.css` with Material Darker theme integration.
### Theme and Font Customization
**Implementation**: `frontend/src/theme-manager.js` and `frontend/src/font-manager.js`
The application supports extensive UI customization:
#### Themes
8 dark themes available via Settings (⚙️ icon):
- **Material Dark** (default) - Material Design inspired
- **Monokai** - Classic Monokai colors
- **Dracula** - Popular purple-tinted theme
- **One Dark** - Atom/VS Code inspired
- **Solarized Dark** - Precision colors by Ethan Schoonover
- **Nord** - Arctic, north-bluish color palette
- **Catppuccin** - Soothing pastel theme
- **Everforest** - Comfortable greenish theme
Themes are applied via CSS custom properties and persist in localStorage.
#### Fonts
8 font options with 4 size presets (small, medium, large, extra-large):
- JetBrains Mono (default)
- Fira Code
- Inter
- IBM Plex Mono
- Source Code Pro
- Cascadia Code
- Roboto Mono
- Ubuntu Mono
Font settings apply to both the editor and preview pane.
### Vim Mode
**Implementation**: `frontend/src/vim-mode-manager.js` using `@replit/codemirror-vim`
Optional Vim keybindings for power users:
- **Enable/Disable**: Toggle via Settings (⚙️ icon)
- **Full Vim Support**: hjkl navigation, visual mode, operators, text objects
- **Mode Indicator**: Shows current Vim mode (Normal/Insert/Visual) in editor
- **Persistence**: Vim mode preference saved to localStorage
- **CodeMirror Integration**: Native Vim extension with excellent compatibility
Vim mode users get full modal editing while maintaining CodeMirror features like syntax highlighting and auto-save.
### Keyboard Shortcuts
**Implementation**: `frontend/src/keyboard-shortcuts.js`
The application provides 10+ global keyboard shortcuts for efficient navigation:
**Essential Shortcuts**:
- `Ctrl/Cmd+D` - Create or open today's daily note
- `Ctrl/Cmd+K` - Open search modal
- `Ctrl/Cmd+S` - Save current note (also triggers auto-save)
- `Ctrl/Cmd+B` - Toggle sidebar visibility
- `Ctrl/Cmd+/` - Show keyboard shortcuts help modal
**Editor Shortcuts**:
- `Tab` - Indent (when in editor)
- `Shift+Tab` - Outdent (when in editor)
- `Ctrl/Cmd+Enter` - Save note (alternative to Cmd+S)
**Navigation**:
- `↑`/`↓` - Navigate search results or command palette
- `Enter` - Select/confirm action
- `Esc` - Close modals, cancel actions, clear search
All shortcuts are non-blocking and work across the application. The shortcuts help modal (triggered by `Ctrl/Cmd+/`) provides a quick reference guide.
For complete documentation, see `docs/KEYBOARD_SHORTCUTS.md`.
### Security Considerations ### Security Considerations
File path validation in `handler.go` and `rest_handler.go`: File path validation in `handler.go` and `rest_handler.go`:
@ -509,19 +644,64 @@ Provides command palette for quick Markdown insertion:
The editor includes a slash command system integrated with CodeMirror 6: The editor includes a slash command system integrated with CodeMirror 6:
- Type `/` at the start of a line to trigger the command palette - Type `/` at the start of a line to trigger the command palette
- Available commands (13 total): - Available commands (14 total):
- **Headings**: h1, h2, h3 - Insert Markdown headers - **Headings**: h1, h2, h3 - Insert Markdown headers
- **Formatting**: bold, italic, code - Text formatting - **Formatting**: bold, italic, code - Text formatting
- **Blocks**: codeblock, quote, hr, table - Block-level elements - **Blocks**: codeblock, quote, hr, table - Block-level elements
- **Lists**: list - Unordered list - **Lists**: list - Unordered list
- **Dynamic**: date - Insert current date in French format (DD/MM/YYYY) - **Dynamic**: date - Insert current date in French format (DD/MM/YYYY)
- **Links**: link - Insert link template `[text](url)` - **Links**:
- `link` - Insert standard Markdown link `[texte](url)`
- `ilink` - Open internal note linking modal (see Note Linking below)
- Navigate with Arrow Up/Down, select with Enter/Tab, cancel with Escape - Navigate with Arrow Up/Down, select with Enter/Tab, cancel with Escape
- Commands are filtered in real-time as you type after the `/` - Commands are filtered in real-time as you type after the `/`
- The palette is positioned dynamically near the cursor using CodeMirror coordinates - The palette is positioned dynamically near the cursor using CodeMirror coordinates
- Implementation in `frontend/src/editor.js` with the `SlashCommands` class - Implementation in `frontend/src/editor.js` with the `SlashCommands` class
- Styled command palette with gradient selection indicator - Styled command palette with gradient selection indicator
### Note Linking
**Implementation**: `frontend/src/link-inserter.js`
The note linking system allows users to create Markdown links between notes without leaving the editor:
**Activation**: Type `/ilink` (internal link) in the editor and select the command from the slash palette
**Features**:
- **Fuzzy Search**: Real-time search across all notes with 200ms debounce
- **Keyboard Navigation**: Navigate with ↑/↓, select with Enter, cancel with Esc
- **Search Integration**: Reuses existing `/api/search` endpoint (no new backend code)
- **Rich Results**: Shows note title, path, tags, and metadata
- **Instant Insertion**: Inserts `[Note Title](path/to/note.md)` format at cursor position
**Architecture**:
- `LinkInserter` class manages the search modal and selection
- Opens via `SlashCommands.openLinkInserter()` when `/ilink` is triggered
- Uses HTMX search API for consistency
- Modal styled to match `SearchModal` design language
**Workflow**:
1. User types `/ilink` → slash palette appears
2. User selects "ilink" → modal opens with search input
3. User types search query → fuzzy search filters notes
4. User selects note (Enter/click) → Markdown link inserted
5. Modal closes → editor regains focus at end of inserted link
**Standard Links**: For external URLs, use `/link` to insert the standard Markdown template `[texte](url)`
**Link Format**: Links are inserted as HTML with HTMX attributes:
```html
<a href="#" hx-get="/api/notes/path/to/note.md" hx-target="#editor-container" hx-swap="innerHTML">Note Title</a>
```
This format:
- **Clickable in preview**: Links open the note directly in the editor when clicked
- **HTMX-powered**: Uses existing HTMX infrastructure (no new backend code)
- **Inline HTML**: Marked.js renders the HTML as-is, DOMPurify sanitizes it, HTMX processes it
- **Editable**: Plain HTML in the source, can be manually edited if needed
**Note**: This format is specific to this application. For compatibility with other Markdown tools, use standard Markdown links with `/link` command.
## Frontend Libraries ## Frontend Libraries
The application uses a mix of npm packages (for the editor) and CDN-loaded libraries (for utilities): The application uses a mix of npm packages (for the editor) and CDN-loaded libraries (for utilities):
@ -533,7 +713,8 @@ Managed in `frontend/package.json`:
- **@codemirror/state (^6.5.2)**: Editor state management - **@codemirror/state (^6.5.2)**: Editor state management
- **@codemirror/view (^6.38.6)**: Editor view layer and rendering - **@codemirror/view (^6.38.6)**: Editor view layer and rendering
- **@codemirror/theme-one-dark (^6.1.3)**: Dark theme for CodeMirror - **@codemirror/theme-one-dark (^6.1.3)**: Dark theme for CodeMirror
- **vite (^5.0.0)**: Build tool for bundling frontend modules - **@replit/codemirror-vim (^6.2.2)**: Vim mode integration for CodeMirror
- **vite (^7.2.2)**: Build tool for bundling frontend modules
### CDN Libraries ### CDN Libraries
Loaded in `templates/index.html`: Loaded in `templates/index.html`:
@ -543,14 +724,16 @@ Loaded in `templates/index.html`:
- **Highlight.js (11.9.0)**: Syntax highlighting for code blocks in preview with Atom One Dark theme - **Highlight.js (11.9.0)**: Syntax highlighting for code blocks in preview with Atom One Dark theme
### Styling ### Styling
- **Material Darker Theme**: Custom dark theme in `static/theme.css` - **8 Dark Themes**: Switchable themes in `static/theme.css`
- Material Dark, Monokai, Dracula, One Dark, Solarized, Nord, Catppuccin, Everforest
- **Color System**: CSS custom properties for consistent theming - **Color System**: CSS custom properties for consistent theming
- Background colors: `--bg-primary`, `--bg-secondary`, `--bg-tertiary`, `--bg-elevated` - Background colors: `--bg-primary`, `--bg-secondary`, `--bg-tertiary`, `--bg-elevated`
- Text colors: `--text-primary`, `--text-secondary`, `--text-muted` - Text colors: `--text-primary`, `--text-secondary`, `--text-muted`
- Accent colors: `--accent-blue`, `--accent-violet` - Accent colors: `--accent-blue`, `--accent-violet`
- **Font Customization**: 8 font families with 4 size presets
- **No CSS Framework**: All styles hand-crafted with CSS Grid and Flexbox - **No CSS Framework**: All styles hand-crafted with CSS Grid and Flexbox
- **Responsive Design**: Adaptive layout for different screen sizes - **Responsive Design**: Adaptive layout for different screen sizes
- **Custom Scrollbars**: Styled scrollbars matching the dark theme - **Custom Scrollbars**: Styled scrollbars matching the current theme
### Build Output ### Build Output
The Vite build process produces: The Vite build process produces:
@ -566,17 +749,28 @@ project-notes/
│ └── main.go # Server entry point │ └── main.go # Server entry point
├── internal/ ├── internal/
│ ├── api/ │ ├── api/
│ │ ── handler.go # HTTP handlers for CRUD operations │ │ ── handler.go # HTTP handlers for CRUD operations
│ │ ├── rest_handler.go # REST API v1 endpoints
│ │ ├── daily_notes.go # Daily notes functionality
│ │ └── favorites.go # Favorites management
│ ├── indexer/ │ ├── indexer/
│ │ ── indexer.go # Note indexing and search │ │ ── indexer.go # Note indexing and search
│ │ └── indexer_test.go # Indexer tests
│ └── watcher/ │ └── watcher/
│ └── watcher.go # Filesystem watcher with fsnotify │ └── watcher.go # Filesystem watcher with fsnotify
├── frontend/ # Frontend build system (NEW) ├── frontend/ # Frontend build system
│ ├── src/ │ ├── src/
│ │ ├── main.js # Entry point │ │ ├── main.js # Entry point - imports all modules
│ │ ├── editor.js # CodeMirror 6 implementation (26 KB) │ │ ├── editor.js # CodeMirror 6 editor with slash commands
│ │ ├── file-tree.js # Drag-and-drop file management (11 KB) │ │ ├── vim-mode-manager.js # Vim mode integration
│ │ ── ui.js # Sidebar toggle (720 B) │ │ ── search.js # Search modal (Ctrl/Cmd+K)
│ │ ├── file-tree.js # Drag-and-drop file tree
│ │ ├── favorites.js # Favorites system
│ │ ├── daily-notes.js # Daily notes and calendar widget
│ │ ├── keyboard-shortcuts.js # Global keyboard shortcuts
│ │ ├── theme-manager.js # Theme switching
│ │ ├── font-manager.js # Font customization
│ │ └── ui.js # Sidebar toggle
│ ├── package.json # NPM dependencies │ ├── package.json # NPM dependencies
│ ├── package-lock.json │ ├── package-lock.json
│ └── vite.config.js # Vite build configuration │ └── vite.config.js # Vite build configuration
@ -592,9 +786,18 @@ project-notes/
│ ├── search-results.html # Search results │ ├── search-results.html # Search results
│ └── new-note-prompt.html # New note modal │ └── new-note-prompt.html # New note modal
├── notes/ # Note storage directory ├── notes/ # Note storage directory
── *.md # Markdown files with YAML front matter ── *.md # Markdown files with YAML front matter
│ ├── daily/ # Daily notes (YYYY-MM-DD.md)
│ ├── .favorites.json # Favorites list (auto-generated)
│ └── daily-note-template.md # Optional daily note template
├── docs/ # Documentation
│ ├── KEYBOARD_SHORTCUTS.md # Keyboard shortcuts reference
│ ├── DAILY_NOTES.md # Daily notes guide
│ ├── USAGE_GUIDE.md # Complete usage guide
│ └── FREEBSD_BUILD.md # FreeBSD build guide
├── go.mod # Go dependencies ├── go.mod # Go dependencies
├── go.sum ├── go.sum
├── API.md # REST API documentation
└── CLAUDE.md # This file └── CLAUDE.md # This file
``` ```
@ -602,15 +805,26 @@ project-notes/
**Backend Development**: **Backend Development**:
- `cmd/server/main.go` - Server initialization and routing - `cmd/server/main.go` - Server initialization and routing
- `internal/api/handler.go` - API endpoints and request handling - `internal/api/handler.go` - Main HTML endpoints and request handling
- `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/indexer/indexer.go` - Search and indexing logic - `internal/indexer/indexer.go` - Search and indexing logic
- `internal/watcher/watcher.go` - Filesystem monitoring - `internal/watcher/watcher.go` - Filesystem monitoring
**Frontend Development**: **Frontend Development**:
- `frontend/src/editor.js` - CodeMirror editor, preview, slash commands - `frontend/src/editor.js` - CodeMirror editor, preview, slash commands
- `frontend/src/vim-mode-manager.js` - Vim mode integration
- `frontend/src/search.js` - Search modal functionality
- `frontend/src/link-inserter.js` - Note linking modal for `/link` command
- `frontend/src/file-tree.js` - File tree interactions and drag-and-drop - `frontend/src/file-tree.js` - File tree interactions and drag-and-drop
- `frontend/src/favorites.js` - Favorites system
- `frontend/src/daily-notes.js` - Daily notes creation and calendar widget
- `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/ui.js` - UI utilities (sidebar toggle) - `frontend/src/ui.js` - UI utilities (sidebar toggle)
- `static/theme.css` - Styling and theming - `static/theme.css` - Styling and theming (8 themes)
- `templates/*.html` - HTML templates (Go template syntax) - `templates/*.html` - HTML templates (Go template syntax)
**Configuration**: **Configuration**:

View File

@ -3,17 +3,17 @@
A lightweight, web-based Markdown note-taking application with a Go backend and a minimalist frontend built with htmx. A lightweight, web-based Markdown note-taking application with a Go backend and a minimalist frontend built with htmx.
- 🚫 No database - 🚫 No database
- 📝 Flat files : Markdown with frontmatters - 📝 Flat files: Markdown with front matter
- 🔒 Your notes, your application, your server, your data - 🔒 Your notes, your application, your server, your data
- ⌨️ Vim Mode - ⌨️ Vim Mode
- 🎹 Keyboard driven with shortcut and "/" command - 🎹 Keyboard driven with shortcuts and "/" commands
- 🔍 Powerful Search - 🔍 Powerful Search
- 🌍 Run everywhere (Linux & FreeBSD) - 🌍 Run everywhere (Linux & FreeBSD)
- 📱 Responsive with laptop and smartphone - 📱 Responsive on laptop and smartphone
- 🛠️ Super Easy to build - 🛠️ Super Easy to build
- 🚀 A powerful API - 🚀 Powerful REST API
![alt text](image.png) ![Project Notes Interface](image.png)
## Features ## Features
@ -39,11 +39,11 @@ A lightweight, web-based Markdown note-taking application with a Go backend and
* **Lightweight Frontend:** Built with htmx for dynamic interactions, minimizing JavaScript complexity. * **Lightweight Frontend:** Built with htmx for dynamic interactions, minimizing JavaScript complexity.
* **Go Backend:** Fast and efficient Go server handles file operations, indexing, and serving the frontend. * **Go Backend:** Fast and efficient Go server handles file operations, indexing, and serving the frontend.
## Road Map ## Roadmap
- Share notes in markdown / pdf - Share notes as Markdown/PDF exports
- Publics notes. - Public notes
- Secure by user/password (You can use Authelia/Authentik for now) - User authentication (use Authelia/Authentik for now)
## Getting Started ## Getting Started
@ -158,7 +158,7 @@ go run ./cmd/server -addr :3000 -notes-dir ~/my-notes
1. **Create your first note**: Press `Ctrl/Cmd+D` to open today's daily note 1. **Create your first note**: Press `Ctrl/Cmd+D` to open today's daily note
2. **Start writing**: Use the Markdown editor with live preview 2. **Start writing**: Use the Markdown editor with live preview
3. **Save**: Press `Ctrl/Cmd+S` or click "Enregistrer" 3. **Save**: Press `Ctrl/Cmd+S` or click "Save"
4. **Search**: Press `Ctrl/Cmd+K` to find any note instantly 4. **Search**: Press `Ctrl/Cmd+K` to find any note instantly
5. **Customize**: Click ⚙️ to choose themes, fonts, and enable Vim mode 5. **Customize**: Click ⚙️ to choose themes, fonts, and enable Vim mode

127
docs/SIDEBAR_RESIZE_TEST.md Normal file
View File

@ -0,0 +1,127 @@
# Test de la Sidebar Redimensionnable
## Étapes pour tester
### 1. Redémarrer le serveur
```bash
# Arrêter le serveur actuel (Ctrl+C)
# Puis relancer :
cd /home/mathieu/git/project-notes
go run ./cmd/server
```
### 2. Ouvrir l'application
```
http://localhost:8080
```
### 3. Tester le redimensionnement
1. **Ouvrez la console développeur** (F12) pour voir les logs
2. Vous devriez voir : `Sidebar resize initialized`
3. **Survolez le bord droit de la sidebar** (zone de 4px)
- Le curseur devrait devenir `↔` (resize cursor)
- Une fine ligne bleue devrait apparaître
4. **Cliquez et glissez** vers la droite ou la gauche
5. **Relâchez** pour sauvegarder la largeur
6. **Rechargez la page** (F5) - la largeur devrait être restaurée
### 4. Tests de limites
- **Minimum** : Essayez de réduire en dessous de 200px (bloqué)
- **Maximum** : Essayez d'agrandir au-delà de 600px (bloqué)
### 5. Reset (si nécessaire)
Dans la console développeur :
```javascript
resetSidebarWidth()
```
## Vérifications si ça ne fonctionne pas
### 1. Le script se charge-t-il ?
Dans la console développeur (F12), onglet Network :
- Cherchez `sidebar-resize.js`
- Status devrait être `200 OK`
- Si `404`, le serveur ne sert pas le fichier
### 2. Y a-t-il des erreurs JavaScript ?
Dans la console développeur (F12), onglet Console :
- Cherchez des erreurs en rouge
- Vous devriez voir : `Sidebar resize initialized`
- Si vous voyez `Sidebar not found`, le sélecteur `#sidebar` ne trouve pas l'élément
### 3. La poignée est-elle créée ?
Dans la console développeur (F12), onglet Elements/Inspecteur :
- Sélectionnez `<aside id="sidebar">`
- À l'intérieur, en bas, il devrait y avoir : `<div class="sidebar-resize-handle" title="Drag to resize sidebar"></div>`
### 4. Les styles CSS sont-ils appliqués ?
Dans la console développeur, inspectez `.sidebar-resize-handle` :
- `width: 4px`
- `cursor: ew-resize`
- `position: absolute`
## Débogage avancé
### Test manuel du script
Dans la console développeur :
```javascript
// Vérifier que la sidebar existe
document.querySelector('#sidebar')
// Vérifier que la poignée existe
document.querySelector('.sidebar-resize-handle')
// Tester le redimensionnement manuel
const sidebar = document.querySelector('#sidebar');
sidebar.style.width = '400px';
document.querySelector('main').style.marginLeft = '400px';
```
### Test de sauvegarde localStorage
```javascript
// Sauvegarder une largeur
localStorage.setItem('sidebar-width', '400');
// Lire la largeur sauvegardée
localStorage.getItem('sidebar-width');
// Effacer
localStorage.removeItem('sidebar-width');
```
## Problèmes connus et solutions
### La poignée n'apparaît pas
- **Cause** : Styles CSS non chargés
- **Solution** : Vider le cache (Ctrl+Shift+R) et recharger
### Le curseur ne change pas
- **Cause** : Z-index trop bas
- **Solution** : Vérifier que `.sidebar-resize-handle` a `z-index: 11`
### Le resize ne fonctionne pas sur mobile
- **Normal** : Désactivé volontairement sur mobile (< 768px)
- La sidebar est fixe à 280px sur mobile
### La sidebar saute au resize
- **Cause** : Transition CSS qui interfère
- **Solution** : Désactiver temporairement `transition` pendant le resize (déjà fait dans le CSS)
## Support
Si ça ne fonctionne toujours pas après ces tests, fournir :
1. Screenshot de la console (onglet Console)
2. Screenshot de la console (onglet Network, fichier sidebar-resize.js)
3. Version du navigateur
4. Si mobile ou desktop

View File

@ -5,6 +5,7 @@ import { markdown } from '@codemirror/lang-markdown';
import { oneDark } from '@codemirror/theme-one-dark'; import { oneDark } from '@codemirror/theme-one-dark';
import { keymap } from '@codemirror/view'; import { keymap } from '@codemirror/view';
import { indentWithTab } from '@codemirror/commands'; import { indentWithTab } from '@codemirror/commands';
import { LinkInserter } from './link-inserter.js';
// Import du mode Vim // Import du mode Vim
let vimExtension = null; let vimExtension = null;
@ -254,6 +255,9 @@ class MarkdownEditor {
htmx.process(this.preview); htmx.process(this.preview);
} }
// Intercepter les clics sur les liens internes (avec hx-get)
this.setupInternalLinkHandlers();
if (typeof hljs !== 'undefined') { if (typeof hljs !== 'undefined') {
this.preview.querySelectorAll('pre code').forEach(block => { this.preview.querySelectorAll('pre code').forEach(block => {
hljs.highlightElement(block); hljs.highlightElement(block);
@ -264,6 +268,41 @@ class MarkdownEditor {
} }
} }
setupInternalLinkHandlers() {
// Trouver tous les liens avec hx-get (liens internes)
const internalLinks = this.preview.querySelectorAll('a[hx-get]');
internalLinks.forEach(link => {
// Retirer les anciens listeners pour éviter les doublons
link.replaceWith(link.cloneNode(true));
});
// Ré-sélectionner après clonage
const freshLinks = this.preview.querySelectorAll('a[hx-get]');
freshLinks.forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const target = link.getAttribute('hx-get');
const targetElement = link.getAttribute('hx-target') || '#editor-container';
const swapMethod = link.getAttribute('hx-swap') || 'innerHTML';
console.log('[InternalLink] Clicked:', target);
if (target && typeof htmx !== 'undefined') {
htmx.ajax('GET', target, {
target: targetElement,
swap: swapMethod
});
}
});
});
console.log('[Preview] Setup', freshLinks.length, 'internal link handlers');
}
syncToTextarea() { syncToTextarea() {
if (this.editorView && this.textarea) { if (this.editorView && this.textarea) {
this.textarea.value = this.editorView.state.doc.toString(); this.textarea.value = this.editorView.state.doc.toString();
@ -332,6 +371,7 @@ class SlashCommands {
{ name: 'list', snippet: '- ' }, { name: 'list', snippet: '- ' },
{ name: 'date', snippet: () => new Date().toLocaleDateString('fr-FR') }, { name: 'date', snippet: () => new Date().toLocaleDateString('fr-FR') },
{ name: 'link', snippet: '[texte](url)' }, { name: 'link', snippet: '[texte](url)' },
{ name: 'ilink', isModal: true, handler: () => this.openLinkInserter() },
{ name: 'bold', snippet: '**texte**' }, { name: 'bold', snippet: '**texte**' },
{ name: 'italic', snippet: '*texte*' }, { name: 'italic', snippet: '*texte*' },
{ name: 'code', snippet: '`code`' }, { name: 'code', snippet: '`code`' },
@ -612,6 +652,15 @@ class SlashCommands {
return; return;
} }
// Commande spéciale avec modal (comme /ilink)
if (command.isModal && command.handler) {
console.log('Executing modal command:', command.name);
// NE PAS cacher la palette tout de suite car le handler a besoin de slashPos
// La palette sera cachée par le handler lui-même
command.handler();
return;
}
let snippet = command.snippet; let snippet = command.snippet;
if (typeof snippet === 'function') { if (typeof snippet === 'function') {
snippet = snippet(); snippet = snippet();
@ -632,6 +681,59 @@ class SlashCommands {
this.hidePalette(); this.hidePalette();
} }
openLinkInserter() {
// Sauvegarder la position du slash IMMÉDIATEMENT avant toute autre opération
const savedSlashPos = this.slashPos;
console.log('[SlashCommands] openLinkInserter - savedSlashPos:', savedSlashPos);
if (!savedSlashPos) {
console.error('[SlashCommands] No slash position available!');
this.hidePalette();
return;
}
// Maintenant on peut cacher la palette en toute sécurité
this.hidePalette();
// S'assurer que le LinkInserter global existe, le créer si nécessaire
if (!window.linkInserter) {
console.log('Initializing LinkInserter...');
window.linkInserter = new LinkInserter();
}
// Ouvrir le modal de sélection de lien
window.linkInserter.open({
editorView: this.editorView,
onSelect: ({ title, path }) => {
console.log('[SlashCommands] onSelect callback received:', { title, path });
console.log('[SlashCommands] savedSlashPos:', savedSlashPos);
// Créer un lien HTMX cliquable dans le preview
// Format : <a href="#" onclick="return false;" hx-get="/api/notes/path" hx-target="#editor-container" hx-swap="innerHTML">Title</a>
// Le onclick="return false;" empêche le comportement par défaut du # qui pourrait rediriger
const linkHtml = `<a href="#" onclick="return false;" hx-get="/api/notes/${path}" hx-target="#editor-container" hx-swap="innerHTML">${title}</a>`;
console.log('[SlashCommands] Inserting:', linkHtml);
const { state, dispatch } = this.editorView;
const { from } = state.selection.main;
// Remplacer depuis le "/" jusqu'au curseur actuel
const replaceFrom = savedSlashPos.absolutePos;
console.log('[SlashCommands] Replacing from', replaceFrom, 'to', from);
dispatch(state.update({
changes: { from: replaceFrom, to: from, insert: linkHtml },
selection: { anchor: replaceFrom + linkHtml.length }
}));
this.editorView.focus();
console.log('[SlashCommands] Link inserted successfully');
}
});
}
destroy() { destroy() {
// Retirer tous les listeners d'événements // Retirer tous les listeners d'événements
if (this.editorView) { if (this.editorView) {

View File

@ -0,0 +1,398 @@
/**
* LinkInserter - Modal de recherche pour insérer des liens vers d'autres notes
* Intégré dans l'éditeur CodeMirror 6
*/
class LinkInserter {
constructor() {
this.modal = null;
this.input = null;
this.resultsContainer = null;
this.isOpen = false;
this.searchTimeout = null;
this.selectedIndex = 0;
this.results = [];
this.callback = null; // Fonction appelée quand un lien est sélectionné
this.editorView = null; // Référence à l'instance CodeMirror
this.init();
}
init() {
this.createModal();
}
createModal() {
// Créer la modale (plus compacte que SearchModal)
this.modal = document.createElement('div');
this.modal.id = 'link-inserter-modal';
this.modal.className = 'link-inserter-modal';
this.modal.style.display = 'none';
this.modal.innerHTML = `
<div class="link-inserter-overlay"></div>
<div class="link-inserter-container">
<div class="link-inserter-header">
<div class="link-inserter-input-wrapper">
<svg class="link-inserter-icon" 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">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
<input
type="text"
class="link-inserter-input"
placeholder="Rechercher une note à lier..."
autocomplete="off"
spellcheck="false"
/>
<kbd class="link-inserter-kbd">ESC</kbd>
</div>
</div>
<div class="link-inserter-body">
<div class="link-inserter-results">
<div class="link-inserter-help">
<div class="link-inserter-help-text">
🔗 Tapez pour rechercher une note
</div>
</div>
</div>
</div>
<div class="link-inserter-footer">
<div class="link-inserter-footer-hint">
<kbd>↑</kbd><kbd>↓</kbd> Navigation
<kbd>↵</kbd> Insérer
<kbd>ESC</kbd> Annuler
</div>
</div>
</div>
`;
document.body.appendChild(this.modal);
// Références aux éléments
this.input = this.modal.querySelector('.link-inserter-input');
this.resultsContainer = this.modal.querySelector('.link-inserter-results');
// Event listeners
this.modal.querySelector('.link-inserter-overlay').addEventListener('click', () => {
this.close();
});
this.input.addEventListener('input', (e) => {
this.handleSearch(e.target.value);
});
this.input.addEventListener('keydown', (e) => {
this.handleKeyNavigation(e);
});
}
handleSearch(query) {
// Debounce de 200ms (plus rapide que SearchModal)
clearTimeout(this.searchTimeout);
if (!query.trim()) {
this.showHelp();
return;
}
this.showLoading();
this.searchTimeout = setTimeout(async () => {
try {
// Utiliser l'API de recherche existante
const response = await fetch(`/api/search?query=${encodeURIComponent(query)}`);
const html = await response.text();
// Parser le HTML pour extraire les résultats
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
// Extraire les liens de résultats
const resultLinks = doc.querySelectorAll('.search-result-link');
this.results = Array.from(resultLinks).map((link, index) => {
const title = link.querySelector('.search-result-title')?.textContent || 'Sans titre';
const path = link.getAttribute('hx-get')?.replace('/api/notes/', '') || '';
const tags = Array.from(link.querySelectorAll('.tag-pill')).map(t => t.textContent);
const pathDisplay = link.querySelector('.search-result-path')?.textContent || '';
return {
index,
title: title.trim(),
path: path.trim(),
pathDisplay: pathDisplay.trim(),
tags
};
});
if (this.results.length > 0) {
this.renderResults(query);
} else {
this.showNoResults(query);
}
} catch (error) {
console.error('[LinkInserter] Erreur de recherche:', error);
this.showError();
}
}, 200);
}
handleKeyNavigation(event) {
console.log('[LinkInserter] Key pressed:', event.key, 'Results:', this.results.length);
if (this.results.length === 0) {
if (event.key === 'Escape') {
event.preventDefault();
this.close();
}
return;
}
switch (event.key) {
case 'ArrowDown':
console.log('[LinkInserter] Arrow Down - moving to index:', this.selectedIndex + 1);
event.preventDefault();
this.selectedIndex = Math.min(this.selectedIndex + 1, this.results.length - 1);
this.updateSelection();
break;
case 'ArrowUp':
console.log('[LinkInserter] Arrow Up - moving to index:', this.selectedIndex - 1);
event.preventDefault();
this.selectedIndex = Math.max(this.selectedIndex - 1, 0);
this.updateSelection();
break;
case 'Enter':
console.log('[LinkInserter] Enter pressed - calling selectResult()');
event.preventDefault();
this.selectResult();
break;
case 'Escape':
console.log('[LinkInserter] Escape pressed - closing modal');
event.preventDefault();
this.close();
break;
}
}
updateSelection() {
const resultItems = this.resultsContainer.querySelectorAll('.link-inserter-result-item');
resultItems.forEach((item, index) => {
if (index === this.selectedIndex) {
item.classList.add('selected');
item.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
} else {
item.classList.remove('selected');
}
});
}
selectResult() {
console.log('[LinkInserter] selectResult called, results:', this.results.length);
if (this.results.length === 0) {
console.warn('[LinkInserter] No results to select');
return;
}
const selected = this.results[this.selectedIndex];
console.log('[LinkInserter] Selected:', selected);
console.log('[LinkInserter] Callback exists:', !!this.callback);
if (selected && this.callback) {
console.log('[LinkInserter] Calling callback with:', { title: selected.title, path: selected.path });
// Sauvegarder le callback localement avant de fermer
const callback = this.callback;
// Fermer le modal d'abord
this.close();
// Puis appeler le callback après un petit délai pour que le modal se ferme proprement
setTimeout(() => {
console.log('[LinkInserter] Executing callback now...');
callback({
title: selected.title,
path: selected.path
});
}, 50);
} else {
console.error('[LinkInserter] Cannot select: no callback or no selected item');
this.close();
}
}
renderResults(query) {
this.resultsContainer.innerHTML = '';
this.selectedIndex = 0;
const resultsHeader = document.createElement('div');
resultsHeader.className = 'link-inserter-results-header';
resultsHeader.innerHTML = `
<span class="link-inserter-results-count">${this.results.length} note${this.results.length > 1 ? 's' : ''}</span>
`;
this.resultsContainer.appendChild(resultsHeader);
this.results.forEach((result, index) => {
const item = document.createElement('div');
item.className = 'link-inserter-result-item';
if (index === 0) item.classList.add('selected');
item.innerHTML = `
<div class="link-inserter-result-icon">📄</div>
<div class="link-inserter-result-content">
<div class="link-inserter-result-title">${this.highlightQuery(result.title, query)}</div>
<div class="link-inserter-result-path">${result.pathDisplay}</div>
${result.tags.length > 0 ? `
<div class="link-inserter-result-tags">
${result.tags.map(tag => `<span class="tag-pill-small">${tag}</span>`).join('')}
</div>
` : ''}
</div>
`;
// Click handler
item.addEventListener('click', (e) => {
console.log('[LinkInserter] Item clicked, index:', index);
e.preventDefault();
e.stopPropagation();
this.selectedIndex = index;
this.selectResult();
});
// Hover handler
item.addEventListener('mouseenter', () => {
this.selectedIndex = index;
this.updateSelection();
});
this.resultsContainer.appendChild(item);
});
}
highlightQuery(text, query) {
if (!query || !text) return text;
const terms = query.split(/\s+/)
.filter(term => !term.includes(':'))
.map(term => term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
if (terms.length === 0) return text;
const regex = new RegExp(`(${terms.join('|')})`, 'gi');
return text.replace(regex, '<mark>$1</mark>');
}
showHelp() {
this.results = [];
this.resultsContainer.innerHTML = `
<div class="link-inserter-help">
<div class="link-inserter-help-text">
🔗 Tapez pour rechercher une note
</div>
</div>
`;
}
showLoading() {
this.resultsContainer.innerHTML = `
<div class="link-inserter-loading">
<div class="link-inserter-spinner"></div>
<p>Recherche...</p>
</div>
`;
}
showNoResults(query) {
this.results = [];
this.resultsContainer.innerHTML = `
<div class="link-inserter-no-results">
<div class="link-inserter-no-results-icon">🔍</div>
<p>Aucune note trouvée pour « <strong>${this.escapeHtml(query)}</strong> »</p>
</div>
`;
}
showError() {
this.resultsContainer.innerHTML = `
<div class="link-inserter-error">
<div class="link-inserter-error-icon">⚠️</div>
<p>Erreur lors de la recherche</p>
</div>
`;
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
/**
* Ouvrir le modal de sélection de lien
* @param {Object} options - Options d'ouverture
* @param {EditorView} options.editorView - Instance CodeMirror
* @param {Function} options.onSelect - Callback appelé avec {title, path}
*/
open({ editorView, onSelect }) {
console.log('[LinkInserter] open() called with callback:', !!onSelect);
if (this.isOpen) return;
this.editorView = editorView;
this.callback = onSelect;
this.isOpen = true;
this.modal.style.display = 'flex';
// Animation
requestAnimationFrame(() => {
this.modal.classList.add('active');
});
// Focus sur l'input
setTimeout(() => {
this.input.focus();
this.input.select();
}, 100);
// Reset
this.selectedIndex = 0;
this.showHelp();
}
close() {
if (!this.isOpen) return;
this.isOpen = false;
this.modal.classList.remove('active');
setTimeout(() => {
this.modal.style.display = 'none';
this.input.value = '';
this.results = [];
this.callback = null;
this.editorView = null;
}, 200);
}
destroy() {
if (this.modal && this.modal.parentNode) {
this.modal.parentNode.removeChild(this.modal);
}
this.modal = null;
this.input = null;
this.resultsContainer = null;
}
}
// Instance globale
window.linkInserter = null;
// Initialisation automatique
document.addEventListener('DOMContentLoaded', () => {
window.linkInserter = new LinkInserter();
});
export { LinkInserter };

View File

@ -3,3 +3,4 @@ import './file-tree.js';
import './ui.js'; import './ui.js';
import './search.js'; import './search.js';
import './daily-notes.js'; import './daily-notes.js';
import './link-inserter.js';

View File

@ -27,6 +27,12 @@ type TreeNode struct {
Children []*TreeNode `json:"children,omitempty"` Children []*TreeNode `json:"children,omitempty"`
} }
// BacklinkInfo représente une note qui référence la note courante
type BacklinkInfo struct {
Path string `json:"path"`
Title string `json:"title"`
}
// Handler gère toutes les routes de l'API. // Handler gère toutes les routes de l'API.
type Handler struct { type Handler struct {
notesDir string notesDir string
@ -696,14 +702,20 @@ func (h *Handler) handleGetNote(w http.ResponseWriter, r *http.Request, filename
content = []byte(initialContent) content = []byte(initialContent)
} }
// Récupérer les backlinks pour cette note
backlinks := h.idx.GetBacklinks(filename)
backlinkData := h.buildBacklinkData(backlinks)
data := struct { data := struct {
Filename string Filename string
Content string Content string
IsHome bool IsHome bool
Backlinks []BacklinkInfo
}{ }{
Filename: filename, Filename: filename,
Content: string(content), Content: string(content),
IsHome: false, IsHome: false,
Backlinks: backlinkData,
} }
err = h.templates.ExecuteTemplate(w, "editor.html", data) err = h.templates.ExecuteTemplate(w, "editor.html", data)
@ -1135,3 +1147,35 @@ func (h *Handler) removeEmptyDirRecursive(relPath string) {
} }
} }
} }
// buildBacklinkData transforme une liste de chemins de notes en BacklinkInfo avec titres
func (h *Handler) buildBacklinkData(paths []string) []BacklinkInfo {
if len(paths) == 0 {
return nil
}
result := make([]BacklinkInfo, 0, len(paths))
for _, path := range paths {
// Lire le fichier pour extraire le titre du front matter
fullPath := filepath.Join(h.notesDir, path)
fm, _, err := indexer.ExtractFrontMatterAndBody(fullPath)
title := ""
if err == nil && fm.Title != "" {
title = fm.Title
} else {
// Fallback: dériver le titre du nom de fichier
title = strings.TrimSuffix(filepath.Base(path), ".md")
title = strings.ReplaceAll(title, "-", " ")
title = strings.Title(title)
}
result = append(result, BacklinkInfo{
Path: path,
Title: title,
})
}
return result
}

View File

@ -7,6 +7,7 @@ import (
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"regexp"
"sort" "sort"
"strings" "strings"
"sync" "sync"
@ -20,6 +21,7 @@ type Indexer struct {
mu sync.RWMutex mu sync.RWMutex
tags map[string][]string tags map[string][]string
docs map[string]*Document docs map[string]*Document
backlinks map[string][]string // note path -> list of notes that reference it
} }
// Document représente une note indexée pour la recherche. // Document représente une note indexée pour la recherche.
@ -53,6 +55,7 @@ func New() *Indexer {
return &Indexer{ return &Indexer{
tags: make(map[string][]string), tags: make(map[string][]string),
docs: make(map[string]*Document), docs: make(map[string]*Document),
backlinks: make(map[string][]string),
} }
} }
@ -112,9 +115,31 @@ func (i *Indexer) Load(root string) error {
indexed[tag] = list indexed[tag] = list
} }
// Build backlinks index
backlinksMap := make(map[string][]string)
for sourcePath, doc := range documents {
links := extractInternalLinks(doc.Body)
for _, targetPath := range links {
// Add sourcePath to the backlinks of targetPath
if _, ok := backlinksMap[targetPath]; !ok {
backlinksMap[targetPath] = make([]string, 0)
}
// Avoid duplicates
if !containsString(backlinksMap[targetPath], sourcePath) {
backlinksMap[targetPath] = append(backlinksMap[targetPath], sourcePath)
}
}
}
// Sort backlinks for consistency
for _, links := range backlinksMap {
sort.Strings(links)
}
i.mu.Lock() i.mu.Lock()
i.tags = indexed i.tags = indexed
i.docs = documents i.docs = documents
i.backlinks = backlinksMap
i.mu.Unlock() i.mu.Unlock()
return nil return nil
@ -668,3 +693,56 @@ func (i *Indexer) GetAllTagsWithCount() []TagCount {
return result return result
} }
// GetBacklinks retourne la liste des notes qui référencent la note spécifiée
func (i *Indexer) GetBacklinks(path string) []string {
i.mu.RLock()
defer i.mu.RUnlock()
links, ok := i.backlinks[path]
if !ok || len(links) == 0 {
return nil
}
// Retourner une copie pour éviter les modifications externes
result := make([]string, len(links))
copy(result, links)
return result
}
// extractInternalLinks extrait tous les liens internes d'un texte Markdown/HTML
// Format: <a ... hx-get="/api/notes/path/to/note.md" ...>
func extractInternalLinks(body string) []string {
// Pattern pour capturer le chemin dans hx-get="/api/notes/..."
// On cherche: hx-get="/api/notes/ suivi de n'importe quoi jusqu'au prochain guillemet
pattern := `hx-get="/api/notes/([^"]+)"`
// Compiler la regex
re, err := regexp.Compile(pattern)
if err != nil {
return nil
}
// Trouver tous les matches
matches := re.FindAllStringSubmatch(body, -1)
if len(matches) == 0 {
return nil
}
// Extraire les chemins (groupe de capture 1)
links := make([]string, 0, len(matches))
seen := make(map[string]struct{})
for _, match := range matches {
if len(match) > 1 {
path := match[1]
// Éviter les doublons
if _, ok := seen[path]; !ok {
seen[path] = struct{}{}
links = append(links, path)
}
}
}
return links
}

View File

@ -1,7 +1,7 @@
--- ---
title: Book Notes title: Book Notes
date: 10-11-2025 date: 10-11-2025
last_modified: 11-11-2025:15:23 last_modified: 11-11-2025:18:07
tags: tags:
- personal - personal
- notes - notes
@ -25,5 +25,4 @@ Key takeaways:
- The Mom Test - Rob Fitzpatrick - The Mom Test - Rob Fitzpatrick
- Shape Up - Basecamp - Shape Up - Basecamp
[texte](/notes/)
/""

View File

@ -1,7 +1,7 @@
--- ---
title: AI Writing Assistant title: AI Writing Assistant
date: 10-11-2025 date: 10-11-2025
last_modified: 11-11-2025:11:13 last_modified: 11-11-2025:17:56
tags: tags:
- idea - idea
- ai - ai
@ -28,3 +28,5 @@ Intégrer un assistant IA pour:
Données restent locales, API optionnelle. Données restent locales, API optionnelle.
Test test Test test

View File

@ -1,8 +1,11 @@
--- ---
title: "Authentication Guide" title: Authentication Guide
date: "10-11-2025" date: 10-11-2025
last_modified: "10-11-2025:19:21" last_modified: 11-11-2025:18:30
tags: ["documentation", "api", "security"] tags:
- documentation
- api
- security
--- ---
# Authentication # Authentication
@ -39,3 +42,5 @@ Authorization: Bearer eyJhbGc...
- HTTPS only in production - HTTPS only in production
- Reverse proxy with nginx - Reverse proxy with nginx
- Rate limiting - Rate limiting
<a href="#" onclick="return false;" hx-get="/api/notes/test-delete-1.md" hx-target="#editor-container" hx-swap="innerHTML">Test Delete 1</a>

View File

@ -1,8 +1,10 @@
--- ---
title: "Archived Ideas" title: Archived Ideas
date: "10-11-2025" date: 10-11-2025
last_modified: "10-11-2025:19:21" last_modified: 11-11-2025:18:24
tags: ["archive", "ideas"] tags:
- archive
- ideas
--- ---
# Archived Ideas # Archived Ideas
@ -20,3 +22,5 @@ No real use case.
## Gamification ## Gamification
Not aligned with minimalist approach. Not aligned with minimalist approach.
<a href="#" hx-get="/api/notes/un-dossier/test/Poppy-test.md" hx-target="#editor-container" hx-swap="innerHTML">Poppy Test</a>

View File

@ -1,8 +1,10 @@
--- ---
title: "Real-time Collaboration" title: Real-time Collaboration
date: "10-11-2025" date: 10-11-2025
last_modified: "10-11-2025:19:21" last_modified: 11-11-2025:17:25
tags: ["idea", "collaboration"] tags:
- idea
- collaboration
--- ---
# Real-time Collaboration # Real-time Collaboration
@ -13,6 +15,7 @@ Plusieurs utilisateurs éditent la même note simultanément.
## Technology ## Technology
- WebSockets - WebSockets
- Operational Transforms ou CRDT - Operational Transforms ou CRDT
- Presence indicators - Presence indicators

View File

@ -1,7 +1,7 @@
--- ---
title: Automatic Tagging title: Automatic Tagging
date: 10-11-2025 date: 10-11-2025
last_modified: 11-11-2025:15:41 last_modified: 11-11-2025:17:56
tags: tags:
- research - research
- ai - ai
@ -30,4 +30,9 @@ Suggest tags based on note content.
## Training Data ## Training Data
Use existing notes with tags as training set. Use existing notes with tags as
training set.
[texte](url)

View File

@ -1,7 +1,7 @@
--- ---
title: Typography Research title: Typography Research
date: 10-11-2025 date: 10-11-2025
last_modified: 11-11-2025:13:52 last_modified: 11-11-2025:18:18
tags: tags:
- research - research
- design - design
@ -34,3 +34,5 @@ tags:
- Line height: 1.6 - Line height: 1.6
- Max width: 65ch - Max width: 65ch
- Font size: 16px base - Font size: 16px base
/ili

View File

@ -1,7 +1,7 @@
--- ---
title: UI Design Inspiration title: UI Design Inspiration
date: 10-11-2025 date: 10-11-2025
last_modified: 11-11-2025:15:25 last_modified: 11-11-2025:18:19
tags: tags:
- research - research
- design - design
@ -32,3 +32,5 @@ Consider:
- Catppuccin - Catppuccin
dldkfdddddd dldkfdddddd
[Poppy Test](un-dossier/test/Poppy-test.md)

View File

@ -1,7 +1,7 @@
--- ---
title: Go Performance Optimization title: Go Performance Optimization
date: 10-11-2025 date: 10-11-2025
last_modified: 11-11-2025:15:16 last_modified: 11-11-2025:18:28
tags: tags:
- research - research
- tech - tech
@ -33,3 +33,4 @@ type Cache struct {
go test -cpuprofile=cpu.prof go test -cpuprofile=cpu.prof
go tool pprof cpu.prof go tool pprof cpu.prof
``` ```
<a href="#" onclick="return false;" hx-get="/api/notes/un-dossier/test/Poppy-test.md" hx-target="#editor-container" hx-swap="innerHTML">Poppy Test</a>

View File

@ -1,7 +1,7 @@
--- ---
title: WebSockets for Live Updates title: WebSockets for Live Updates
date: 10-11-2025 date: 10-11-2025
last_modified: 11-11-2025:15:27 last_modified: 11-11-2025:18:14
tags: tags:
- research - research
- tech - tech
@ -37,3 +37,6 @@ type Hub struct {
``` ```
lfkfdkfd dd lfkfdkfd dd
/il

View File

@ -1,7 +1,7 @@
--- ---
title: Test Delete 1 title: Test Delete 1
date: 11-11-2025 date: 11-11-2025
last_modified: 11-11-2025:15:40 last_modified: 11-11-2025:18:31
--- ---
test file 1 test file 1
@ -9,3 +9,6 @@ test file 1
ddddddddlffdfdddddddddddddd ddddddddlffdfdddddddddddddd
[texte](url)
<a href="#" onclick="return false;" hx-get="/api/notes/documentation/bienvenue.md" hx-target="#editor-container" hx-swap="innerHTML">Bienvenue dans Project Notes</a>

View File

@ -1,9 +1,17 @@
--- ---
title: Poppy Test title: Poppy Test
date: 10-11-2025 date: 10-11-2025
last_modified: 10-11-2025:18:08 last_modified: 11-11-2025:18:41
--- ---
# Poppy Test # Poppy Test
Commencez à écrire votre note ici... Commencez à écrire votre note ici...
Logiquement cette page à des backlinks.
On verra bien à la fin.
<a href="#" onclick="return false;" hx-get="/api/notes/un-dossier/test/Poppy-test.md" hx-target="#editor-container" hx-swap="innerHTML">Poppy Test</a>
<a href="#" onclick="return false;" hx-get="/api/notes/research/design/typography.md" hx-target="#editor-container" hx-swap="innerHTML">Typography Research</a>

176
static/sidebar-resize.js Normal file
View File

@ -0,0 +1,176 @@
/**
* Sidebar Resizable
* Allows users to resize the sidebar by dragging the right edge
*/
(function() {
'use strict';
const STORAGE_KEY = 'sidebar-width';
const DEFAULT_WIDTH = 300;
const MIN_WIDTH = 200;
const MAX_WIDTH = 600;
let sidebar = null;
let resizeHandle = null;
let isResizing = false;
let startX = 0;
let startWidth = 0;
/**
* Initialize sidebar resize functionality
*/
function init() {
sidebar = document.querySelector('#sidebar');
if (!sidebar) {
console.warn('Sidebar not found');
return;
}
console.log('Sidebar resize initialized');
// Create resize handle
createResizeHandle();
// Restore saved width
restoreSidebarWidth();
// Add event listeners
setupEventListeners();
}
/**
* Create the resize handle element
*/
function createResizeHandle() {
resizeHandle = document.createElement('div');
resizeHandle.className = 'sidebar-resize-handle';
resizeHandle.title = 'Drag to resize sidebar / Glisser pour redimensionner';
sidebar.appendChild(resizeHandle);
console.log('Resize handle created and appended');
}
/**
* Setup event listeners for resizing
*/
function setupEventListeners() {
resizeHandle.addEventListener('mousedown', startResize);
document.addEventListener('mousemove', handleResize);
document.addEventListener('mouseup', stopResize);
// Prevent text selection while resizing
resizeHandle.addEventListener('selectstart', (e) => e.preventDefault());
}
/**
* Start resizing
*/
function startResize(e) {
isResizing = true;
startX = e.clientX;
startWidth = sidebar.offsetWidth;
resizeHandle.classList.add('resizing');
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
console.log('Started resizing from:', startX, 'width:', startWidth);
e.preventDefault();
e.stopPropagation();
}
/**
* Handle resize dragging
*/
function handleResize(e) {
if (!isResizing) return;
const delta = e.clientX - startX;
const newWidth = Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, startWidth + delta));
applySidebarWidth(newWidth);
e.preventDefault();
e.stopPropagation();
}
/**
* Stop resizing
*/
function stopResize(e) {
if (!isResizing) return;
isResizing = false;
resizeHandle.classList.remove('resizing');
document.body.style.cursor = '';
document.body.style.userSelect = '';
console.log('Stopped resizing at width:', sidebar.offsetWidth);
// Save the new width
saveSidebarWidth();
}
/**
* Save sidebar width to localStorage
*/
function saveSidebarWidth() {
const width = sidebar.offsetWidth;
try {
localStorage.setItem(STORAGE_KEY, width.toString());
} catch (e) {
console.warn('Failed to save sidebar width:', e);
}
}
/**
* Restore sidebar width from localStorage
*/
function restoreSidebarWidth() {
try {
const savedWidth = localStorage.getItem(STORAGE_KEY);
if (savedWidth) {
const width = parseInt(savedWidth, 10);
if (width >= MIN_WIDTH && width <= MAX_WIDTH) {
applySidebarWidth(width);
}
}
} catch (e) {
console.warn('Failed to restore sidebar width:', e);
}
}
/**
* Apply sidebar width to both sidebar and main content
*/
function applySidebarWidth(width) {
sidebar.style.width = `${width}px`;
// Update main content margin
const main = document.querySelector('main');
if (main) {
main.style.marginLeft = `${width}px`;
}
}
/**
* Reset sidebar to default width
*/
function resetSidebarWidth() {
applySidebarWidth(DEFAULT_WIDTH);
saveSidebarWidth();
}
// Initialize on DOM ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
// DOM already loaded
init();
}
// Expose reset function globally for debugging
window.resetSidebarWidth = resetSidebarWidth;
})();

View File

@ -132,6 +132,8 @@ aside {
left: 0; left: 0;
bottom: 0; bottom: 0;
width: 300px; width: 300px;
min-width: 200px;
max-width: 600px;
transform: translateX(0); transform: translateX(0);
transition: transform 0.25s ease; transition: transform 0.25s ease;
z-index: 10; z-index: 10;
@ -144,6 +146,51 @@ aside {
gap: var(--spacing-md); gap: var(--spacing-md);
} }
/* Resize handle for sidebar */
.sidebar-resize-handle {
position: absolute;
top: 0;
right: -6px; /* Overlap for easier grab */
bottom: 0;
width: 16px; /* Even wider for Firefox */
cursor: col-resize;
background: rgba(66, 165, 245, 0.05);
border-left: 2px solid transparent;
border-right: 2px solid transparent;
transition: all 0.2s ease;
z-index: 11;
display: flex;
align-items: center;
justify-content: center;
}
.sidebar-resize-handle:hover {
background: rgba(66, 165, 245, 0.15);
border-left-color: var(--accent-primary);
border-right-color: var(--accent-primary);
}
.sidebar-resize-handle.resizing {
background: rgba(66, 165, 245, 0.25);
border-left: 3px solid var(--accent-primary);
border-right: 3px solid var(--accent-primary);
}
/* Visual indicator - always visible */
.sidebar-resize-handle::before {
content: '⋮';
font-size: 18px;
color: var(--accent-primary);
opacity: 0.5;
transition: all 0.2s ease;
pointer-events: none;
}
.sidebar-resize-handle:hover::before {
opacity: 1;
text-shadow: 0 0 4px var(--accent-primary);
}
aside::-webkit-scrollbar { aside::-webkit-scrollbar {
width: 8px; width: 8px;
} }
@ -1483,7 +1530,9 @@ body, html {
position: fixed; position: fixed;
top: 0; top: 0;
left: -280px; left: -280px;
width: 280px; width: 280px !important; /* Force width on mobile, ignore resize */
min-width: 280px;
max-width: 280px;
height: 100vh; height: 100vh;
z-index: 1000; z-index: 1000;
transition: left 0.3s cubic-bezier(0.4, 0, 0.2, 1); transition: left 0.3s cubic-bezier(0.4, 0, 0.2, 1);
@ -1493,6 +1542,11 @@ body, html {
background: var(--bg-primary); background: var(--bg-primary);
} }
/* Hide resize handle on mobile */
.sidebar-resize-handle {
display: none;
}
aside.sidebar-visible { aside.sidebar-visible {
left: 0; left: 0;
} }
@ -2923,3 +2977,382 @@ body, html {
max-height: 180px; max-height: 180px;
} }
} }
/* ==========================================================================
Link Inserter Modal Styles
========================================================================== */
.link-inserter-modal {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 10000;
align-items: flex-start;
justify-content: center;
padding-top: 15vh;
opacity: 0;
transition: opacity 200ms ease;
}
.link-inserter-modal.active {
opacity: 1;
}
.link-inserter-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(3px);
}
.link-inserter-container {
position: relative;
width: 90%;
max-width: 560px;
background: var(--bg-secondary);
border: 1px solid var(--border-primary);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg), var(--shadow-glow);
display: flex;
flex-direction: column;
max-height: 60vh;
transform: translateY(-20px);
transition: transform 200ms ease;
}
.link-inserter-modal.active .link-inserter-container {
transform: translateY(0);
}
/* Header */
.link-inserter-header {
padding: var(--spacing-md);
border-bottom: 1px solid var(--border-primary);
}
.link-inserter-input-wrapper {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.link-inserter-icon {
color: var(--accent-primary);
flex-shrink: 0;
}
.link-inserter-input {
flex: 1;
background: transparent;
border: none;
color: var(--text-primary);
font-size: 15px;
outline: none;
padding: var(--spacing-sm) 0;
}
.link-inserter-input::placeholder {
color: var(--text-muted);
}
.link-inserter-kbd {
background: var(--bg-tertiary);
border: 1px solid var(--border-primary);
border-radius: var(--radius-sm);
padding: 2px 6px;
font-size: 11px;
font-family: monospace;
color: var(--text-muted);
flex-shrink: 0;
}
/* Body */
.link-inserter-body {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
}
.link-inserter-results {
flex: 1;
overflow-y: auto;
padding: var(--spacing-sm);
}
/* Results Header */
.link-inserter-results-header {
padding: var(--spacing-sm) var(--spacing-md);
color: var(--text-muted);
font-size: 0.8rem;
}
.link-inserter-results-count {
font-weight: 500;
}
/* Result Item */
.link-inserter-result-item {
display: flex;
gap: var(--spacing-sm);
padding: var(--spacing-sm) var(--spacing-md);
margin: var(--spacing-xs) 0;
border-radius: var(--radius-md);
cursor: pointer;
transition: all var(--transition-fast);
border: 1px solid transparent;
}
.link-inserter-result-item:hover {
background: var(--bg-tertiary);
}
.link-inserter-result-item.selected {
background: linear-gradient(135deg, rgba(130, 170, 255, 0.15), rgba(199, 146, 234, 0.15));
border-color: var(--accent-primary);
}
.link-inserter-result-icon {
font-size: 1.2rem;
flex-shrink: 0;
}
.link-inserter-result-content {
flex: 1;
min-width: 0;
}
.link-inserter-result-title {
font-weight: 500;
color: var(--text-primary);
margin-bottom: 3px;
font-size: 0.95rem;
}
.link-inserter-result-title mark {
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
color: white;
padding: 2px 4px;
border-radius: 3px;
font-weight: 600;
}
.link-inserter-result-path {
font-size: 0.75rem;
color: var(--text-muted);
margin-bottom: 4px;
}
.link-inserter-result-tags {
display: flex;
gap: 4px;
flex-wrap: wrap;
margin-top: 4px;
}
.tag-pill-small {
background: var(--bg-tertiary);
color: var(--accent-primary);
border: 1px solid var(--border-primary);
padding: 1px 6px;
border-radius: 10px;
font-size: 0.7rem;
font-weight: 500;
}
/* Help */
.link-inserter-help {
padding: var(--spacing-xl);
text-align: center;
}
.link-inserter-help-text {
font-size: 0.95rem;
color: var(--text-muted);
}
/* Loading */
.link-inserter-loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: var(--spacing-xl);
gap: var(--spacing-md);
}
.link-inserter-spinner {
width: 32px;
height: 32px;
border: 3px solid var(--bg-tertiary);
border-top-color: var(--accent-primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.link-inserter-loading p {
color: var(--text-muted);
font-size: 0.9rem;
}
/* No Results */
.link-inserter-no-results {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: var(--spacing-xl);
gap: var(--spacing-sm);
}
.link-inserter-no-results-icon {
font-size: 2.5rem;
opacity: 0.6;
}
.link-inserter-no-results p {
color: var(--text-secondary);
font-size: 0.9rem;
text-align: center;
}
/* Error */
.link-inserter-error {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: var(--spacing-xl);
gap: var(--spacing-sm);
}
.link-inserter-error-icon {
font-size: 2.5rem;
}
.link-inserter-error p {
color: var(--text-secondary);
font-size: 0.9rem;
}
/* Footer */
.link-inserter-footer {
padding: var(--spacing-sm) var(--spacing-md);
border-top: 1px solid var(--border-primary);
background: var(--bg-tertiary);
}
.link-inserter-footer-hint {
display: flex;
align-items: center;
gap: var(--spacing-sm);
font-size: 0.75rem;
color: var(--text-muted);
}
.link-inserter-footer-hint kbd {
background: var(--bg-secondary);
border: 1px solid var(--border-primary);
border-radius: var(--radius-sm);
padding: 2px 5px;
font-family: monospace;
font-size: 0.7rem;
}
/* ============================================
BACKLINKS SECTION
============================================ */
/* Preview wrapper to contain both preview and backlinks */
.preview-wrapper {
display: flex;
flex-direction: column;
gap: var(--spacing-md);
min-height: 0;
height: 100%;
}
/* Backlinks section styling */
.backlinks-section {
background: var(--bg-secondary);
border: 1px solid var(--border-primary);
border-radius: var(--radius-md);
padding: var(--spacing-md);
margin-top: var(--spacing-md);
}
.backlinks-title {
font-size: 1rem;
font-weight: 600;
color: var(--text-primary);
margin: 0 0 var(--spacing-sm) 0;
padding-bottom: var(--spacing-sm);
border-bottom: 1px solid var(--border-primary);
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
.backlinks-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
}
.backlink-item {
margin: 0;
padding: 0;
}
.backlink-link {
display: block;
padding: var(--spacing-sm) var(--spacing-md);
background: var(--bg-tertiary);
border: 1px solid var(--border-secondary);
border-radius: var(--radius-sm);
color: var(--text-primary);
text-decoration: none;
transition: all var(--transition-fast);
font-size: 0.9rem;
font-weight: 500;
}
.backlink-link:hover {
background: var(--bg-primary);
border-color: var(--accent-primary);
color: var(--accent-primary);
transform: translateX(4px);
box-shadow: var(--shadow-sm);
}
.backlink-link:active {
transform: translateX(2px);
}
/* Mobile Adaptation */
@media screen and (max-width: 768px) {
.link-inserter-container {
width: 100%;
max-width: 100%;
margin: 0;
border-radius: 0;
max-height: 100vh;
}
.link-inserter-input {
font-size: 14px;
}
.link-inserter-results {
max-height: calc(100vh - 200px);
}
}

View File

@ -23,8 +23,24 @@
<div class="editor-panel {{if .IsHome}}hidden{{end}}"> <div class="editor-panel {{if .IsHome}}hidden{{end}}">
<textarea id="editor" name="content">{{.Content}}</textarea> <textarea id="editor" name="content">{{.Content}}</textarea>
</div> </div>
<div class="preview-wrapper">
<div id="preview" class="preview markdown-preview"> <div id="preview" class="preview markdown-preview">
</div> </div>
{{if .Backlinks}}
<div id="backlinks-section" class="backlinks-section">
<h3 class="backlinks-title">🔗 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" class="backlink-link">
📄 {{.Title}}
</a>
</li>
{{end}}
</ul>
</div>
{{end}}
</div>
</div> </div>
{{if not .IsHome}} {{if not .IsHome}}
<div class="editor-actions"> <div class="editor-actions">

View File

@ -16,6 +16,7 @@
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify/dist/purify.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/dompurify/dist/purify.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js"></script> <script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js"></script>
<script src="/static/sidebar-resize.js"></script>
<script src="/frontend/src/theme-manager.js"></script> <script src="/frontend/src/theme-manager.js"></script>
<script src="/frontend/src/font-manager.js"></script> <script src="/frontend/src/font-manager.js"></script>
<script src="/frontend/src/vim-mode-manager.js"></script> <script src="/frontend/src/vim-mode-manager.js"></script>