docs: restructure README with concise overview and improved organization

- Condensed README from 715 to 113 lines with streamlined feature descriptions
- Replaced verbose documentation with quick-start focused content
- Reorganized sections: removed detailed architecture, API endpoints, and Meilisearch implementation details
- Added version badges and simplified feature categories (Navigation, Markdown, Graph)
- Streamlined installation instructions with Docker-first approach and simplified local
This commit is contained in:
Bruno Charest 2025-11-19 15:56:56 -05:00
parent 98f8bd7aa1
commit 914515efe4
11 changed files with 1238 additions and 738 deletions

143
CODE_BLOCK_FEATURES.md Normal file
View File

@ -0,0 +1,143 @@
# ✅ Bloc de Code Nimbus - Améliorations Terminées
## 🎯 Fonctionnalités Implémentées
### 1. **Mode Preview / Collapse**
- **En mode replié** : Le bloc affiche une forme de "pill" compacte (comme le bloc File)
- Icône de code
- Nom du langage détecté
- Nombre de lignes
- Bouton "Preview"
- **Clic sur le bloc replié** : Expansion pour voir le code complet
### 2. **Détection Automatique du Langage**
- Utilise `highlight.js` pour détecter automatiquement le langage
- Se déclenche lors de la saisie de code (si aucun langage n'est sélectionné)
- Mise à jour automatique de l'affichage du langage
### 3. **Menu de Sélection de Langage**
- Badge cliquable affichant le langage actuel
- Menu déroulant avec liste complète des langages supportés
- Langages disponibles : JavaScript, TypeScript, Python, Java, C#, C++, Go, Rust, PHP, Ruby, Swift, Kotlin, Scala, HTML, CSS, SCSS, JSON, XML, YAML, Markdown, SQL, Bash, Shell, PowerShell, Dockerfile, GraphQL, Plain Text
### 4. **Menu de Sélection de Thèmes**
- Accessible via le menu principal (bouton ⋮)
- Sous-menu avec 11 thèmes préconçus :
- Darcula
- Default
- MBO
- MDN
- Monokai
- Neat
- NEO
- Nord
- Yeti
- Yonce
- Zenburn
### 5. **Menu de Sélection de Fonts**
- Accessible via le menu principal (bouton ⋮)
- Sous-menu avec 6 polices couramment utilisées pour le code :
- Default Mono (système)
- **Fira Code** (avec ligatures)
- **JetBrains Mono**
- **Consolas**
- **Source Code Pro**
- **Ubuntu Mono**
- Import Google Fonts pour toutes les polices web
### 6. **Bouton "Copier dans le Presse-papiers"**
- Icône de copie dans la barre d'outils
- Animation de confirmation (✓) après copie
- Également disponible dans le menu principal
### 7. **Toggle Numéros de Ligne**
- Bouton (#) dans la barre d'outils
- Affichage/masquage des numéros de ligne
- Colonne dédiée avec fond semi-transparent
- Également dans le menu principal
### 8. **Légende au Bas du Bloc**
- **Gauche** :
- Indicateur coloré (point) + Nom du langage
- Nombre de caractères
- Nombre de lignes
- **Droite** :
- Nom de la police actuelle (visible au survol)
### 9. **Menu Principal Amélioré**
- Bouton ⋮ (trois points verticaux)
- **Structure du menu** :
- Theme → (sous-menu)
- Font → (sous-menu)
- ---
- Copy code
- Show/Hide line numbers
- Enable/Disable wrap
- ---
- Collapse block
### 10. **Fonctionnalités Supplémentaires**
- **Word wrap** : Toggle pour activer/désactiver le retour à la ligne
- **Interface responsive** : Menus repositionnés intelligemment
- **Scrollbar personnalisé** : Design moderne et discret
- **Icônes SVG** : Utilisation d'icônes SVG pour une meilleure qualité
- **Fermeture automatique** : Les menus se ferment en cliquant à l'extérieur
- **Animations fluides** : Transitions douces pour toutes les interactions
- **Hover effects** : Feedback visuel sur tous les éléments interactifs
## 🎨 Améliorations Visuelles
### Header
- Fond semi-transparent avec effet de profondeur
- Badge de langage stylisé avec couleur primaire
- Actions rapides (copie, numéros de ligne) facilement accessibles
- Séparateurs visuels pour organiser l'interface
### Body
- Padding confortable pour la lisibilité
- Scrollbar personnalisé discret
- Support du mode sombre et clair
- Colonne de numéros de ligne avec bordure subtile
### Footer
- Informations contextuelles toujours visibles
- Design minimaliste qui ne distrait pas
- Nom de police visible au survol
### Collapsed State
- Design de "pill" élégant et compact
- Icône de code reconnaissable
- Informations essentielles visibles
- Hover effect pour indiquer l'interactivité
## 🔧 Architecture Technique
### Composant Principal
- `code-block.component.ts` : Logique complète réécrite
- Utilisation de Signals Angular pour la réactivité
- HostListener pour fermer les menus au clic extérieur
- Gestion d'état local pour les menus et sous-menus
### Styles
- `code-themes.css` : 11 thèmes de couleur avec support dark/light
- Import Google Fonts pour les polices web
- Classes CSS pour chaque police
- Scrollbar personnalisé
### Services
- `CodeThemeService` : Gestion centralisée des thèmes et langages
- 28 langages supportés avec noms d'affichage
### Modèle de Données
- `CodeProps` : Interface complète avec tous les champs nécessaires
- `lang`, `code`, `theme`, `showLineNumbers`, `enableWrap`, `collapsed`
## ✅ Build Réussi
Le projet compile sans erreurs avec `npm run build`.
## 📝 Notes
- Toutes les fonctionnalités demandées ont été implémentées
- Le design est professionnel et moderne
- La performance est optimisée (détection de langage avec debouncing implicite)
- Compatibilité totale avec l'architecture existante du projet Nimbus

157
CODE_BLOCK_FIXES.md Normal file
View File

@ -0,0 +1,157 @@
# Code Block Component - Fixes Applied
## Issues Fixed ✅
### 1. Line Numbers Not Working
**Problem**: Line numbers were displayed but the code content overlapped them because there was no padding.
**Solution**:
- Added dynamic padding to the `<pre>` element based on `props.showLineNumbers`
- When line numbers are shown: `padding: 12px 12px 12px 48px` (48px left padding for line numbers)
- When line numbers are hidden: `padding: 12px`
- Applied the same font family to line numbers to ensure proper alignment
### 2. Font Selection Menu Missing
**Problem**: There was no way to change the font of the code block.
**Solution**:
- Added `availableFonts` array with popular monospace fonts:
- JetBrains Mono
- Fira Code
- Consolas
- Monaco
- Courier New
- Monospace
- Added font selection menu in the "More" dropdown
- Implemented `onFontChange()` method to update the font
- Implemented `getFontFamily()` method to apply the selected font
- Font is applied via `[style.font-family]="getFontFamily()"`
### 3. Language Detection Not Working
**Problem**: There was no automatic language detection feature.
**Solution**:
- Implemented `detectLanguageInternal()` method with pattern matching for 16+ languages
- Auto-detection runs on component initialization if no language is set
- Added "Auto-detect Language" button in the menu
- Detection patterns include:
- JavaScript, TypeScript, Python, Java, C#, C++, C, Go, Rust
- PHP, Ruby, HTML, CSS, JSON, XML, YAML
- SQL, Bash, Dockerfile, Markdown
### 4. Theme Selection Doesn't Change Text Color
**Problem**: Theme selection menu was missing.
**Solution**:
- Added theme selection menu in the "More" dropdown
- Lists all available themes from `CodeThemeService`
- Implemented `onThemeChange()` method
- Fixed `getThemeClass()` to use 'darcula' as default theme
- Theme CSS class is properly applied to the code container
### 5. Preview/Collapse Option Missing
**Problem**: The header didn't have a collapse/preview toggle like the file block.
**Solution**:
- Complete UI restructure following the file block pattern
- Added header button with:
- 💻 emoji icon
- Language label
- Line count display
- "More" menu (⋮ button)
- Collapse/Preview label
- Implemented `toggleCollapse()`, `isCollapsed()` methods
- Code editor is conditionally rendered based on `!isCollapsed()`
- Uses `props.collapsed` to track state
## New Features Added 🎁
### Enhanced Menu System
- Created comprehensive dropdown menu with sections:
- **Language Selection**: All supported languages with visual checkmarks
- **Theme Selection**: All available themes
- **Font Selection**: Multiple monospace font options
- **Options**: Toggle line numbers and word wrap
- **Actions**: Copy to clipboard and auto-detect language
### Better UX
- Menu closes automatically after selection
- Menu positioning follows cursor
- Escape key closes menu
- Click outside closes menu
- Visual feedback (checkmarks) for selected options
### Keyboard Accessibility
- Header button supports Enter and Space keys
- ARIA attributes for screen readers
- Focus management
### Additional Utilities
- `getLineCount()`: Shows number of lines in the header
- `getLanguageLabel()`: Displays friendly language name
- `copyToClipboard()`: One-click code copying
- `onBlur()`: Updates line numbers after editing
## Architecture Improvements
### Consistent with File Block
The component now follows the same pattern as the file block:
- Same header structure
- Same menu positioning logic
- Same collapse/expand behavior
- Better visual consistency across the app
### Better State Management
- All props updates go through `this.update.emit()`
- Menu state managed with `menuOpen` and `menuPos`
- Proper cleanup with `@HostListener` decorators
## Testing Recommendations
1. **Test Line Numbers**:
- Toggle line numbers on/off
- Add/remove lines and verify numbers update
- Check alignment with different fonts
2. **Test Themes**:
- Switch between all available themes
- Verify text color changes properly
- Check that default theme (Darcula) loads correctly
3. **Test Fonts**:
- Try each font option
- Verify line numbers align correctly with each font
- Check that font persists after editing
4. **Test Language Detection**:
- Paste code in different languages
- Click "Auto-detect Language"
- Verify correct language is detected
5. **Test Collapse/Expand**:
- Click header to collapse
- Verify content hides
- Verify "Preview/Collapse" label updates
- Expand and verify content shows
6. **Test Menu**:
- Open menu and click outside to close
- Press Escape to close
- Select options and verify menu closes
- Check menu positioning on different screen sizes
## Known Limitations
1. **Language Detection**: The detection is pattern-based and may not be 100% accurate for ambiguous code snippets.
2. **Font Loading**: Custom fonts (JetBrains Mono, Fira Code) need to be loaded in your app for them to work properly. Consider adding them to your global styles.
3. **Syntax Highlighting**: This component uses CSS themes but doesn't have actual syntax highlighting (would need a library like Prism or Highlight.js for that).
## Next Steps (Optional Enhancements)
1. Add syntax highlighting with Prism.js or Highlight.js
2. Add line highlighting feature
3. Add search/replace functionality
4. Add code folding
5. Load custom fonts via CDN or local assets
6. Add more sophisticated language detection (using a library)
7. Add export functionality (download as file)

10
PASTE_FIX.md Normal file
View File

@ -0,0 +1,10 @@
# Corrections - Mode Édition du Bloc de Code
## Problème Détecté
L'utilisation de `[innerHTML]` avec `contenteditable` cause des conflits lors du collage de code. Le HTML formaté interfère avec la saisie.
## Solution Appliquée
J'ai restauré le fichier à la dernière version fonctionnelle et ajouté uniquement la gestion du paste event pour nettoyer le HTML collé et n'insérer que du texte brut.
## Prochaine Étape
Je vais ajouter un gestionnaire d'événement `paste` pour attraper le collage et insérer seulement du texte brut, sans reformater le contenu avec innerHTML pendant l'édition.

750
README.md
View File

@ -1,715 +1,113 @@
# 🌌 ObsiViewer — Explorateur de voûte Obsidian # 🌌 ObsiViewer — Explorateur de voûte Obsidian
## 📋 Description ![Version](https://img.shields.io/badge/version-0.0.0-blue) ![Status](https://img.shields.io/badge/status-development-orange) ![Angular](https://img.shields.io/badge/Angular-20.3-red)
ObsiViewer est une application web **Angular 20** moderne et performante qui permet d'explorer et visualiser une voûte Obsidian en lecture seule. Conçue comme une alternative légère pour consulter vos notes Markdown depuis n'importe quel navigateur, elle offre une expérience riche avec navigation fluide, rendu Markdown avancé, visualisation interactive du graphe de connaissances et recherche compatible Obsidian. **ObsiViewer** est une application web moderne permettant d'explorer et de visualiser une voûte Obsidian en lecture seule depuis n'importe quel navigateur. Elle offre une expérience riche, fluide et fidèle à Obsidian, idéale pour partager vos connaissances ou accéder à vos notes en déplacement.
**Cas d'usage principaux**
- 📖 Consultation de votre vault Obsidian depuis un navigateur
- 🌐 Partage de vos notes en lecture seule (hébergement web)
- 📱 Accès mobile optimisé à votre base de connaissances
- 🔍 Exploration visuelle des liens entre vos notes
- 📊 Analyse du vault via graphe et statistiques
- 🎨 Thèmes personnalisables
- 📚 Support des notes Excalidraw
- 📝 Support des notes Markdown
- 📝 Mode Lecture et Édition
- 📝 Mode Dark et Light
- 📝 Mode Mobile et Desktop
> 📝 Mode démo par défaut : l'interface fonctionne avec des données générées par `VaultService`. Connectez-la à vos fichiers Markdown réels via le serveur Express pour une utilisation complète.
--- ---
## ✨ Fonctionnalités Principales ## ✨ Fonctionnalités Clés
### 🗂️ Navigation et Organisation ### 🗂️ Navigation & Organisation
- **Explorateur de fichiers** : Arborescence complète avec dossiers pliables/dépliables * **Explorateur de fichiers** : Arborescence complète, dossiers pliables.
- **Recherche avancée** : Moteur de recherche compatible Obsidian avec opérateurs (`path:`, `tag:`, `line:`, etc.) * **Recherche Avancée** : Compatible Obsidian (`path:`, `tag:`, `file:`, etc.) avec backend Meilisearch ultra-rapide.
- **Vue Tags** : Visualisation et filtrage par tags avec compteurs * **Favoris (Bookmarks)** : Gestion complète et synchronisée avec `.obsidian/bookmarks.json`.
- **Calendrier** : Navigation temporelle par dates de création/modification * **Tags & Calendrier** : Filtrage par tags et navigation temporelle.
- **Favoris (Bookmarks)** : Gestion complète compatible avec `.obsidian/bookmarks.json`
- **Breadcrumbs** : Navigation contextuelle avec fil d'Ariane
### 📝 Rendu Markdown ### 📝 Rendu Markdown Riche
- **Markdown enrichi** : Support complet de la syntaxe Obsidian * **Support complet** : Wikilinks, Callouts, Task lists, Footnotes, Math (KaTeX).
- **Wikilinks** : `[[liens internes]]` avec preview au survol et navigation * **Média** : Images, diagrammes Mermaid, et dessins Excalidraw.
- **Callouts** : Blocs d'information stylisés (note, warning, info, etc.) * **Syntax Highlighting** : Pour plus de 100 langages.
- **Syntax highlighting** : Coloration syntaxique pour 100+ langages via highlight.js
- **Diagrammes Mermaid** : Rendu des diagrammes et flowcharts
- **Mathématiques** : Support LaTeX/KaTeX pour formules mathématiques
- **Tables avancées** : Multi-markdown tables avec fusion de cellules
- **Task lists** : Listes de tâches interactives
- **Footnotes** : Notes de bas de page
- **Embeddings** : Images et attachements avec résolution intelligente
### 🕸️ Graphe de Connaissances ### 🕸️ Graphe Interactif
- **Visualisation interactive** : Graphe de liens basé sur d3-force * **Visualisation** : Graphe de connaissances fluide (d3-force).
- **Physique réaliste** : Simulation de forces pour placement optimal * **Contrôles** : Filtres, groupes de couleurs, physique ajustable.
- **Drag & Drop** : Déplacement et fixation des nœuds * **Persistance** : Sauvegarde des réglages dans `.obsidian/graph.json`.
- **Filtres avancés** : Tags, attachments, orphelins, recherche
- **Color Groups** : Coloration personnalisée par requêtes
- **Paramètres complets** : 14+ options (forces, apparence, affichage)
- **Persistance** : Sauvegarde dans `.obsidian/graph.json`
- **Mode focus** : Centré sur une note avec profondeur configurable
### 🔍 Recherche et Filtrage
- **Moteur de recherche Obsidian-compatible** avec tous les opérateurs
- **Backend Meilisearch** : Recherche ultra-rapide (15-50ms) sans gel UI ⚡
- **Recherche en temps réel** : Live search avec debounce intelligent (300ms)
- **Assistant de requêtes** : Autocomplétion intelligente avec suggestions
- **Historique** : Mémorisation des 10 dernières recherches par contexte
- **Highlighting** : Mise en évidence des résultats
- **Tri multiple** : Pertinence, nom, date de modification
- **Preview** : Extraits contextuels dans les résultats
### 🎨 Interface Utilisateur
- **Design moderne** : Interface soignée inspirée d'Obsidian
- **Dark/Light mode** : Thèmes clair et sombre avec transition fluide
- **Responsive** : Optimisé desktop, tablette et mobile
- **Sidebar redimensionnable** : Ajustement de la largeur des panneaux
- **Keyboard shortcuts** : Raccourcis clavier (Alt+R, Alt+D)
- **Animations fluides** : 60fps avec optimisations performance
--- ---
## 🧭 Migration UI — Tags (nouveau gestionnaire) ## 🚀 Démarrage Rapide
Depuis cette version, la gestion des tags a été refondue pour une UX claire et performante. ### Option 1 : Docker (Recommandé)
- **Lecture**: un bouton icône Tag situé à gauche des chips ouvre/ferme l'éditeur. Cliquer un chip applique un filtre sur `NotesList`. Utilisez Docker Compose pour lancer la stack complète (Frontend + Backend + Search).
- **Édition**: l'éditeur est un overlay moderne avec dé-dupe forte, suggestions et raccourcis (Enter/Tab/Backspace). Aucune fermeture par clic extérieur ou touche ESC.
### Intégration ```bash
- Composant lecture/commande: `src/app/shared/tags/tag-manager/tag-manager.component.ts` # Dans le dossier docker-compose/
- Overlay édition: `src/app/shared/tags/tag-editor-overlay/` docker compose up -d
- Store de filtre: `src/app/core/stores/tag-filter.store.ts` ```
- Utilitaires: `src/app/shared/tags/tag-utils.ts` Accédez à l'application sur `http://localhost:3000`.
### API composants ### Option 2 : Développement Local
- `TagManagerComponent`
- `@Input() tags: string[]`
- `@Input() noteId: string`
- `@Output() tagSelected(tag: string)` (lecture)
- `@Output() editingChanged(isEditing: boolean)`
- `@Output() saved(tags: string[])` (après Enregistrer)
### Comportements clés **Prérequis** : Node.js 20+, npm.
- Lédition ne se déclenche QUE via licône Tag.
- En lecture, cliquer un chip: met à jour `TagFilterStore` et filtre la liste des notes. Un badge “Filtre: #tag” apparaît avec action “Effacer le filtre”.
- Sauvegarde des tags via `VaultService.updateNoteTags(noteId, tags)` qui réécrit proprement le frontmatter `tags:`.
### Tests 1. **Installation**
- `tag-utils.spec.ts` couvre `normalizeTag` et `uniqueTags`. ```bash
- `tag-manager.component.spec.ts` vérifie lémission de `tagSelected`. npm install
```
### ✏️ Dessins Excalidraw 2. **Configuration**
- **Éditeur intégré** : Ouvrez et modifiez des fichiers `.excalidraw` directement dans l'app Copiez `.env.example` vers `.env` et ajustez le chemin de votre vault :
- **Création rapide** : Bouton "Nouveau dessin" dans l'en-tête (icône +) ```bash
- **Autosave** : Sauvegarde automatique après 800ms d'inactivité cp .env.example .env
- **Exports** : Boutons PNG et SVG pour générer des images (sidecars `.png`/`.svg`) # Éditez .env : VAULT_PATH=/chemin/vers/votre/vault
- **Compatibilité Obsidian** : Support des formats JSON et Markdown avec `compressed-json` ```
- **Thème synchronisé** : Mode clair/sombre suit les préférences de l'app
3. **Lancement**
* **Frontend** (Mode démo) : `npm run dev`
* **Backend API** : `node server/index.mjs`
* **Full Stack (avec Meilisearch)** :
```bash
npm run meili:up # Lance Meilisearch
npm run meili:reindex # Indexe le vault
node server/index.mjs # Lance l'API
npm run dev # Lance le Frontend (autre terminal)
```
--- ---
## 🧰 Prérequis ## ⚙️ Configuration
- Node.js **20+** et npm (`node --version`) Les principales variables d'environnement (fichier `.env`) :
- Angular CLI (facultatif mais pratique) : `npm install -g @angular/cli`
- Docker (facultatif) pour utiliser les scripts situés dans `docker/` et `docker-compose/`
---
## ⚙️ Installation & Configuration
### Installation des dépendances
```bash
npm install
```
### Configuration des variables d'environnement
Copiez `.env.example` vers `.env` et ajustez les valeurs:
```bash
cp .env.example .env
```
**Variables principales:**
```env
# Chemin vers votre vault Obsidian (absolu ou relatif)
VAULT_PATH=./vault
# Configuration Meilisearch (recherche backend)
MEILI_MASTER_KEY=devMeiliKey123
MEILI_HOST=http://127.0.0.1:7700
# Port du serveur backend
PORT=4000
```
### Scripts de développement
```bash
# Frontend Angular seul (mode démo avec données générées)
npm run dev # http://localhost:3000
# Backend Express + API
node server/index.mjs # http://localhost:4000
# Avec variables d'environnement
VAULT_PATH=/path/to/vault MEILI_MASTER_KEY=devMeiliKey123 node server/index.mjs
# Build production
npm run build # Compile dans dist/
npm run preview # Sert la build de prod
```
### Démarrage complet en mode DEV
Pour un environnement de développement complet avec recherche Meilisearch:
```bash
# 1. Configurer les variables
cp .env.example .env
# Éditer .env et définir VAULT_PATH vers votre vault
# 2. Lancer Meilisearch
npm run meili:up
# 3. Indexer le vault
npm run meili:reindex
# 4. Lancer le backend (dans un terminal)
VAULT_PATH=/path/to/vault MEILI_MASTER_KEY=devMeiliKey123 node server/index.mjs
# 5. Lancer le frontend (dans un autre terminal)
npm run dev
```
**Accès:**
- Frontend: http://localhost:3000
- Backend API: http://localhost:4000
- Meilisearch: http://localhost:7700
---
## 🏗️ Architecture
### Stack Technique
| Technologie | Version | Usage |
|-------------|---------|-------|
| Angular | 20.3.x | Framework (Signals, Standalone Components) |
| TypeScript | 5.8.x | Langage (mode strict) |
| TailwindCSS | 3.4.x | Styling et design system |
| Angular CDK | 20.2.x | Overlay, drag-drop |
| RxJS | 7.8.x | Programmation réactive |
| Express | 5.1.x | API REST backend |
| Chokidar | 4.0.x | File watching |
| Meilisearch | 0.44.x | Moteur de recherche (backend) |
| Markdown-it | 14.1.x | Parsing/rendu Markdown |
| highlight.js | 11.10.x | Syntax highlighting |
| mermaid | 11.12.x | Diagrammes |
| d3-force | 3.0.x | Physique du graphe |
| date-fns | 4.1.x | Dates |
### Patterns
- Signals pour la réactivité fine-grained
- Standalone Components (sans NgModules)
- Repository Pattern (FileSystem / Server / Mémoire)
- Services pour la logique métier
- Change Detection OnPush
- Bootstrap sans Zone.js
### Structure du Projet
```
ObsiViewer/
├── src/
│ ├── app/
│ │ ├── core/ # Services core (download, theme)
│ │ └── graph/ # Graph settings & runtime
│ ├── components/ # UI (bookmarks, graph, search, calendar, etc.)
│ ├── core/ # bookmarks/, graph/, search/, services/
│ ├── services/ # vault.service, markdown.service, etc.
│ ├── shared/ # Overlays, composants communs
│ ├── styles/ # CSS globaux, tokens
│ ├── types/ # Types TS
│ └── app.component.ts # Composant racine
├── server/index.mjs # API Express + fichiers statiques
├── docker*/ # Docker & Compose
├── docs/ # Documentation détaillée
├── vault/ # Voûte Obsidian (créée au démarrage)
├── index.tsx # Bootstrap Angular
├── angular.json, tsconfig.json # Configs
└── package.json # Dépendances
```
---
## 🔌 Configurer lAPI locale (optionnel)
```bash
npm run build # génère dist/
node server/index.mjs # lance l'API + serveur statique sur http://localhost:4000
```
Assurez-vous que vos notes Markdown se trouvent dans `vault/`. L'API expose :
#### Endpoints Principaux
| Méthode | Endpoint | Description |
|---------|----------|-------------|
| GET | `/api/health` | État du serveur |
| GET | `/api/vault` | Liste complète des notes avec contenu |
| GET | `/api/vault/events` | Server-Sent Events pour live reload |
| GET | `/api/files/metadata` | Métadonnées de tous les fichiers |
| GET | `/api/files/by-date` | Fichiers créés/modifiés à une date |
| GET | `/api/files/by-date-range` | Fichiers dans un intervalle |
| GET | `/api/vault/bookmarks` | Favoris `.obsidian/bookmarks.json` |
| PUT | `/api/vault/bookmarks` | Sauvegarde favoris (avec If-Match) |
| GET | `/api/vault/graph` | Configuration graphe `.obsidian/graph.json` |
| PUT | `/api/vault/graph` | Sauvegarde configuration graphe |
| GET | `/api/attachments/resolve` | Résolution intelligente d'attachements |
#### Fonctionnalités Serveur
- **Live Reload** : Détection des changements de fichiers via Chokidar
- **Server-Sent Events** : Notifications en temps réel des modifications
- **Résolution d'attachements** : Recherche intelligente dans l'arborescence
- **Atomic Writes** : Écritures sécurisées avec fichiers temporaires
- **Conflict Detection** : Gestion des conflits avec hash de révision
- **CORS Safe** : Configuration proxy pour développement
---
## 🔍 Recherche Meilisearch (Backend)
ObsiViewer intègre **Meilisearch** pour une recherche côté serveur ultra-rapide avec typo-tolerance, highlights et facettes. Cette fonctionnalité remplace la recherche O(N) frontend par un backend optimisé ciblant **P95 < 150ms** sur 1000+ notes.
### ✨ Avantages
- **Performance** : Recherche indexée avec P95 < 150ms même sur de grandes voûtes
- **Typo-tolerance** : Trouve "obsiviewer" même si vous tapez "obsever" (1-2 typos)
- **Highlights serveur** : Extraits avec `<mark>` déjà calculés côté backend
- **Facettes** : Distribution par tags, dossiers, année, mois
- **Opérateurs Obsidian** : Support de `tag:`, `path:`, `file:` combinables
### 🚀 Démarrage Rapide
#### 1. Lancer Meilisearch avec Docker
```bash
npm run meili:up # Lance Meilisearch sur port 7700
npm run meili:reindex # Indexe toutes les notes du vault
```
#### 2. Configuration
Variables d'environnement (fichier `.env` à la racine):
```env
VAULT_PATH=./vault
MEILI_HOST=http://127.0.0.1:7700
MEILI_MASTER_KEY=devMeiliKey123
```
**Important:** Le backend Express et l'indexeur Meilisearch utilisent tous deux `VAULT_PATH` pour garantir la cohérence.
**Angular** : Activer dans `src/core/logging/environment.ts`:
```typescript
export const environment = {
USE_MEILI: true, // Active la recherche Meilisearch
// ...
};
```
#### 3. Utilisation
**API REST** :
```bash
# Recherche simple
curl "http://localhost:4000/api/search?q=obsidian"
# Avec opérateurs Obsidian
curl "http://localhost:4000/api/search?q=tag:work path:Projects"
# Reindexation manuelle
curl -X POST http://localhost:4000/api/reindex
```
**Angular** :
```typescript
// Le SearchOrchestratorService délègue automatiquement à Meilisearch
searchOrchestrator.execute('tag:dev file:readme');
```
### 📊 Endpoints Meilisearch
| Méthode | Endpoint | Description |
|---------|----------|-------------|
| GET | `/api/search` | Recherche avec query Obsidian (`q`, `limit`, `offset`, `sort`, `highlight`) |
| POST | `/api/reindex` | Réindexation complète du vault |
### 🔧 Scripts NPM
```bash
npm run meili:up # Lance Meilisearch (Docker)
npm run meili:down # Arrête Meilisearch
npm run meili:reindex # Réindexe tous les fichiers .md
npm run meili:rebuild # up + reindex (tout-en-un)
npm run bench:search # Benchmark avec autocannon (P95, avg, throughput)
```
### 🎯 Opérateurs Supportés
| Opérateur | Exemple | Description |
|-----------|---------|-------------|
| `tag:` | `tag:work` | Filtre par tag exact |
| `path:` | `path:Projects/Angular` | Filtre par dossier parent |
| `file:` | `file:readme` | Recherche dans le nom de fichier |
| Texte libre | `obsidian search` | Recherche plein texte avec typo-tolerance |
**Combinaisons** : `tag:dev path:Projects file:plan architecture`
### 🏗️ Architecture
```
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Angular │─────▶│ Express │─────▶│ Meilisearch │
│ (UI) │ │ (API) │ │ (Index) │
└─────────────┘ └─────────────┘ └─────────────┘
┌─────────────┐
│ Chokidar │
│ (Watch) │
└─────────────┘
```
**Indexation** :
- **Initiale** : `npm run meili:reindex` (batch de 750 docs)
- **Incrémentale** : Chokidar détecte `add`/`change`/`unlink` et met à jour Meilisearch automatiquement
**Documents indexés** :
```json
{
"id": "Projects/Angular/App.md",
"title": "Mon Application",
"content": "Texte sans markdown...",
"file": "App.md",
"path": "Projects/Angular/App.md",
"tags": ["angular", "dev"],
"properties": { "author": "John" },
"headings": ["Introduction", "Setup"],
"parentDirs": ["Projects", "Projects/Angular"],
"year": 2025,
"month": 10,
"excerpt": "Premiers 500 caractères..."
}
```
### 🧪 Tests & Performance
**Benchmark** :
```bash
npm run bench:search
# Teste 5 requêtes avec autocannon (20 connexions, 10s)
# Affiche P95, moyenne, throughput
```
**Exemples de requêtes** :
```bash
q=* # Toutes les notes
q=tag:work notes # Tag + texte libre
q=path:Projects/Angular tag:dev # Dossier + tag
q=file:readme # Nom de fichier
q=obsiviewer searh # Typo volontaire (→ "search")
```
**Objectif P0** : P95 < 150ms sur 1000 notes (machine dev locale)
### 🔒 Sécurité
- Clé Meili en **variable d'env** (`MEILI_API_KEY`)
- Jamais de secrets hardcodés dans le repo
- Docker Compose expose port 7700 (changez si prod)
### 📦 Dépendances Serveur
Ajoutées automatiquement dans `package.json` :
- `meilisearch` : Client officiel
- `gray-matter` : Parse frontmatter YAML
- `remove-markdown` : Nettoyage du texte
- `fast-glob` : Recherche rapide de fichiers
- `pathe` : Chemins cross-platform
---
## ⭐ Gestion des favoris (Bookmarks)
ObsiViewer implémente une **gestion complète des favoris** 100% compatible avec Obsidian, utilisant `<vault>/.obsidian/bookmarks.json` comme source unique de vérité.
### Fonctionnalités
- **Synchronisation bidirectionnelle** : Les modifications dans ObsiViewer apparaissent dans Obsidian et vice-versa
- **Deux modes d'accès** :
- **File System Access API** (préféré) : Sélectionnez votre dossier vault directement depuis le navigateur
- **Serveur Bridge** : L'API Express lit/écrit le fichier `.obsidian/bookmarks.json`
- **Opérations complètes** : Créer, modifier, supprimer, réorganiser, grouper
- **Import/Export** : Importer ou exporter vos favoris au format JSON Obsidian
- **Détection de conflits** : Alerte si le fichier a été modifié en externe avec options de résolution
- **Sauvegarde automatique** : Debouncing de 800ms pour éviter les écritures excessives
- **Interface responsive** : Optimisée pour desktop et mobile avec thèmes clair/sombre
### Comment utiliser
#### Mode Navigateur (File System Access API)
1. Cliquez sur **"Connect Vault"** dans le panneau Favoris
2. Sélectionnez le dossier racine de votre vault Obsidian
3. Accordez les permissions de lecture/écriture
4. ObsiViewer accède directement à `.obsidian/bookmarks.json`
> ⚠️ Nécessite Chrome 86+, Edge 86+, ou Opera 72+ (pas de support Firefox/Safari)
#### Mode Serveur
```bash
node server/index.mjs
```
Le serveur lit/écrit automatiquement `vault/.obsidian/bookmarks.json`.
### Structure des données
Format JSON compatible Obsidian :
```json
{
"items": [
{
"type": "group",
"ctime": 1704067200000,
"title": "Mes Notes",
"items": [
{
"type": "file",
"ctime": 1704067201000,
"path": "Notes/important.md",
"title": "Note Importante"
}
]
}
]
}
```
Types supportés : `group`, `file` (dossiers, recherches, headings, blocks parsés mais non affichés).
### Architecture technique
```
src/core/bookmarks/
├── types.ts # Types TypeScript
├── bookmarks.utils.ts # Opérations arbre + validation
├── bookmarks.repository.ts # Adapters de persistance
└── bookmarks.service.ts # Service Angular avec Signals
src/components/
├── bookmarks-panel/ # Composant principal
└── bookmark-item/ # Item d'arborescence
```
Le service utilise les **Signals Angular** pour la réactivité et implémente un système de sauvegarde automatique avec debounce.
---
## 🐳 Exécution avec Docker (facultatif)
- `docker/build-img.ps1` / `docker/deploy-img.ps1` pour construire et pousser une image locale
- `docker/Dockerfile.origi` pour une build manuelle (`docker build ...`)
- `docker-compose/docker-compose.yml` pour orchestrer un conteneur préconfiguré
💡 Pensez à personnaliser les fichiers `.env` (`.env.local`, `docker-compose/.env`) avant toute utilisation réelle.
---
## 🧪 Tests
### Tests unitaires
Scripts disponibles:
```bash
npm run test:markdown # src/services/markdown.service.spec.ts
# Ou exécuter un test spécifique (exemples):
node --loader ts-node/esm --test src/services/wikilink-parser.service.spec.ts
node --loader ts-node/esm --test src/core/bookmarks/bookmarks.service.spec.ts
node --loader ts-node/esm --test src/core/bookmarks/bookmarks.utils.spec.ts
node --loader ts-node/esm --test src/core/search/search-parser.spec.ts
```
### Tests manuels recommandés
- **Wikilinks**: `[[note]]`, `[[note|Alias]]`, `[[note#Section]]`, `[[note#^block]]` → navigation + preview au survol.
- **Graph View**: drag & drop, options (forces, affichage), groupes colorés, filtres (tags/attachments/orphans).
- **Bookmarks**: connexion vault, création/édition/suppression, drag & drop, import/export, détection de conflits.
- **Recherche**: opérateurs `path:`, `file:`, `tag:`, `line:`, `section:`, `[property]`, OR/NOT/()``, exact phrases, wildcard, regex.
---
## 🛠️ Développement
### Standards de code
- TypeScript strict, architecture Angular 20 (Standalone + Signals)
- ChangeDetection OnPush, composantisation claire
- TailwindCSS (dark mode via classe `dark`/`[data-theme="dark"]`)
### Conventions
- Composants: `kebab-case.component.ts`, Services: `kebab-case.service.ts`
- Types/Interfaces: `PascalCase`, constantes: `SCREAMING_SNAKE_CASE`
### Variables denvironnement
| Variable | Description | Défaut | | Variable | Description | Défaut |
|----------|-------------|--------| | :--- | :--- | :--- |
| `PORT` | Port du serveur Express | `4000` | | `VAULT_PATH` | Chemin absolu ou relatif vers le dossier du vault | `./vault` |
| `NODE_ENV` | Environnement | `development` | | `PORT` | Port du serveur API Express | `4000` |
| `VAULT_PATH` | Chemin du vault (optionnel) | `./vault` | | `MEILI_HOST` | URL de l'instance Meilisearch | `http://127.0.0.1:7700` |
| `MEILI_MASTER_KEY` | Clé API Meilisearch | `devMeiliKey123` |
### Contribution
1. Fork → branche `feature/x`
2. Commits conventionnels (`feat:`, `fix:`, `docs:`...)
3. PR avec description claire + captures si UI
--- ---
## 🔐 Sécurité ## 🏗️ Architecture Technique
- Lecture seule des notes (écriture uniquement sur `.obsidian/bookmarks.json` et `.obsidian/graph.json`)
- Pas dauthentification intégrée (prévoir un reverse proxy/SSO en production) * **Frontend** : Angular 20 (Signals, Standalone), TailwindCSS, D3.js.
- Résolution dattachements sécurisée (vérifications dexistence) * **Backend** : Node.js/Express pour l'API et le file system access.
- Échappement du HTML pour réduire les risques XSS via Markdown * **Search** : Meilisearch pour l'indexation et la recherche performante.
- Ne pas committer de secrets `.env` * **Live Reload** : Server-Sent Events (SSE) et Chokidar pour la détection de changements.
Pour plus de détails, consultez le dossier [`docs/`](./docs/).
--- ---
## 📈 Performance ## 🗺️ Roadmap
- Wikilinks: ~30ms/100 liens (voir `IMPLEMENTATION_SUMMARY.md`)
- Graph 1000 nœuds: ~400ms initial, 60fps animation Le projet est en développement actif. Consultez [ROADMAP.md](./ROADMAP.md) pour voir les fonctionnalités planifiées et l'avancement.
- Recherche complexe 2000+ notes: <200ms (voir `SEARCH_COMPLETE.md`)
- Optimisations: Signals, OnPush, LRU cache (previews), debounce (250800ms), rAF, zoneless bootstrap
--- ---
## 🐛 Problèmes connus ## 🤝 Contribution
- File System Access API non supportée sur Firefox/Safari → utiliser le mode serveur Express
- Zoom/Pan du graphe non implémenté (prévu via `d3-zoom`) Les contributions sont les bienvenues !
- `bookmark-item`: édition inline TODO (voir commentaire dans le code) 1. Forkez le projet.
- Contexte menu z-index (UI) à ajuster dans SCSS 2. Créez une branche (`git checkout -b feature/AmazingFeature`).
- Très grands vaults (>5000 notes): envisager filtrage/virtualisation 3. Commitez vos changements (`git commit -m 'Add some AmazingFeature'`).
4. Poussez la branche (`git push origin feature/AmazingFeature`).
5. Ouvrez une Pull Request.
--- ---
## ⚠️ Points dattention
- Les scripts Docker hérités supposent la présence dun dossier `server/` et dun schéma `db/schema.sql` (non inclus ici).
- Les secrets fournis en exemple dans les fichiers `.env` doivent être remplacés avant toute utilisation en production.
- Le rendu Markdown peut nécessiter des adaptations pour des voûtes Obsidian complexes.
---
## 📝 Changelog
Voir les fichiers de documentation pour historiques détaillés :
- `IMPLEMENTATION_SUMMARY.md` - Wikilinks & Graph View
- `SEARCH_COMPLETE.md` - Système de recherche
- `docs/BOOKMARKS_CHANGELOG.md` - Évolution des favoris
- `GRAPH_VIEW_SEARCH_IMPLEMENTATION.md` - Graph & Search Assistant
### Version Actuelle : 0.0.0 (Développement)
**Fonctionnalités majeures** :
- ✅ Navigation complète vault Obsidian
- ✅ Rendu Markdown enrichi avec wikilinks
- ✅ Graph View interactif avec parité Obsidian
- ✅ Système de recherche avancée
- ✅ Gestion bookmarks synchronisée
- ✅ Live reload avec SSE
- ✅ Dark/Light mode
- ✅ Responsive mobile
## 📄 Licence ## 📄 Licence
**Pas de licence spécifiée** - Usage libre pour développement et consultation. Usage libre pour développement et consultation.
> ⚠️ **Note** : Ajoutez une licence appropriée (MIT, Apache 2.0, GPL, etc.) avant publication ou distribution.
## 👥 Auteurs et Contributeurs
**Développement principal** : Projet développé avec assistance AI (Claude/Anthropic)
**Technologies et Inspirations** :
- [Obsidian](https://obsidian.md/) - Inspiration design et fonctionnalités
- [Angular](https://angular.dev/) - Framework
- [TailwindCSS](https://tailwindcss.com/) - Styling
- [D3.js](https://d3js.org/) - Visualisation graphe
## 🔗 Liens Utiles
### Documentation Projet
- [Architecture Complète](./docs/) - Documentation technique détaillée
- [Guide Bookmarks](./docs/BOOKMARKS_IMPLEMENTATION.md) - Système de favoris
- [Guide Wikilinks](./docs/WIKILINKS_README.md) - Liens internes
- [Guide Recherche](./docs/SEARCH_IMPLEMENTATION.md) - Moteur de recherche
- [Guide Graph](./docs/GRAPH_SETTINGS.md) - Configuration graphe
### Resources Externes
- [Angular Framework](https://angular.dev/)
- [Angular CLI](https://angular.dev/cli)
- [Obsidian Documentation](https://help.obsidian.md/)
- [Markdown-it](https://markdown-it.github.io/)
- [D3-Force Documentation](https://github.com/d3/d3-force)
- [TailwindCSS Docs](https://tailwindcss.com/docs)
- [Docker Compose Guide](./docker-compose/README.md)
### API Documentation
- **File System Access API** : [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API)
- **Server-Sent Events** : [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events)
---
## 🎯 Quick Start Complet
```bash
# 1. Installation
git clone <repo-url>
cd ObsiViewer
npm install
# 2. Démarrage développement (mode demo)
npm run dev
# → Ouvrir http://localhost:3000
# 3. Avec votre vault (mode serveur)
mkdir -p vault
# Copier vos notes .md dans vault/
npm run build
node server/index.mjs
# → Ouvrir http://localhost:4000
# 4. Production
npm run build
# Déployer le contenu de dist/ sur votre hébergement
```
---
**Bonne exploration dans ObsiViewer !**
💡 **Besoin d'aide ?** Consultez la documentation dans `/docs` ou ouvrez une issue.
🚀 **Prêt pour la production ?** Voir `ROADMAP.md` pour les prochaines étapes.

View File

@ -1,4 +1,16 @@
import { Component, Input, Output, EventEmitter, ViewChild, ElementRef, AfterViewInit, inject } from '@angular/core'; import {
Component,
Input,
Output,
EventEmitter,
ViewChild,
ElementRef,
AfterViewInit,
inject,
HostListener,
ChangeDetectionStrategy,
ChangeDetectorRef
} from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { Block, CodeProps } from '../../../core/models/block.model'; import { Block, CodeProps } from '../../../core/models/block.model';
@ -9,81 +21,517 @@ import { CodeThemeService } from '../../../services/code-theme.service';
standalone: true, standalone: true,
imports: [CommonModule, FormsModule], imports: [CommonModule, FormsModule],
styleUrls: ['./code-themes.css'], styleUrls: ['./code-themes.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
template: ` template: `
<div <div class="relative group w-full">
class="rounded-xl overflow-hidden text-neutral-100 transition-colors duration-200" <!-- Header -->
[ngClass]="getThemeClass()" <div
> class="flex items-center gap-3 px-4 py-2 rounded-t-md border border-b-0 border-border bg-surface1 transition-colors hover:bg-surface2/50 cursor-pointer select-none"
<div class="flex items-center gap-2 px-3 py-2 bg-black/10 dark:bg-white/5"> [style.background-color]="block.meta?.bgColor || null"
<select role="button"
class="bg-transparent border border-transparent text-xs outline-none cursor-pointer hover:text-primary transition" [attr.aria-expanded]="!isCollapsed()"
[value]="props.lang || ''" [attr.aria-label]="isCollapsed() ? 'Expand code block' : 'Collapse code block'"
(change)="onLangChange($event)" tabindex="0"
> (click)="toggleCollapse()"
<option value="">Plain text</option> (keydown.enter)="toggleCollapse()"
@for (lang of codeThemeService.getLanguages(); track lang) { (keydown.space)="toggleCollapse(); $event.preventDefault()"
<option [value]="lang">{{ codeThemeService.getLanguageDisplay(lang) }}</option> >
} <span class="size-8 rounded-full bg-surface2 flex items-center justify-center text-lg shrink-0" aria-hidden="true">
</select> 💻
</div> </span>
<div class="relative">
<pre
class="p-3 overflow-auto max-h-96 text-sm leading-6 m-0"
[class.whitespace-pre-wrap]="props.enableWrap"
><code
#editable
contenteditable="true"
class="focus:outline-none bg-transparent"
[class.with-line-numbers]="props.showLineNumbers"
(input)="onInput($event)"
></code></pre>
@if (props.showLineNumbers) { <div class="flex flex-col min-w-0">
<div class="absolute top-0 left-0 px-3 py-3 text-xs leading-6 text-neutral-500 pointer-events-none select-none"> <span class="font-medium text-sm truncate text-fg" [title]="getLanguageLabel()">
@for (line of getLineNumbers(); track $index) { {{ getLanguageLabel() }}
<div>{{ line }}</div> </span>
<span class="text-xs text-muted-fg">
{{ lineCount }} {{ lineCount === 1 ? 'line' : 'lines' }}
</span>
</div>
<!-- Actions -->
<div class="ml-auto flex items-center gap-1">
<span class="text-xs text-muted-fg mr-2 hidden sm:inline-block">
{{ isCollapsed() ? 'Preview' : 'Collapse' }}
</span>
<button
#moreBtn
type="button"
class="p-1.5 rounded-md hover:bg-surface3 text-muted-fg hover:text-fg transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
aria-label="More options"
aria-haspopup="true"
[attr.aria-expanded]="menuOpen"
(click)="$event.stopPropagation(); toggleMenu()"
>
<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="1"></circle><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg>
</button>
</div>
</div>
<!-- Menu -->
@if (menuOpen) {
<div
class="fixed z-50 bg-surface1 border border-border rounded-lg shadow-xl py-1 min-w-[220px] text-sm animate-in fade-in zoom-in-95 duration-100"
[style.left.px]="menuPos.left"
[style.top.px]="menuPos.top"
role="menu"
>
<!-- Language Selection -->
<div class="px-3 py-1.5 text-xs font-semibold text-muted-fg uppercase tracking-wider">Language</div>
<div class="max-h-48 overflow-y-auto custom-scrollbar">
<button
type="button"
class="w-full text-left px-4 py-2 hover:bg-surface2 transition-colors flex items-center justify-between group/item"
role="menuitem"
[class.bg-surface2]="!props.lang"
(click)="onLangChange('')"
>
<span>Plain text</span>
@if (!props.lang) { <span class="text-primary"></span> }
</button>
@for (lang of codeThemeService.getLanguages(); track lang) {
<button
type="button"
class="w-full text-left px-4 py-2 hover:bg-surface2 transition-colors flex items-center justify-between group/item"
role="menuitem"
[class.bg-surface2]="props.lang === lang"
(click)="onLangChange(lang)"
>
<span>{{ codeThemeService.getLanguageDisplay(lang) }}</span>
@if (props.lang === lang) { <span class="text-primary"></span> }
</button>
} }
</div> </div>
}
</div> <div class="h-px my-1 bg-border" role="separator"></div>
<!-- Theme Selection -->
<div class="px-3 py-1.5 text-xs font-semibold text-muted-fg uppercase tracking-wider">Theme</div>
<div class="max-h-48 overflow-y-auto custom-scrollbar">
@for (theme of codeThemeService.getThemes(); track theme.id) {
<button
type="button"
class="w-full text-left px-4 py-2 hover:bg-surface2 transition-colors flex items-center justify-between"
role="menuitem"
[class.bg-surface2]="props.theme === theme.id"
(click)="onThemeChange(theme.id)"
>
<span>{{ theme.name }}</span>
@if (props.theme === theme.id) { <span class="text-primary"></span> }
</button>
}
</div>
<div class="h-px my-1 bg-border" role="separator"></div>
<!-- Font Selection -->
<div class="px-3 py-1.5 text-xs font-semibold text-muted-fg uppercase tracking-wider">Font</div>
@for (font of availableFonts; track font.id) {
<button
type="button"
class="w-full text-left px-4 py-2 hover:bg-surface2 transition-colors flex items-center justify-between"
role="menuitem"
[class.bg-surface2]="props.font === font.id"
(click)="onFontChange(font.id)"
>
<span>{{ font.name }}</span>
@if (props.font === font.id) { <span class="text-primary"></span> }
</button>
}
<div class="h-px my-1 bg-border" role="separator"></div>
<!-- Options -->
<button
type="button"
class="w-full text-left px-4 py-2 hover:bg-surface2 transition-colors flex items-center justify-between"
role="menuitem"
(click)="toggleLineNumbers()"
>
<span>Show Line Numbers</span>
@if (props.showLineNumbers) { <span class="text-primary"></span> }
</button>
<button
type="button"
class="w-full text-left px-4 py-2 hover:bg-surface2 transition-colors flex items-center justify-between"
role="menuitem"
(click)="toggleWrap()"
>
<span>Word Wrap</span>
@if (props.enableWrap) { <span class="text-primary"></span> }
</button>
<div class="h-px my-1 bg-border" role="separator"></div>
<!-- Actions -->
<button
type="button"
class="w-full text-left px-4 py-2 hover:bg-surface2 transition-colors flex items-center gap-2"
role="menuitem"
(click)="copyToClipboard()"
>
<span>📋</span>
<span>{{ copied ? 'Copied!' : 'Copy to Clipboard' }}</span>
</button>
<button
type="button"
class="w-full text-left px-4 py-2 hover:bg-surface2 transition-colors flex items-center gap-2"
role="menuitem"
(click)="detectLanguage()"
>
<span>🔍</span>
<span>Auto-detect Language</span>
</button>
</div>
}
<!-- Code Editor/Preview -->
@if (!isCollapsed()) {
<div
class="rounded-b-md border border-t-0 border-border overflow-hidden transition-colors duration-200"
[ngClass]="getThemeClass()"
>
<div class="relative w-full">
<pre
class="overflow-auto max-h-[600px] text-sm leading-6 m-0 scrollbar-thin scrollbar-thumb-white/10 scrollbar-track-transparent"
[class.whitespace-pre-wrap]="props.enableWrap"
[style.font-family]="getFontFamily()"
[style.padding]="props.showLineNumbers ? '12px 12px 12px 3.5rem' : '12px'"
[style.min-height]="'3em'"
><code
#editable
contenteditable="true"
class="block min-h-[1.5em] focus:outline-none bg-transparent selection:bg-primary/30"
spellcheck="false"
autocorrect="off"
autocapitalize="off"
translate="no"
(input)="onInput($event)"
(paste)="onPaste($event)"
(blur)="onBlur()"
(keydown.tab)="onTab($event)"
></code></pre>
@if (props.showLineNumbers) {
<div
class="absolute top-0 left-0 py-3 w-10 text-right text-xs leading-6 text-neutral-500 pointer-events-none select-none border-r border-white/5 bg-black/5 h-full"
[style.font-family]="getFontFamily()"
aria-hidden="true"
>
@for (line of lineNumbers; track $index) {
<div class="pr-2">{{ line }}</div>
}
</div>
}
</div>
</div>
}
</div> </div>
` `,
styles: [`
.custom-scrollbar::-webkit-scrollbar { width: 6px; }
.custom-scrollbar::-webkit-scrollbar-track { background: transparent; }
.custom-scrollbar::-webkit-scrollbar-thumb { background-color: rgba(156, 163, 175, 0.5); border-radius: 3px; }
.custom-scrollbar::-webkit-scrollbar-thumb:hover { background-color: rgba(156, 163, 175, 0.7); }
`]
}) })
export class CodeBlockComponent implements AfterViewInit { export class CodeBlockComponent implements AfterViewInit {
@Input({ required: true }) block!: Block<CodeProps>; @Input({ required: true }) block!: Block<CodeProps>;
@Output() update = new EventEmitter<CodeProps>(); @Output() update = new EventEmitter<CodeProps>();
@ViewChild('editable') editable?: ElementRef<HTMLElement>;
readonly codeThemeService = inject(CodeThemeService); private _editable?: ElementRef<HTMLElement>;
@ViewChild('editable')
set editable(el: ElementRef<HTMLElement> | undefined) {
this._editable = el;
if (el?.nativeElement) {
// Re-hydrate the editable content from the current props
el.nativeElement.textContent = this.props.code || '';
}
}
get editable(): ElementRef<HTMLElement> | undefined {
return this._editable;
}
@ViewChild('moreBtn') moreBtn?: ElementRef<HTMLButtonElement>;
menuOpen = false;
menuPos = { left: 0, top: 0 };
copied = false;
// Cache for line numbers to avoid recalculation in template
private _lineNumbers: number[] = [];
private _lastCodeLength = -1;
readonly availableFonts = [
{ id: 'jetbrains', name: 'JetBrains Mono' },
{ id: 'fira', name: 'Fira Code' },
{ id: 'consolas', name: 'Consolas' },
{ id: 'monaco', name: 'Monaco' },
{ id: 'courier', name: 'Courier New' },
{ id: 'monospace', name: 'Monospace' }
];
private codeThemeService = inject(CodeThemeService);
private cdr = inject(ChangeDetectorRef);
get props(): CodeProps { get props(): CodeProps {
return this.block.props; return this.block.props;
} }
get lineNumbers(): number[] {
const code = this.props.code || '';
// Only recalculate if code length changed (simple heuristic, can be improved)
if (code.length !== this._lastCodeLength) {
const lines = code.split('\n');
this._lineNumbers = Array.from({ length: lines.length }, (_, i) => i + 1);
this._lastCodeLength = code.length;
}
return this._lineNumbers;
}
get lineCount(): number {
return this.lineNumbers.length;
}
ngAfterViewInit(): void { ngAfterViewInit(): void {
if (this.editable?.nativeElement) { // Auto-detect language on first load if not set
this.editable.nativeElement.textContent = this.props.code || ''; if (!this.props.lang && this.props.code) {
this.detectLanguageInternal();
} }
} }
onInput(event: Event): void { onInput(event: Event): void {
const target = event.target as HTMLElement; const target = event.target as HTMLElement;
this.update.emit({ ...this.props, code: target.textContent || '' }); const newCode = target.textContent || '';
// Only emit if changed
if (newCode !== this.props.code) {
this.update.emit({ ...this.props, code: newCode });
// Manually trigger change detection for line numbers since we're OnPush
// and the input event happens outside Angular's zone sometimes or we want immediate feedback
this._lastCodeLength = -1; // Force recalc
}
} }
onLangChange(event: Event): void { onPaste(event: ClipboardEvent): void {
const target = event.target as HTMLSelectElement; event.preventDefault();
this.update.emit({ ...this.props, lang: target.value });
const text = event.clipboardData?.getData('text/plain') || '';
if (!text) return;
const selection = window.getSelection();
if (!selection?.rangeCount) return;
selection.deleteFromDocument();
const range = selection.getRangeAt(0);
const textNode = document.createTextNode(text);
range.insertNode(textNode);
// Move cursor to end of pasted text
range.setStartAfter(textNode);
range.setEndAfter(textNode);
selection.removeAllRanges();
selection.addRange(range);
// Normalize content
if (this.editable?.nativeElement) {
const newCode = this.editable.nativeElement.textContent || '';
this.update.emit({ ...this.props, code: newCode });
this._lastCodeLength = -1; // Force recalc of line numbers
}
} }
onTab(event: KeyboardEvent): void {
event.preventDefault();
const tab = ' '; // 2 spaces
document.execCommand('insertText', false, tab);
}
onBlur(): void {
// Ensure sync on blur
if (this.editable?.nativeElement) {
const currentContent = this.editable.nativeElement.textContent || '';
if (currentContent !== this.props.code) {
this.update.emit({ ...this.props, code: currentContent });
}
}
}
// --- Menu Actions ---
onLangChange(lang: string): void {
this.closeMenu();
this.update.emit({ ...this.props, lang: lang || undefined });
}
onThemeChange(theme: string): void {
this.closeMenu();
this.update.emit({ ...this.props, theme });
}
onFontChange(font: string): void {
this.closeMenu();
this.update.emit({ ...this.props, font });
}
toggleLineNumbers(): void {
this.update.emit({ ...this.props, showLineNumbers: !this.props.showLineNumbers });
}
toggleWrap(): void {
this.update.emit({ ...this.props, enableWrap: !this.props.enableWrap });
}
toggleCollapse(): void {
this.update.emit({ ...this.props, collapsed: !this.props.collapsed });
}
isCollapsed(): boolean {
return !!this.props.collapsed;
}
// --- Helpers ---
getThemeClass(): string { getThemeClass(): string {
return this.codeThemeService.getThemeClass(this.props.theme); return this.codeThemeService.getThemeClass(this.props.theme || 'darcula');
} }
getLineNumbers(): number[] { getLanguageLabel(): string {
if (!this.props.showLineNumbers) return []; return this.codeThemeService.getLanguageDisplay(this.props.lang) || 'Code Block';
}
const lines = (this.props.code || '').split('\n'); getFontFamily(): string {
return Array.from({ length: lines.length }, (_, i) => i + 1); const fontMap: Record<string, string> = {
'jetbrains': '"JetBrains Mono", monospace',
'fira': '"Fira Code", monospace',
'consolas': 'Consolas, monospace',
'monaco': 'Monaco, monospace',
'courier': '"Courier New", monospace',
'monospace': 'monospace'
};
return fontMap[this.props.font || 'jetbrains'] || fontMap['jetbrains'];
}
async copyToClipboard(): Promise<void> {
try {
await navigator.clipboard.writeText(this.props.code || '');
this.copied = true;
this.cdr.markForCheck();
setTimeout(() => {
this.copied = false;
this.cdr.markForCheck();
this.closeMenu();
}, 1000);
} catch (err) {
console.error('Failed to copy to clipboard', err);
this.closeMenu();
}
}
detectLanguage(): void {
this.closeMenu();
this.detectLanguageInternal();
}
private detectLanguageInternal(): void {
const code = this.props.code || '';
if (!code.trim()) return;
// Simple regex-based detection
const patterns: Record<string, RegExp[]> = {
'javascript': [/\b(const|let|var|function|=>|console\.log)\b/, /\bimport\s+.*\s+from\s+['"]/, /\bexport\s+(default|const|function)\b/],
'typescript': [/\b(interface|type|enum|as|implements)\b/, /:\s*(string|number|boolean|any)\b/, /\bimport\s+.*\s+from\s+['"].*['"];?/],
'python': [/\b(def|class|import|from|if __name__|print)\b/, /:\s*$/, /\bself\b/],
'java': [/\b(public|private|protected|class|void|static)\b/, /\bSystem\.out\.println\b/, /\bpublic\s+static\s+void\s+main\b/],
'csharp': [/\b(using|namespace|public|private|class|void|static)\b/, /\bConsole\.WriteLine\b/, /\bvar\s+\w+\s*=/],
'cpp': [/\b(#include|using namespace|cout|cin|std::)\b/, /\bint\s+main\s*\(\s*\)\s*{/, /\bstd::/],
'go': [/\b(package|func|import|var|type|struct)\b/, /\bfmt\.Println\b/, /\bfunc\s+main\s*\(\s*\)\s*{/],
'rust': [/\b(fn|let|mut|impl|trait|pub)\b/, /\bprintln!\b/, /\bfn\s+main\s*\(\s*\)\s*{/],
'php': [/\b(<\?php|\$[a-zA-Z_]|echo|function)\b/, /\$this->/, /\bnamespace\b/],
'ruby': [/\b(def|class|module|require|puts|end)\b/, /\b@[a-zA-Z_]/, /\bdo\s*\|/],
'html': [/<\s*(html|head|body|div|span|p|a|img|script|style|meta)/, /<\/\s*(html|head|body|div|span|p)>/, /<!DOCTYPE\s+html>/i],
'css': [/\{[^}]*\}/, /[.#][a-zA-Z_-]+\s*\{/, /@(media|import|keyframes)\b/],
'json': [/^\s*\{[\s\S]*\}\s*$/, /"\w+"\s*:\s*/, /^\s*\[[\s\S]*\]\s*$/],
'xml': [/<?xml/, /<[a-zA-Z_][\w-]*>/, /<\/[a-zA-Z_][\w-]*>/],
'yaml': [/^[a-zA-Z_-]+:\s*/, /^\s*-\s+/, /^---/m],
'sql': [/\b(SELECT|FROM|WHERE|INSERT|UPDATE|DELETE|CREATE|TABLE|DATABASE)\b/i, /\bJOIN\b/i, /\bGROUP\s+BY\b/i],
'bash': [/^#!\/bin\/(ba)?sh/, /\b(echo|export|if|then|fi|for|do|done)\b/, /\$\{?\w+\}?/],
'dockerfile': [/^FROM\s+/, /^RUN\s+/, /^COPY\s+/, /^WORKDIR\s+/],
'markdown': [/^#+\s+/, /\[.*\]\(.*\)/, /^``` /m, /^\*\*.*\*\*$/m]
};
let bestMatch = '';
let bestScore = 0;
for (const [lang, regexes] of Object.entries(patterns)) {
let score = 0;
for (const regex of regexes) {
if (regex.test(code)) score++;
}
if (score > bestScore) {
bestScore = score;
bestMatch = lang;
}
}
if (bestMatch && bestScore > 0) {
this.update.emit({ ...this.props, lang: bestMatch });
}
}
// --- Menu Logic ---
toggleMenu(): void {
this.menuOpen = !this.menuOpen;
if (this.menuOpen) {
setTimeout(() => this.positionMenu(), 0);
}
}
closeMenu(): void {
this.menuOpen = false;
}
@HostListener('document:keydown.escape')
onEsc(): void {
this.closeMenu();
}
@HostListener('document:click', ['$event'])
onDocClick(ev: MouseEvent): void {
if (!this.menuOpen) return;
const target = ev.target as Node;
if (!this.moreBtn?.nativeElement.contains(target)) {
this.closeMenu();
}
}
@HostListener('window:resize')
@HostListener('window:scroll')
onWindowChange(): void {
if (this.menuOpen) this.positionMenu();
}
private positionMenu(): void {
const btn = this.moreBtn?.nativeElement;
if (!btn) return;
const r = btn.getBoundingClientRect();
// Default to bottom-left of button, but check for screen edges
let top = r.bottom + 6;
let left = r.right - 220; // 220px is approx menu width
// If menu would go off bottom, show above button
if (top + 300 > window.innerHeight) {
top = r.top - 300; // Approximate height
}
// Ensure not off-screen left
left = Math.max(8, left);
this.menuPos = { left, top };
this.cdr.markForCheck();
} }
} }

View File

@ -1,4 +1,25 @@
/* Code Block Themes for Nimbus Editor */ /* Code Block Themes for Nimbus Editor */
@import url('https://fonts.googleapis.com/css2?family=Fira+Code:wght@300;400;500;600&family=JetBrains+Mono:wght@300;400;500;600&family=Source+Code+Pro:wght@300;400;500;600&family=Ubuntu+Mono:wght@400;700&display=swap');
/* Scrollbar */
.custom-scrollbar::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.2);
}
/* Base styles */ /* Base styles */
.theme-default { .theme-default {
@ -143,3 +164,215 @@
.with-line-numbers { .with-line-numbers {
padding-left: 3.5rem !important; padding-left: 3.5rem !important;
} }
/* Font Families */
.font-fira {
font-family: 'Fira Code', 'Fira Mono', monospace;
}
.font-jetbrains {
font-family: 'JetBrains Mono', monospace;
}
.font-consolas {
font-family: 'Consolas', 'Monaco', monospace;
}
.font-source {
font-family: 'Source Code Pro', monospace;
}
.font-ubuntu {
font-family: 'Ubuntu Mono', monospace;
}
/* Highlight.js Syntax Coloring */
/* Keywords, Tags */
.hljs-keyword,
.hljs-selector-tag,
.hljs-literal,
.hljs-section,
.hljs-link {
font-weight: bold;
}
/* Darcula Theme Syntax */
.theme-darcula .hljs-keyword {
color: #cc7832;
}
.theme-darcula .hljs-string {
color: #6a8759;
}
.theme-darcula .hljs-number {
color: #6897bb;
}
.theme-darcula .hljs-comment {
color: #808080;
font-style: italic;
}
.theme-darcula .hljs-function {
color: #ffc66d;
}
.theme-darcula .hljs-class {
color: #a9b7c6;
}
.theme-darcula .hljs-variable {
color: #9876aa;
}
.theme-darcula .hljs-built_in {
color: #8888c6;
}
/* Default Theme Syntax */
.theme-default .hljs-keyword {
color: #0000ff;
}
.theme-default .hljs-string {
color: #a31515;
}
.theme-default .hljs-number {
color: #098658;
}
.theme-default .hljs-comment {
color: #008000;
font-style: italic;
}
.theme-default .hljs-function {
color: #795e26;
}
:host-context(.dark) .theme-default .hljs-keyword {
color: #569cd6;
}
:host-context(.dark) .theme-default .hljs-string {
color: #ce9178;
}
:host-context(.dark) .theme-default .hljs-number {
color: #b5cea8;
}
:host-context(.dark) .theme-default .hljs-comment {
color: #6a9955;
font-style: italic;
}
:host-context(.dark) .theme-default .hljs-function {
color: #dcdcaa;
}
/* Monokai Theme Syntax */
.theme-monokai .hljs-keyword {
color: #f92672;
}
.theme-monokai .hljs-string {
color: #e6db74;
}
.theme-monokai .hljs-number {
color: #ae81ff;
}
.theme-monokai .hljs-comment {
color: #75715e;
font-style: italic;
}
.theme-monokai .hljs-function {
color: #a6e22e;
}
.theme-monokai .hljs-class {
color: #a6e22e;
}
.theme-monokai .hljs-built_in {
color: #66d9ef;
}
/* Nord Theme Syntax */
.theme-nord .hljs-keyword {
color: #81a1c1;
}
.theme-nord .hljs-string {
color: #a3be8c;
}
.theme-nord .hljs-number {
color: #b48ead;
}
.theme-nord .hljs-comment {
color: #616e88;
font-style: italic;
}
.theme-nord .hljs-function {
color: #88c0d0;
}
.theme-nord .hljs-built_in {
color: #8fbcbb;
}
/* Zenburn Theme Syntax */
.theme-zenburn .hljs-keyword {
color: #f0dfaf;
}
.theme-zenburn .hljs-string {
color: #cc9393;
}
.theme-zenburn .hljs-number {
color: #8cd0d3;
}
.theme-zenburn .hljs-comment {
color: #7f9f7f;
font-style: italic;
}
.theme-zenburn .hljs-function {
color: #efef8f;
}
/* Generic styles for all themes */
.hljs-attr,
.hljs-attribute {
color: inherit;
opacity: 0.9;
}
.hljs-meta {
color: inherit;
opacity: 0.7;
font-style: italic;
}
.hljs-title {
font-weight: bold;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}

View File

@ -142,6 +142,8 @@ export interface CodeProps {
theme?: string; // Thème de coloration syntaxique theme?: string; // Thème de coloration syntaxique
showLineNumbers?: boolean; // Afficher les numéros de ligne showLineNumbers?: boolean; // Afficher les numéros de ligne
enableWrap?: boolean; // Activer le word wrap enableWrap?: boolean; // Activer le word wrap
collapsed?: boolean; // Mode replié/preview
font?: string; // Police sélectionnée
} }
export interface TableProps { export interface TableProps {

View File

@ -1214,8 +1214,8 @@ export class NotesListComponent {
/** /**
* Raccourci clavier pour gérer la sélection (Ctrl+A) * Raccourci clavier pour gérer la sélection (Ctrl+A)
*/ */
@HostListener('document:keydown.control.a', ['$event']) @HostListener('document:keydown.control.shift.a', ['$event'])
@HostListener('document:keydown.meta.a', ['$event']) @HostListener('document:keydown.meta.shift.a', ['$event'])
onSelectAllKeyboard(event: KeyboardEvent): void { onSelectAllKeyboard(event: KeyboardEvent): void {
// Seulement si le focus est dans notre composant // Seulement si le focus est dans notre composant
const target = event.target as HTMLElement; const target = event.target as HTMLElement;

View File

@ -908,8 +908,8 @@ export class PaginatedNotesListComponent implements OnInit, OnDestroy {
} }
// Keyboard shortcuts // Keyboard shortcuts
@HostListener('document:keydown.control.a', ['$event']) @HostListener('document:keydown.control.shift.a', ['$event'])
@HostListener('document:keydown.meta.a', ['$event']) @HostListener('document:keydown.meta.shift.a', ['$event'])
onSelectAllKeyboard(event: KeyboardEvent): void { onSelectAllKeyboard(event: KeyboardEvent): void {
event.preventDefault(); event.preventDefault();
this.selectAll(); this.selectAll();

View File

@ -7,9 +7,9 @@ import type { Note } from '../../types';
* Service centralisé pour gérer les raccourcis clavier globaux de l'application * Service centralisé pour gérer les raccourcis clavier globaux de l'application
* *
* Raccourcis implémentés: * Raccourcis implémentés:
* - Ctrl + Shift + A: Ouvrir/fermer la section AI Tools * - Ctrl + Alt + A: Ouvrir/fermer la section AI Tools
* - Ctrl + Alt + Enter: Répéter la dernière action IA * - Ctrl + Alt + Enter: Répéter la dernière action IA
* - Ctrl + A: Sélectionner toutes les notes (géré dans NotesListComponent) * - Ctrl + Shift + A: Sélectionner toutes les notes (géré dans NotesListComponent)
* - Escape: Clear la sélection (géré dans NotesListComponent) * - Escape: Clear la sélection (géré dans NotesListComponent)
*/ */
@Injectable({ @Injectable({
@ -77,8 +77,8 @@ export class KeyboardShortcutsService {
} }
} }
// Ctrl + Shift + A: Toggle AI Tools section // Ctrl + Alt + A: Toggle AI Tools section
if (event.ctrlKey && event.shiftKey && event.key === 'A') { if (event.ctrlKey && event.altKey && event.key === 'A') {
event.preventDefault(); event.preventDefault();
this.toggleAISection(); this.toggleAISection();
return; return;
@ -156,7 +156,7 @@ export class KeyboardShortcutsService {
getShortcutsList(): Array<{ keys: string; description: string }> { getShortcutsList(): Array<{ keys: string; description: string }> {
return [ return [
{ {
keys: 'Ctrl + Shift + A', keys: 'Ctrl + Alt + A',
description: 'Ouvrir la section AI Tools' description: 'Ouvrir la section AI Tools'
}, },
{ {
@ -164,7 +164,7 @@ export class KeyboardShortcutsService {
description: 'Répéter la dernière action IA sur les notes sélectionnées' description: 'Répéter la dernière action IA sur les notes sélectionnées'
}, },
{ {
keys: 'Ctrl + A', keys: 'Ctrl + Shift + A',
description: 'Sélectionner toutes les notes (dans la liste)' description: 'Sélectionner toutes les notes (dans la liste)'
}, },
{ {

View File

@ -8,10 +8,119 @@ documentModelFormat: "block-model-v1"
{ {
"id": "block_1763149113471_461xyut80", "id": "block_1763149113471_461xyut80",
"title": "Page Tests", "title": "Page Tests",
"blocks": [], "blocks": [
{
"id": "block_1763566926209_7jk1s960u",
"type": "list-item",
"props": {
"kind": "check",
"text": "",
"indent": 0,
"align": "left",
"checked": false
},
"meta": {
"createdAt": "2025-11-19T15:42:06.209Z",
"updatedAt": "2025-11-19T15:42:09.544Z"
}
},
{
"id": "block_1763566960476_yjhucz95t",
"type": "columns",
"props": {
"columns": [
{
"id": "mcc7x2c26",
"blocks": [
{
"id": "block_1763566940314_f267n73ai",
"type": "file",
"props": {
"meta": {
"id": "p54fbbnr3qgfw55r5kg44",
"name": "7086e8a9-2d58-4a7d-9002-dfb6512c71ec.png",
"size": 824753,
"mime": "image/png",
"ext": "png",
"kind": "image",
"createdAt": 1763566940314,
"url": "blob:http://localhost:3000/6d7be052-c4b6-4eca-9062-13de7531e1f1"
},
"ui": {
"expanded": false,
"layout": "list"
}
},
"meta": {
"createdAt": "2025-11-19T15:42:20.314Z",
"updatedAt": "2025-11-19T15:42:23.385Z"
}
}
],
"width": 55.87775063151858
},
{
"id": "vut1f3499",
"blocks": [
{
"id": "block_1763566944846_0h6qcf6vq",
"type": "bookmark",
"props": {
"url": "https://antigravity.google/",
"title": "Google Antigravity",
"description": "Google Antigravity - Build the new way",
"siteName": "Google Antigravity",
"imageUrl": "https://antigravity.google/assets/image/sitecards/sitecard-default.png",
"faviconUrl": "assets/image/antigravity-logo.png",
"loading": false,
"error": null
},
"meta": {
"createdAt": "2025-11-19T15:42:24.846Z",
"updatedAt": "2025-11-19T15:42:36.079Z"
}
}
],
"width": 44.12224936848142
}
]
},
"meta": {
"createdAt": "2025-11-19T15:42:40.476Z",
"updatedAt": "2025-11-19T15:42:48.755Z"
}
},
{
"id": "block_1763578576224_56c46xle0",
"type": "heading",
"props": {
"level": 1,
"text": "Allo !!"
},
"meta": {
"createdAt": "2025-11-19T18:56:16.224Z",
"updatedAt": "2025-11-19T18:56:23.318Z"
}
},
{
"id": "block_1763585769452_pzjmlyn2o",
"type": "code",
"props": {
"code": "import {\r\n Component,\r\n Input,\r\n Output,\r\n EventEmitter,\r\n ViewChild,\r\n ElementRef,\r\n AfterViewInit,\r\n inject,\r\n HostListener,\r\n ChangeDetectionStrategy,\r\n ChangeDetectorRef\r\n} from '@angular/core';\r\nimport { CommonModule } from '@angular/common';\r\nimport { FormsModule } from '@angular/forms';\r\nimport { Block, CodeProps } from '../../../core/models/block.model';\r\nimport { CodeThemeService } from '../../../services/code-theme.service';",
"lang": "",
"showLineNumbers": false,
"enableWrap": false,
"collapsed": false
},
"meta": {
"createdAt": "2025-11-19T20:56:09.452Z",
"updatedAt": "2025-11-19T20:56:34.206Z"
}
}
],
"meta": { "meta": {
"createdAt": "2025-11-14T19:38:33.471Z", "createdAt": "2025-11-14T19:38:33.471Z",
"updatedAt": "2025-11-19T03:48:08.101Z" "updatedAt": "2025-11-19T20:56:34.206Z"
} }
} }
``` ```