feat: add syntax highlighting and code block styling with theme support

This commit is contained in:
Bruno Charest 2025-10-21 08:34:46 -04:00
parent 062d743481
commit d788c9d267
75 changed files with 4783 additions and 830 deletions

321
QUICK_TEST_GUIDE.md Normal file
View File

@ -0,0 +1,321 @@
# 🧪 Guide de Test Rapide - Système de Théming
## Démarrage rapide
```bash
# 1. Installer les dépendances (si nécessaire)
npm install
# 2. Lancer l'application en mode dev
npm run dev
# 3. Ouvrir dans le navigateur
# http://localhost:4200
```
## Test manuel des thèmes
### Accéder aux paramètres
1. Ouvrir l'application
2. Naviguer vers **Parameters** / **Paramètres**
3. Section **Apparence** / **Appearance**
### Tester les modes
**Mode System** :
- Sélectionner "System"
- Changer le mode de votre OS (light/dark)
- L'application doit suivre automatiquement
**Mode Light** :
- Sélectionner "Light"
- Toute l'interface doit être en mode clair
**Mode Dark** :
- Sélectionner "Dark"
- Toute l'interface doit être en mode sombre
### Tester les thèmes
Pour chaque thème, vérifier en mode Light ET Dark (si applicable) :
#### 1. **Light** (thème par défaut clair)
- Mode : Light uniquement
- Palette : Blanc, gris, bleus
#### 2. **Dark** (thème par défaut sombre)
- Mode : Dark uniquement
- Palette : Bleu-gris foncé, texte clair
#### 3. **Obsidian**
- Mode Light : Beige chaud, texte anthracite
- Mode Dark : Gris très foncé, accents beiges
#### 4. **Nord**
- Mode Light : Blanc neigeux, palette nordique pastel
- Mode Dark : Bleu-gris arctique, palette nordique
#### 5. **Notion**
- Mode Light : Blanc pur, minimaliste
- Mode Dark : Noir profond, contraste élevé
#### 6. **GitHub**
- Mode Light : Style GitHub classique
- Mode Dark : GitHub Dimmed Dark
#### 7. **Discord**
- Mode Light : Gris très clair, violet Discord
- Mode Dark : Gris Discord, violet Discord
#### 8. **Monokai**
- Mode Light : Crème, accents atténués
- Mode Dark : Vert-gris foncé, palette Monokai
## Checklist de vérification
Pour chaque combinaison thème/mode, vérifier :
### 🏠 Page d'accueil / Liste
- [ ] **Sidebar gauche**
- Fond cohérent
- Items hover visibles
- Item actif distinct
- Badges de compteurs lisibles
- Séparateurs visibles
- [ ] **Topbar**
- Fond cohérent
- Barre de recherche lisible
- Placeholder visible
- Focus ring visible
- Icônes visibles
- [ ] **Liste des notes**
- Items lisibles
- Hover distinct
- Selected visible
- Path muted lisible
- Séparateurs visibles
### 📄 Page de détail
- [ ] **Contenu markdown**
- Titres (H1-H6) lisibles
- Paragraphes lisibles
- Liens visibles et hover
- Listes (ul, ol) lisibles
- Blockquotes distincts
- Code inline lisible
- Code blocks (fences) lisibles
- Tables lisibles (headers, zebra)
- HR visibles
- [ ] **Panneaux latéraux**
- TOC lisible
- Propriétés lisibles
- Tags lisibles et hover
### ✏️ Mode édition
- [ ] **Éditeur**
- Fond CodeMirror cohérent
- Texte lisible
- Sélection visible
- Gutters lisibles
- Cursor visible
- Coloration syntaxique cohérente
- [ ] **Toolbar**
- Boutons lisibles
- Hover visible
- Active visible
### 🎨 Composants UI
- [ ] **Modales**
- Fond cohérent
- Overlay visible
- Bordures visibles
- Boutons lisibles
- [ ] **Menus contextuels**
- Fond cohérent
- Items hover visibles
- Séparateurs visibles
- [ ] **Toasts**
- Fond cohérent
- Texte lisible
- Bordure visible
- Barre de progression visible
- [ ] **Chips/Tags**
- Fond cohérent
- Texte lisible
- Hover visible
- Selected distinct
- [ ] **Inputs**
- Fond cohérent
- Texte lisible
- Placeholder visible
- Focus ring visible
- Border visible
- [ ] **Buttons**
- Fond cohérent
- Texte lisible
- Hover visible
- Active visible
- Disabled visible
### 📱 Mobile
- [ ] **Bottom navigation**
- Fond cohérent
- Icônes visibles
- Active visible
- [ ] **Drawers/Sheets**
- Fond cohérent
- Overlay visible
### 🎯 Graphes (si applicable)
- [ ] **Graph view**
- Nœuds visibles
- Liens visibles
- Labels lisibles
- Légende lisible
## Tests de contraste
### Outils recommandés
1. **Chrome DevTools**
- F12 → Lighthouse → Accessibility
- Vérifier les contrastes automatiquement
2. **WebAIM Contrast Checker**
- https://webaim.org/resources/contrastchecker/
- Tester manuellement les combinaisons critiques
### Combinaisons à tester
Pour chaque thème :
- [ ] Texte principal / Fond principal (ratio ≥ 4.5:1)
- [ ] Texte muted / Fond principal (ratio ≥ 4.5:1)
- [ ] Liens / Fond principal (ratio ≥ 4.5:1)
- [ ] Boutons / Fond bouton (ratio ≥ 4.5:1)
## Tests de persistance
1. **Sélectionner un thème** (ex: Obsidian Dark)
2. **Recharger la page** (F5)
- ✅ Le thème doit être conservé
- ✅ Pas de flash de contenu (anti-FOUC)
3. **Fermer et rouvrir le navigateur**
- ✅ Le thème doit être conservé
4. **Tester en navigation privée**
- ✅ Thème par défaut (Light)
- ✅ Changement de thème fonctionne
- ⚠️ Non persisté après fermeture (comportement attendu)
## Tests de réactivité
1. **Ouvrir la page Parameters**
2. **Changer le mode** (Light → Dark)
- ✅ Changement immédiat sur toute la page
- ✅ Pas de rechargement nécessaire
3. **Changer le thème** (Obsidian → Nord)
- ✅ Changement immédiat sur toute la page
- ✅ Pas de rechargement nécessaire
4. **Ouvrir plusieurs onglets**
- ✅ Changement dans un onglet se reflète dans les autres (après focus)
## Debugging
### Vérifier le thème actif
Ouvrir la console (F12) et taper :
```javascript
// Vérifier la classe dark
document.documentElement.classList.contains('dark')
// Vérifier le thème
document.documentElement.getAttribute('data-theme')
// Vérifier les préférences stockées
JSON.parse(localStorage.getItem('obsiviewer.preferences.v1'))
```
### Forcer un thème manuellement
```javascript
// Forcer dark mode
document.documentElement.classList.add('dark')
// Forcer un thème
document.documentElement.setAttribute('data-theme', 'nord')
// Recharger pour restaurer les préférences
location.reload()
```
### Inspecter les tokens CSS
```javascript
// Obtenir la valeur d'un token
getComputedStyle(document.documentElement).getPropertyValue('--bg-main')
// Lister tous les tokens
Array.from(document.styleSheets)
.flatMap(sheet => Array.from(sheet.cssRules))
.filter(rule => rule.style?.getPropertyValue('--bg'))
```
## Rapport de bugs
Si vous trouvez un problème :
1. **Capturer** :
- Screenshot du problème
- Thème actif (nom + mode)
- Navigateur + version
- Résolution d'écran
2. **Reproduire** :
- Étapes pour reproduire
- Comportement attendu
- Comportement observé
3. **Vérifier** :
- Console (erreurs JS ?)
- Network (ressources manquantes ?)
- Computed styles (tokens appliqués ?)
## Checklist finale
Avant de valider le système de théming :
- [ ] Les 14 combinaisons sont testées
- [ ] Tous les composants UI sont cohérents
- [ ] Les contrastes respectent WCAG AA
- [ ] La persistance fonctionne
- [ ] Pas de flash au chargement (anti-FOUC)
- [ ] Les transitions sont fluides
- [ ] Le mode System fonctionne
- [ ] Mobile est testé
- [ ] Aucune couleur hardcodée visible
- [ ] Documentation à jour
---
**Bon test ! 🎨**

233
THEMING_STATUS.md Normal file
View File

@ -0,0 +1,233 @@
# 🎨 État du Système de Théming - ObsiViewer
**Date** : 20 octobre 2025
**Statut** : 🟢 Infrastructure complète - Refactoring en cours
---
## ✅ Complété
### 1. Architecture de base (100%)
- ✅ **themes.css** : 14 variantes (7 thèmes × 2 modes) avec tokens CSS complets
- Light, Dark, Obsidian, Nord, Notion, GitHub, Discord, Monokai
- Chaque thème a une version light ET dark
- Tokens : backgrounds, text, borders, brand, status, UI, editor, shadows
- ✅ **tailwind.config.js** : Configuration complète
- Mapping de tous les tokens CSS vers classes Tailwind
- 40+ tokens mappés (bg, fg, surface1/2, primary, accent, etc.)
- Support des variantes (hover, focus, dark)
- ✅ **ThemeService** : Service Angular fonctionnel
- Gestion mode (system/light/dark) + thème
- Persistance localStorage
- Observable pour réactivité
- Application sur `<html>` (classe `.dark` + `data-theme`)
- ✅ **codemirror-themes.ts** : 14 variantes CodeMirror 6
- Fonction `cm6ThemeFor(themeId, mode)` complète
- Chaque thème a sa palette de coloration syntaxique
- Cohérence avec les tokens CSS
- ✅ **Anti-FOUC** : Script inline dans `index.html`
- Lecture des préférences avant Angular
- Application immédiate du thème
- Pas de flash de contenu non stylé
### 2. Refactoring automatisé (68%)
- ✅ **Script `refactor-colors.mjs`** créé
- ✅ **66 fichiers** mis à jour automatiquement
- ✅ **232 occurrences** de couleurs hardcodées remplacées
- `bg-slate-*``bg-surface1/2`
- `text-gray-*``text-main/muted`
- `border-slate-*``border-border`
- Variantes dark:, hover:, focus: gérées
### 3. Documentation
- ✅ **THEMING_IMPLEMENTATION.md** : Documentation technique complète
- ✅ **THEMING_STATUS.md** : Ce fichier (état du projet)
---
## 🔄 En cours
### Refactoring manuel (109 occurrences restantes)
**Fichiers prioritaires** (14+ matches) :
1. `app-sidebar-drawer.component.ts` (14 matches)
2. `graph-options-panel.component.ts` (11 matches)
3. `app-shell-nimbus.component.ts` (9 matches)
**Fichiers moyens** (4-8 matches) :
- `quick-links.component.ts` (8)
- `nimbus-sidebar.component.ts` (8)
- `markdown-playground.component.ts` (6)
- `add-bookmark-modal.component.html` (6)
- `search-query-assistant.component.ts` (5)
- `filters-section.component.ts` (4)
**Fichiers mineurs** (1-3 matches) :
- 23 fichiers avec 1-3 occurrences chacun
**Types de cas restants** :
- Styles conditionnels complexes
- Classes dynamiques avec logique métier
- Variantes rares (active:, disabled:, group-hover:)
- Couleurs dans des contextes spécifiques (graphes, visualisations)
---
## ⏳ À faire
### 1. Refactoring manuel (Priorité : Haute)
**Approche recommandée** :
1. Traiter les 3 fichiers prioritaires (34 matches)
2. Traiter les 7 fichiers moyens (47 matches)
3. Traiter les 23 fichiers mineurs (28 matches)
**Commande pour identifier** :
```bash
grep -rn "bg-slate-\|bg-gray-\|text-slate-\|text-gray-" src/ | wc -l
```
### 2. Tests de la matrice complète (Priorité : Haute)
**14 combinaisons à tester** :
| Thème | Light | Dark |
|-----------|-------|------|
| Light | ✅ | N/A |
| Dark | N/A | ✅ |
| Obsidian | ⏳ | ⏳ |
| Nord | ⏳ | ⏳ |
| Notion | ⏳ | ⏳ |
| GitHub | ⏳ | ⏳ |
| Discord | ⏳ | ⏳ |
| Monokai | ⏳ | ⏳ |
**Checklist par combinaison** :
- [ ] Sidebar (fond, hover, actif, badges)
- [ ] Topbar + search (input, focus, placeholder)
- [ ] Liste notes (items, hover, selected)
- [ ] Page détail (titres, liens, code, tables)
- [ ] Mode édition (inputs, buttons, CodeMirror)
- [ ] Panneaux (TOC, propriétés, tags)
- [ ] Modales (fond, overlay, bordure)
- [ ] Menus contextuels
- [ ] Toasts
- [ ] Tables (headers, zebra, hover)
- [ ] Chips/Tags (fond, hover, selected)
- [ ] Scrollbars
- [ ] Page Parameters
- [ ] Mobile (bottom nav, drawers)
### 3. Validation contrastes (Priorité : Moyenne)
**Outils** :
- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/)
- Chrome DevTools (Lighthouse)
**Critères WCAG AA** :
- Texte normal : ratio ≥ 4.5:1
- Texte large : ratio ≥ 3:1
- Éléments UI : ratio ≥ 3:1
**À vérifier pour chaque thème** :
- [ ] Texte principal / fond
- [ ] Texte muted / fond
- [ ] Liens / fond
- [ ] Boutons / fond
- [ ] Bordures / fond
### 4. Vérification finale (Priorité : Basse)
- [ ] Aucune couleur hardcodée restante (grep complet)
- [ ] Tous les composants utilisent les tokens
- [ ] Documentation à jour
- [ ] Screenshots comparatifs (avant/après)
- [ ] Guide utilisateur pour la page Parameters
---
## 📊 Métriques
### Progression globale
```
Infrastructure : ████████████████████ 100% (5/5)
Refactoring : █████████████░░░░░░░ 68% (232/341)
Tests : ░░░░░░░░░░░░░░░░░░░░ 0% (0/14)
Documentation : ████████████████████ 100% (2/2)
---
TOTAL : ████████████░░░░░░░░ 67%
```
### Détails refactoring
| Catégorie | Avant | Après | Restant |
|--------------------|-------|-------|---------|
| Couleurs hardcodées| 341 | 109 | 109 |
| Fichiers touchés | 0 | 66 | ~32 |
| Tokens créés | 0 | 40+ | - |
| Thèmes complets | 2 | 14 | - |
---
## 🚀 Commandes rapides
```bash
# Refactoring automatique (réexécuter si besoin)
node scripts/refactor-colors.mjs
# Rechercher couleurs hardcodées
grep -rn "bg-slate-\|bg-gray-\|text-slate-\|text-gray-" src/
# Lancer l'application
npm run dev
# Tests E2E
npm run test:e2e
# Build production
npm run build
```
---
## 🎯 Objectif final
**Application entièrement thémable** :
- ✅ 7 thèmes × 2 modes = 14 combinaisons
- ✅ Aucune couleur hardcodée
- ✅ Cohérence visuelle totale
- ✅ Accessibilité WCAG AA
- ✅ Performance optimale (CSS variables)
- ✅ Expérience utilisateur fluide (anti-FOUC)
---
## 📝 Notes
### Points d'attention
1. **CodeMirror 6** : Les thèmes sont appliqués dynamiquement via `ThemeService.getCodeMirrorExtensions()`
2. **Persistance** : Clé localStorage `obsiviewer.preferences.v1`
3. **Réactivité** : Tous les composants doivent s'abonner à `ThemeService.onPrefs$` si nécessaire
4. **Mobile** : Vérifier les overlays et bottom sheets dans chaque thème
5. **Graphes** : Les visualisations D3/Canvas peuvent nécessiter un traitement spécial
### Améliorations futures
- [ ] Thème personnalisé (éditeur de couleurs)
- [ ] Import/export de thèmes
- [ ] Prévisualisation live dans Parameters
- [ ] Thèmes communautaires
- [ ] Mode haute contraste dédié
---
**Dernière mise à jour** : 20 octobre 2025, 17:05 UTC-04:00

View File

@ -25,6 +25,7 @@
"node_modules/angular-calendar/css/angular-calendar.css", "node_modules/angular-calendar/css/angular-calendar.css",
"src/styles/tokens.css", "src/styles/tokens.css",
"src/styles/components.css", "src/styles/components.css",
"src/styles/syntax.css",
"src/styles.css" "src/styles.css"
], ],
"assets": [ "assets": [

View File

@ -0,0 +1,239 @@
# Système de Théming ObsiViewer - Documentation
## Vue d'ensemble
Système de théming double dimension : **7 thèmes × 2 modes = 14 combinaisons**
- **Modes** : Light, Dark
- **Thèmes** : Light (default), Dark (default), Obsidian, Nord, Notion, GitHub, Discord, Monokai
## Architecture
### 1. Tokens CSS (`src/styles/themes.css`)
Tous les thèmes définissent un ensemble cohérent de tokens CSS :
```css
/* Backgrounds */
--bg, --bg-main, --bg-muted
--card, --card-bg, --elevated
--sidebar-bg, --surface-1, --surface-2
/* Text */
--fg, --text-main, --text-muted, --muted
/* Borders */
--border
/* Brand & Accents */
--primary, --brand, --brand-700, --brand-800
--secondary, --accent
/* Status */
--success, --warning, --danger, --info
/* UI Elements */
--chip-bg, --link, --link-hover, --ring
/* Editor (CodeMirror 6) */
--editor-bg, --editor-fg, --editor-selection
--editor-gutter-bg, --editor-gutter-fg, --editor-cursor
/* Shadows */
--shadow-color, --scrollbar-thumb
```
### 2. Configuration Tailwind (`tailwind.config.js`)
Tous les tokens sont mappés dans Tailwind :
```javascript
colors: {
bg: 'var(--bg)',
'bg-main': 'var(--bg-main)',
fg: 'var(--fg)',
surface1: 'var(--surface-1)',
primary: 'var(--primary)',
// ... etc
}
```
### 3. ThemeService (`src/app/core/services/theme.service.ts`)
Service central qui gère :
- Mode : `'system' | 'light' | 'dark'`
- Thème : `'light' | 'dark' | 'obsidian' | 'nord' | 'notion' | 'github' | 'discord' | 'monokai'`
- Application sur `<html>` : classe `.dark` + attribut `data-theme`
- Persistance dans localStorage
- Observable `onPrefs$` pour réactivité
### 4. CodeMirror 6 (`src/app/core/codemirror-themes.ts`)
Fonction `cm6ThemeFor(themeId, mode)` qui retourne les extensions CM6 appropriées pour chaque combinaison thème/mode.
### 5. Anti-FOUC (`index.html`)
Script inline qui lit les préférences et applique le thème avant le chargement d'Angular :
```javascript
(function () {
const prefs = JSON.parse(localStorage.getItem('obsiviewer.preferences.v1') || '{}');
const isDark = prefs.mode === 'dark' || (prefs.mode === 'system' && prefersDark);
html.classList.toggle('dark', isDark);
html.setAttribute('data-theme', prefs.theme || 'light');
})();
```
## Matrice des Thèmes
### Light (Default)
- **Light mode** : Blanc pur, texte gris foncé, accents bleus
- **Dark mode** : N/A (thème light uniquement)
### Dark (Default)
- **Light mode** : N/A (thème dark uniquement)
- **Dark mode** : Fond bleu-gris foncé, texte clair, accents bleus clairs
### Obsidian
- **Light mode** : Beige chaud (#fafaf8), texte anthracite, accents terre
- **Dark mode** : Gris très foncé (#1e1e1e), texte clair, accents beiges
### Nord
- **Light mode** : Blanc neigeux (#eceff4), texte bleu-gris, palette nordique pastel
- **Dark mode** : Bleu-gris arctique (#2e3440), texte givré, palette nordique
### Notion
- **Light mode** : Blanc pur, texte charbon, minimaliste
- **Dark mode** : Noir profond (#171717), texte ivoire, contraste élevé
### GitHub
- **Light mode** : Blanc, texte GitHub (#24292f), palette GitHub
- **Dark mode** : Dimmed dark (#0d1117), palette GitHub dark
### Discord
- **Light mode** : Gris très clair (#f6f7f9), texte foncé, violet Discord
- **Dark mode** : Gris Discord (#2b2d31), texte clair, violet Discord
### Monokai
- **Light mode** : Crème (#fbfbf7), texte foncé, accents verts/oranges atténués
- **Dark mode** : Vert-gris foncé (#272822), texte clair, palette Monokai classique
## Utilisation dans les Composants
### Classes Tailwind recommandées
```html
<!-- Backgrounds -->
<div class="bg-card">Carte</div>
<div class="bg-surface1">Surface niveau 1</div>
<div class="bg-surface2">Surface niveau 2</div>
<!-- Text -->
<p class="text-main">Texte principal</p>
<p class="text-muted">Texte secondaire</p>
<!-- Borders -->
<div class="border border-border">Bordure</div>
<!-- Interactive -->
<button class="bg-primary text-white hover:bg-brand-700">
Action
</button>
<!-- Links -->
<a class="text-link hover:text-link-hover">Lien</a>
```
### ⚠️ À ÉVITER
```html
<!-- ❌ Couleurs hardcodées -->
<div class="bg-slate-100 text-gray-900">...</div>
<div class="dark:bg-gray-800">...</div>
<!-- ✅ Utiliser les tokens -->
<div class="bg-surface1 text-main">...</div>
```
## Tests
### Checklist de validation
Pour chaque combinaison (14 au total) :
- [ ] **Sidebar** : Fond, hover, items actifs, badges
- [ ] **Topbar** : Barre de recherche, focus, placeholder
- [ ] **Liste des notes** : Items, hover, selected, path muted
- [ ] **Page détail** : Titres, liens, listes, blockquotes, code, tables
- [ ] **Mode édition** : Inputs, buttons, toolbars, CodeMirror
- [ ] **Panneaux** : TOC, propriétés, tags
- [ ] **Modales** : Fond, bordure, overlay
- [ ] **Menus contextuels** : Dropdown, tooltips
- [ ] **Toasts** : Fond, bordure, barre de progression
- [ ] **Tables** : Headers, zebra, hover
- [ ] **Chips/Tags** : Fond, texte, bordure, hover, selected
- [ ] **Scrollbars** : Thumb, track
- [ ] **Page Parameters** : Contrôles mode/thème
- [ ] **Mobile** : Bottom nav, sheets, drawers
### Contraste
Tous les thèmes doivent respecter WCAG AA :
- Texte normal : ratio ≥ 4.5:1
- Texte large : ratio ≥ 3:1
## Refactoring effectué
### Automatisé (script `refactor-colors.mjs`)
- ✅ 66 fichiers mis à jour automatiquement
- ✅ Remplacement de 232+ occurrences de couleurs hardcodées
- ✅ Gestion des variantes : hover, focus, dark
### Restant (109 occurrences)
Cas complexes nécessitant révision manuelle :
- Composants avec logique conditionnelle
- Styles inline dynamiques
- Cas spécifiques métier
## Commandes utiles
```bash
# Lancer le refactoring automatique
node scripts/refactor-colors.mjs
# Rechercher les couleurs hardcodées restantes
grep -r "bg-slate-\|bg-gray-\|text-slate-\|text-gray-" src/
# Lancer l'application en mode dev
npm run dev
# Tests E2E
npm run test:e2e
```
## Prochaines étapes
1. ✅ Créer themes.css avec 14 variantes
2. ✅ Configurer Tailwind
3. ✅ Mettre à jour codemirror-themes.ts
4. ✅ Refactoring automatisé (68% des couleurs)
5. 🔄 Refactoring manuel des 109 occurrences restantes
6. ⏳ Tests matrice complète
7. ⏳ Validation contrastes
8. ⏳ Documentation utilisateur
## Notes techniques
- **Persistance** : `localStorage` clé `obsiviewer.preferences.v1`
- **Réactivité** : Observable `ThemeService.onPrefs$`
- **SSR-safe** : Vérifications `typeof window !== 'undefined'`
- **Performance** : Transitions CSS 150ms
- **Accessibilité** : `color-scheme` CSS pour scrollbars natives
## Ressources
- [Tailwind CSS Variables](https://tailwindcss.com/docs/customizing-colors#using-css-variables)
- [CodeMirror 6 Theming](https://codemirror.net/docs/ref/#view.EditorView^theme)
- [WCAG Contrast Guidelines](https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html)

95
docs/ui-theming.md Normal file
View File

@ -0,0 +1,95 @@
# UI Theming and CM6 Highlight
## Theme Variables
- **Global tokens** (in `src/styles.css`):
- `--color-accent` main accent color (per-theme override via `[data-theme]`).
- `--cm-hl-bg` computed background for inline highlights (theme dependent).
- `--cm-hl-br` border radius for highlights (default 3px).
- **Button tokens**:
- `--btn-radius`, `--btn-padding-y`, `--btn-padding-x`, `--btn-font`, `--btn-shadow`, `--btn-ring`, `--btn-speed`.
- `--btn-bg`, `--btn-fg`, `--btn-bg-hover`, `--btn-bg-active`, `--btn-outline`.
### Defaults
```css
:root {
--color-accent: #3b82f6;
--cm-hl-bg: color-mix(in srgb, var(--color-accent) 28%, transparent);
--cm-hl-br: 3px;
}
[data-theme="dark"], .dark {
--cm-hl-bg: color-mix(in srgb, var(--color-accent) 22%, transparent);
}
[data-theme="nimbus"] { --color-accent: #7c3aed; }
[data-theme="emerald"] { --color-accent: #10b981; }
```
## CodeMirror 6 Highlight Extensions
Files in `src/app/shared/editor/extensions/`:
- `highlight-occurrences.extension.ts` inline dynamic occurrences via regex.
- `ranged-highlights.extension.ts` ranged highlights via `StateEffect`/`StateField`.
- `markdown-theme-highlight.extension.ts` composes ranged + Markdown `HighlightStyle` and facet-driven CSS variable.
### CSS class
- `.cm-md-highlight { background: var(--cm-hl-bg); border-radius: var(--cm-hl-br, 3px); transition: background var(--btn-speed) ease; }`
## Angular Integration
Service: `EditorHighlightService` (`src/app/shared/editor/editor-highlight.service.ts`)
- `extensions: Extension[]` base markdown theme highlight extensions to add at editor creation.
- `occurrencesExtension(pattern)` build occurrences extension.
- `applyOccurrences(view, compartment, pattern)` reconfigure a `Compartment` with occurrences.
- `setRanges(view, ranges)` / `clearRanges(view)` ranged highlights via effects.
- `facet()` access the facet to optionally override `--cm-hl-bg` per-view.
Example inside editor component:
```ts
// compartments
occurrencesCompartment = new Compartment();
highlightFacetCompartment = new Compartment();
// in EditorState.create extensions
...highlightService.extensions,
occurrencesCompartment.of([]),
highlightFacetCompartment.of([]),
// API usage
highlightOccurrences("TODO");
setHighlights([{ from: 10, to: 24 }]);
clearHighlights();
// optional color override
view.dispatch({ effects: highlightFacetCompartment.reconfigure([ highlightService.facet().of('color-mix(in srgb, var(--color-accent) 40%, transparent)') ])});
```
Live theme changes are applied by reconfiguring the theme compartment and calling `EditorView.requestMeasure(...)` to refresh decorations that rely on CSS variables.
## Button Utilities
Classes in `src/styles.css`:
- Base: `btn`
- Variants: `btn-solid`, `btn-outline`, `btn-ghost`
- Sizes: `btn-sm`, `btn-md`, `btn-lg`
Example:
```html
<button class="btn btn-solid btn-sm">Save</button>
<button class="btn btn-outline btn-sm">Cancel</button>
<button class="btn btn-ghost btn-sm" aria-label="Close"></button>
```
## Accessibility
- Buttons use focus ring via `--btn-outline`.
- Ensure icon-only buttons have `aria-label`.
## Performance Notes
- Occurrence highlights compute decorations only over visible ranges; cost is O(visibleText) per viewport update.
- Ranged highlights use a `StateField` and `tr.changes.mapPos` to keep ranges aligned after edits; updates are incremental.
- For large docs (≥5k lines), prefer ranged highlights for bulk operations; occurrences are fine for quick regex patterns like `TODO`.

View File

@ -1,9 +1,33 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" data-theme="light"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ObsiWatcher - Obsidian Vault Viewer</title> <title>ObsiViewer - Obsidian Vault Viewer</title>
<script>
(function () {
try {
const key = 'obsiviewer.preferences.v1';
const raw = localStorage.getItem(key);
const prefs = raw ? JSON.parse(raw) : null;
const html = document.documentElement;
function apply(mode, theme) {
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
const dark = mode === 'system' ? prefersDark : (mode === 'dark');
html.classList.toggle('dark', !!dark);
if (theme) html.setAttribute('data-theme', theme);
}
if (prefs && prefs.mode && prefs.theme) {
apply(prefs.mode, prefs.theme);
} else {
// défaut : system + light tokens
apply('system', 'light');
}
} catch (e) { /* no-op */ }
})();
</script>
<script type="importmap"> <script type="importmap">
{ {
"imports": { "imports": {

214
package-lock.json generated
View File

@ -23,7 +23,19 @@
"@angular/router": "20.3.2", "@angular/router": "20.3.2",
"@codemirror/autocomplete": "^6.19.0", "@codemirror/autocomplete": "^6.19.0",
"@codemirror/commands": "^6.9.0", "@codemirror/commands": "^6.9.0",
"@codemirror/lang-markdown": "^6.4.0", "@codemirror/lang-css": "^6.0.1",
"@codemirror/lang-go": "^6.0.1",
"@codemirror/lang-html": "^6.0.1",
"@codemirror/lang-java": "^6.0.1",
"@codemirror/lang-javascript": "^6.0.1",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-markdown": "^6.0.1",
"@codemirror/lang-php": "^6.0.1",
"@codemirror/lang-python": "^6.0.1",
"@codemirror/lang-rust": "^6.0.1",
"@codemirror/lang-sql": "^6.0.1",
"@codemirror/lang-xml": "^6.0.1",
"@codemirror/lang-yaml": "^6.0.1",
"@codemirror/language": "^6.11.3", "@codemirror/language": "^6.11.3",
"@codemirror/legacy-modes": "^6.5.2", "@codemirror/legacy-modes": "^6.5.2",
"@codemirror/lint": "^6.9.0", "@codemirror/lint": "^6.9.0",
@ -2692,6 +2704,19 @@
"@lezer/css": "^1.1.7" "@lezer/css": "^1.1.7"
} }
}, },
"node_modules/@codemirror/lang-go": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@codemirror/lang-go/-/lang-go-6.0.1.tgz",
"integrity": "sha512-7fNvbyNylvqCphW9HD6WFnRpcDjr+KXX/FgqXy5H5ZS0eC5edDljukm/yNgYkwTsgp2busdod50AOTIy6Jikfg==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.6.0",
"@codemirror/state": "^6.0.0",
"@lezer/common": "^1.0.0",
"@lezer/go": "^1.0.0"
}
},
"node_modules/@codemirror/lang-html": { "node_modules/@codemirror/lang-html": {
"version": "6.4.11", "version": "6.4.11",
"resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.11.tgz", "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.11.tgz",
@ -2709,6 +2734,16 @@
"@lezer/html": "^1.3.12" "@lezer/html": "^1.3.12"
} }
}, },
"node_modules/@codemirror/lang-java": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@codemirror/lang-java/-/lang-java-6.0.2.tgz",
"integrity": "sha512-m5Nt1mQ/cznJY7tMfQTJchmrjdjQ71IDs+55d1GAa8DGaB8JXWsVCkVT284C3RTASaY43YknrK2X3hPO/J3MOQ==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@lezer/java": "^1.0.0"
}
},
"node_modules/@codemirror/lang-javascript": { "node_modules/@codemirror/lang-javascript": {
"version": "6.2.4", "version": "6.2.4",
"resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz", "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz",
@ -2724,6 +2759,16 @@
"@lezer/javascript": "^1.0.0" "@lezer/javascript": "^1.0.0"
} }
}, },
"node_modules/@codemirror/lang-json": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.2.tgz",
"integrity": "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@lezer/json": "^1.0.0"
}
},
"node_modules/@codemirror/lang-markdown": { "node_modules/@codemirror/lang-markdown": {
"version": "6.4.0", "version": "6.4.0",
"resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.4.0.tgz", "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.4.0.tgz",
@ -2739,6 +2784,85 @@
"@lezer/markdown": "^1.0.0" "@lezer/markdown": "^1.0.0"
} }
}, },
"node_modules/@codemirror/lang-php": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@codemirror/lang-php/-/lang-php-6.0.2.tgz",
"integrity": "sha512-ZKy2v1n8Fc8oEXj0Th0PUMXzQJ0AIR6TaZU+PbDHExFwdu+guzOA4jmCHS1Nz4vbFezwD7LyBdDnddSJeScMCA==",
"license": "MIT",
"dependencies": {
"@codemirror/lang-html": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@lezer/common": "^1.0.0",
"@lezer/php": "^1.0.0"
}
},
"node_modules/@codemirror/lang-python": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.2.1.tgz",
"integrity": "sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.3.2",
"@codemirror/language": "^6.8.0",
"@codemirror/state": "^6.0.0",
"@lezer/common": "^1.2.1",
"@lezer/python": "^1.1.4"
}
},
"node_modules/@codemirror/lang-rust": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@codemirror/lang-rust/-/lang-rust-6.0.2.tgz",
"integrity": "sha512-EZaGjCUegtiU7kSMvOfEZpaCReowEf3yNidYu7+vfuGTm9ow4mthAparY5hisJqOHmJowVH3Upu+eJlUji6qqA==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@lezer/rust": "^1.0.0"
}
},
"node_modules/@codemirror/lang-sql": {
"version": "6.10.0",
"resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.10.0.tgz",
"integrity": "sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@codemirror/lang-xml": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.1.0.tgz",
"integrity": "sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.4.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/common": "^1.0.0",
"@lezer/xml": "^1.0.0"
}
},
"node_modules/@codemirror/lang-yaml": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/@codemirror/lang-yaml/-/lang-yaml-6.1.2.tgz",
"integrity": "sha512-dxrfG8w5Ce/QbT7YID7mWZFKhdhsaTNOYjOkSIMt1qmC4VQnXSDSYVHHHn8k6kJUfIhtLo8t1JJgltlxWdsITw==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.2.0",
"@lezer/lr": "^1.0.0",
"@lezer/yaml": "^1.0.0"
}
},
"node_modules/@codemirror/language": { "node_modules/@codemirror/language": {
"version": "6.11.3", "version": "6.11.3",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz", "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz",
@ -4075,6 +4199,17 @@
"@lezer/lr": "^1.3.0" "@lezer/lr": "^1.3.0"
} }
}, },
"node_modules/@lezer/go": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@lezer/go/-/go-1.0.1.tgz",
"integrity": "sha512-xToRsYxwsgJNHTgNdStpcvmbVuKxTapV0dM0wey1geMMRc9aggoVyKgzYp41D2/vVOx+Ii4hmE206kvxIXBVXQ==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.3.0"
}
},
"node_modules/@lezer/highlight": { "node_modules/@lezer/highlight": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.2.tgz", "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.2.tgz",
@ -4095,6 +4230,17 @@
"@lezer/lr": "^1.0.0" "@lezer/lr": "^1.0.0"
} }
}, },
"node_modules/@lezer/java": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@lezer/java/-/java-1.1.3.tgz",
"integrity": "sha512-yHquUfujwg6Yu4Fd1GNHCvidIvJwi/1Xu2DaKl/pfWIA2c1oXkVvawH3NyXhCaFx4OdlYBVX5wvz2f7Aoa/4Xw==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@lezer/javascript": { "node_modules/@lezer/javascript": {
"version": "1.5.4", "version": "1.5.4",
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz", "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz",
@ -4106,6 +4252,17 @@
"@lezer/lr": "^1.3.0" "@lezer/lr": "^1.3.0"
} }
}, },
"node_modules/@lezer/json": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz",
"integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@lezer/lr": { "node_modules/@lezer/lr": {
"version": "1.4.2", "version": "1.4.2",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz",
@ -4125,6 +4282,61 @@
"@lezer/highlight": "^1.0.0" "@lezer/highlight": "^1.0.0"
} }
}, },
"node_modules/@lezer/php": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@lezer/php/-/php-1.0.5.tgz",
"integrity": "sha512-W7asp9DhM6q0W6DYNwIkLSKOvxlXRrif+UXBMxzsJUuqmhE7oVU+gS3THO4S/Puh7Xzgm858UNaFi6dxTP8dJA==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.1.0"
}
},
"node_modules/@lezer/python": {
"version": "1.1.18",
"resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.18.tgz",
"integrity": "sha512-31FiUrU7z9+d/ElGQLJFXl+dKOdx0jALlP3KEOsGTex8mvj+SoE1FgItcHWK/axkxCHGUSpqIHt6JAWfWu9Rhg==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@lezer/rust": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@lezer/rust/-/rust-1.0.2.tgz",
"integrity": "sha512-Lz5sIPBdF2FUXcWeCu1//ojFAZqzTQNRga0aYv6dYXqJqPfMdCAI0NzajWUd4Xijj1IKJLtjoXRPMvTKWBcqKg==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@lezer/xml": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.6.tgz",
"integrity": "sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@lezer/yaml": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@lezer/yaml/-/yaml-1.0.3.tgz",
"integrity": "sha512-GuBLekbw9jDBDhGur82nuwkxKQ+a3W5H0GfaAthDXcAu+XdpS43VlnxA9E9hllkpSP5ellRDKjLLj7Lu9Wr6xA==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.4.0"
}
},
"node_modules/@listr2/prompt-adapter-inquirer": { "node_modules/@listr2/prompt-adapter-inquirer": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-3.0.1.tgz", "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-3.0.1.tgz",

View File

@ -40,7 +40,19 @@
"@angular/router": "20.3.2", "@angular/router": "20.3.2",
"@codemirror/autocomplete": "^6.19.0", "@codemirror/autocomplete": "^6.19.0",
"@codemirror/commands": "^6.9.0", "@codemirror/commands": "^6.9.0",
"@codemirror/lang-markdown": "^6.4.0", "@codemirror/lang-css": "^6.0.1",
"@codemirror/lang-go": "^6.0.1",
"@codemirror/lang-html": "^6.0.1",
"@codemirror/lang-java": "^6.0.1",
"@codemirror/lang-javascript": "^6.0.1",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-markdown": "^6.0.1",
"@codemirror/lang-php": "^6.0.1",
"@codemirror/lang-python": "^6.0.1",
"@codemirror/lang-rust": "^6.0.1",
"@codemirror/lang-sql": "^6.0.1",
"@codemirror/lang-xml": "^6.0.1",
"@codemirror/lang-yaml": "^6.0.1",
"@codemirror/language": "^6.11.3", "@codemirror/language": "^6.11.3",
"@codemirror/legacy-modes": "^6.5.2", "@codemirror/legacy-modes": "^6.5.2",
"@codemirror/lint": "^6.9.0", "@codemirror/lint": "^6.9.0",

169
scripts/refactor-colors.mjs Normal file
View File

@ -0,0 +1,169 @@
#!/usr/bin/env node
/**
* Script pour remplacer les couleurs hardcodées par des tokens CSS
* Usage: node scripts/refactor-colors.mjs
*/
import { readFileSync, writeFileSync, readdirSync, statSync } from 'fs';
import { join, extname } from 'path';
// Mapping des classes Tailwind hardcodées vers les tokens
const COLOR_REPLACEMENTS = [
// Dark mode backgrounds
{ from: /\bdark:bg-slate-700\b/g, to: 'dark:bg-surface2' },
{ from: /\bdark:bg-slate-800\b/g, to: 'dark:bg-card' },
{ from: /\bdark:bg-slate-900\b/g, to: 'dark:bg-main' },
{ from: /\bdark:bg-gray-700\b/g, to: 'dark:bg-surface2' },
{ from: /\bdark:bg-gray-800\b/g, to: 'dark:bg-card' },
{ from: /\bdark:bg-gray-900\b/g, to: 'dark:bg-main' },
{ from: /\bdark:bg-zinc-700\b/g, to: 'dark:bg-surface2' },
{ from: /\bdark:bg-zinc-800\b/g, to: 'dark:bg-card' },
{ from: /\bdark:bg-zinc-900\b/g, to: 'dark:bg-main' },
// Dark mode text
{ from: /\bdark:text-slate-200\b/g, to: 'dark:text-main' },
{ from: /\bdark:text-slate-300\b/g, to: 'dark:text-main' },
{ from: /\bdark:text-slate-400\b/g, to: 'dark:text-muted' },
{ from: /\bdark:text-gray-200\b/g, to: 'dark:text-main' },
{ from: /\bdark:text-gray-300\b/g, to: 'dark:text-main' },
{ from: /\bdark:text-gray-400\b/g, to: 'dark:text-muted' },
{ from: /\bdark:text-zinc-200\b/g, to: 'dark:text-main' },
{ from: /\bdark:text-zinc-300\b/g, to: 'dark:text-main' },
{ from: /\bdark:text-zinc-400\b/g, to: 'dark:text-muted' },
// Dark mode borders
{ from: /\bdark:border-slate-600\b/g, to: 'dark:border-border' },
{ from: /\bdark:border-slate-700\b/g, to: 'dark:border-border' },
{ from: /\bdark:border-gray-600\b/g, to: 'dark:border-border' },
{ from: /\bdark:border-gray-700\b/g, to: 'dark:border-border' },
{ from: /\bdark:border-zinc-600\b/g, to: 'dark:border-border' },
{ from: /\bdark:border-zinc-700\b/g, to: 'dark:border-border' },
// Dark hover states
{ from: /\bdark:hover:bg-slate-700\b/g, to: 'dark:hover:bg-surface2' },
{ from: /\bdark:hover:bg-slate-800\b/g, to: 'dark:hover:bg-card' },
{ from: /\bdark:hover:bg-gray-700\b/g, to: 'dark:hover:bg-surface2' },
{ from: /\bdark:hover:bg-gray-800\b/g, to: 'dark:hover:bg-card' },
{ from: /\bdark:hover:text-slate-200\b/g, to: 'dark:hover:text-main' },
{ from: /\bdark:hover:text-gray-200\b/g, to: 'dark:hover:text-main' },
// Backgrounds
{ from: /\bbg-slate-50\b/g, to: 'bg-surface1' },
{ from: /\bbg-slate-100\b/g, to: 'bg-surface1' },
{ from: /\bbg-slate-200\b/g, to: 'bg-surface2' },
{ from: /\bbg-slate-300\b/g, to: 'bg-muted' },
{ from: /\bbg-gray-50\b/g, to: 'bg-surface1' },
{ from: /\bbg-gray-100\b/g, to: 'bg-surface1' },
{ from: /\bbg-gray-200\b/g, to: 'bg-surface2' },
{ from: /\bbg-gray-300\b/g, to: 'bg-muted' },
{ from: /\bbg-gray-800\b/g, to: 'bg-card' },
{ from: /\bbg-gray-900\b/g, to: 'bg-main' },
{ from: /\bbg-zinc-50\b/g, to: 'bg-surface1' },
{ from: /\bbg-zinc-100\b/g, to: 'bg-surface1' },
{ from: /\bbg-zinc-800\b/g, to: 'bg-card' },
{ from: /\bbg-zinc-900\b/g, to: 'bg-main' },
{ from: /\bbg-white\b/g, to: 'bg-card' },
// Text colors
{ from: /\btext-slate-400\b/g, to: 'text-muted' },
{ from: /\btext-slate-500\b/g, to: 'text-muted' },
{ from: /\btext-slate-600\b/g, to: 'text-muted' },
{ from: /\btext-slate-700\b/g, to: 'text-main' },
{ from: /\btext-slate-800\b/g, to: 'text-main' },
{ from: /\btext-slate-900\b/g, to: 'text-main' },
{ from: /\btext-gray-400\b/g, to: 'text-muted' },
{ from: /\btext-gray-500\b/g, to: 'text-muted' },
{ from: /\btext-gray-600\b/g, to: 'text-muted' },
{ from: /\btext-gray-700\b/g, to: 'text-main' },
{ from: /\btext-gray-800\b/g, to: 'text-main' },
{ from: /\btext-gray-900\b/g, to: 'text-main' },
{ from: /\btext-zinc-400\b/g, to: 'text-muted' },
{ from: /\btext-zinc-500\b/g, to: 'text-muted' },
{ from: /\btext-zinc-600\b/g, to: 'text-muted' },
{ from: /\btext-zinc-700\b/g, to: 'text-main' },
{ from: /\btext-zinc-800\b/g, to: 'text-main' },
{ from: /\btext-zinc-900\b/g, to: 'text-main' },
// Borders
{ from: /\bborder-slate-200\b/g, to: 'border-border' },
{ from: /\bborder-slate-300\b/g, to: 'border-border' },
{ from: /\bborder-gray-200\b/g, to: 'border-border' },
{ from: /\bborder-gray-300\b/g, to: 'border-border' },
{ from: /\bborder-zinc-200\b/g, to: 'border-border' },
{ from: /\bborder-zinc-300\b/g, to: 'border-border' },
// Hover states
{ from: /\bhover:bg-slate-100\b/g, to: 'hover:bg-surface1' },
{ from: /\bhover:bg-slate-200\b/g, to: 'hover:bg-surface2' },
{ from: /\bhover:bg-gray-100\b/g, to: 'hover:bg-surface1' },
{ from: /\bhover:bg-gray-200\b/g, to: 'hover:bg-surface2' },
{ from: /\bhover:bg-zinc-100\b/g, to: 'hover:bg-surface1' },
{ from: /\bhover:text-slate-900\b/g, to: 'hover:text-main' },
{ from: /\bhover:text-gray-900\b/g, to: 'hover:text-main' },
// Focus states
{ from: /\bfocus:border-slate-400\b/g, to: 'focus:border-primary' },
{ from: /\bfocus:border-gray-400\b/g, to: 'focus:border-primary' },
{ from: /\bfocus:ring-slate-500\b/g, to: 'focus:ring-ring' },
{ from: /\bfocus:ring-gray-500\b/g, to: 'focus:ring-ring' },
];
function processFile(filePath) {
try {
let content = readFileSync(filePath, 'utf-8');
let modified = false;
for (const { from, to } of COLOR_REPLACEMENTS) {
if (from.test(content)) {
content = content.replace(from, to);
modified = true;
}
}
if (modified) {
writeFileSync(filePath, content, 'utf-8');
console.log(`✓ Updated: ${filePath}`);
return 1;
}
return 0;
} catch (error) {
console.error(`✗ Error processing ${filePath}:`, error.message);
return 0;
}
}
function walkDirectory(dir, extensions = ['.ts', '.html']) {
let filesUpdated = 0;
const items = readdirSync(dir);
for (const item of items) {
const fullPath = join(dir, item);
const stat = statSync(fullPath);
if (stat.isDirectory()) {
// Skip node_modules, .angular, dist, etc.
if (!['node_modules', '.angular', 'dist', '.git'].includes(item)) {
filesUpdated += walkDirectory(fullPath, extensions);
}
} else if (stat.isFile()) {
const ext = extname(fullPath);
if (extensions.includes(ext)) {
filesUpdated += processFile(fullPath);
}
}
}
return filesUpdated;
}
// Main execution
const srcDir = join(process.cwd(), 'src');
console.log('🎨 Starting color refactoring...\n');
console.log(`Scanning: ${srcDir}\n`);
const filesUpdated = walkDirectory(srcDir);
console.log(`\n✨ Refactoring complete!`);
console.log(`📝 Files updated: ${filesUpdated}`);

View File

@ -419,38 +419,102 @@
} }
:host ::ng-deep .code-block { :host ::ng-deep .code-block {
margin: 1.8rem auto; margin: 1.25rem auto;
border-radius: 0.95rem; border-radius: 0.75rem;
border: 1px solid rgba(71, 85, 105, 0.25); border: 1px solid var(--md-pre-border);
overflow: hidden; overflow: hidden;
background: rgba(248, 250, 252, 0.75); background: color-mix(in oklab, var(--md-pre-bg) 92%, transparent);
box-shadow: 0 22px 50px -30px rgba(15, 23, 42, 0.45); box-shadow: 0 16px 40px -28px rgba(15, 23, 42, 0.35);
width: fit-content; width: fit-content;
max-width: 100%; max-width: 100%;
transition: transform 0.2s ease, box-shadow 0.2s ease; transition: transform 0.2s ease, box-shadow 0.2s ease;
} }
:host-context(.dark) ::ng-deep .code-block { :host-context(.dark) ::ng-deep .code-block {
background: rgba(15, 23, 42, 0.82); background: var(--md-pre-bg);
border-color: rgba(148, 163, 184, 0.3); border-color: var(--md-pre-border);
box-shadow: 0 25px 60px -35px rgba(15, 23, 42, 0.9); box-shadow: 0 18px 48px -30px rgba(0, 0, 0, 0.55);
} }
:host ::ng-deep .code-block__header { :host ::ng-deep .code-block__header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 1rem; gap: 0.75rem;
padding: 0.75rem 1rem; padding: 0.5rem 0.75rem;
background: linear-gradient(90deg, rgba(15, 118, 110, 0.14), rgba(15, 118, 110, 0)); background: color-mix(in oklab, var(--md-pre-bg) 85%, transparent);
border-bottom: 1px solid rgba(13, 148, 136, 0.22); border-bottom: 1px solid color-mix(in oklab, var(--md-pre-border) 85%, transparent);
} }
:host-context(.dark) ::ng-deep .code-block__header { :host-context(.dark) ::ng-deep .code-block__header {
background: linear-gradient(90deg, rgba(45, 212, 191, 0.25), rgba(45, 212, 191, 0)); background: color-mix(in oklab, var(--md-pre-bg) 92%, transparent);
border-bottom: 1px solid rgba(34, 197, 94, 0.25); border-bottom: 1px solid var(--md-pre-border);
} }
/* Header content: kind icon + label */
:host ::ng-deep .code-block__kind {
display: inline-flex;
align-items: center;
gap: 0.4rem;
color: var(--md-pre-fg);
}
:host ::ng-deep .code-block__kind-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.25rem;
height: 1.25rem;
}
:host ::ng-deep .code-block__kind-icon svg {
width: 1rem;
height: 1rem;
}
/* Kind-specific header accents (light) */
:host ::ng-deep .code-block--kind-code .code-block__header {
background: linear-gradient(90deg,
color-mix(in oklab, var(--md-syntax-2) 12%, transparent) 0%,
transparent 60%);
}
:host ::ng-deep .code-block--kind-shell .code-block__header {
background: linear-gradient(90deg,
color-mix(in oklab, var(--md-syntax-4) 12%, transparent) 0%,
transparent 60%);
}
:host ::ng-deep .code-block--kind-mermaid .code-block__header {
background: linear-gradient(90deg,
color-mix(in oklab, var(--md-syntax-3) 12%, transparent) 0%,
transparent 60%);
}
/* Dark mode keeps subtle tint but relies more on border color */
:host-context(.dark) ::ng-deep .code-block--kind-code .code-block__header {
background: linear-gradient(90deg,
color-mix(in oklab, var(--md-syntax-2) 14%, transparent) 0%,
transparent 60%);
}
:host-context(.dark) ::ng-deep .code-block--kind-shell .code-block__header {
background: linear-gradient(90deg,
color-mix(in oklab, var(--md-syntax-4) 14%, transparent) 0%,
transparent 60%);
}
:host-context(.dark) ::ng-deep .code-block--kind-mermaid .code-block__header {
background: linear-gradient(90deg,
color-mix(in oklab, var(--md-syntax-3) 14%, transparent) 0%,
transparent 60%);
}
/* Icon color per kind */
:host ::ng-deep .code-block--kind-code .code-block__kind-icon { color: var(--md-syntax-2); }
:host ::ng-deep .code-block--kind-shell .code-block__kind-icon { color: var(--md-syntax-4); }
:host ::ng-deep .code-block--kind-mermaid .code-block__kind-icon { color: var(--md-syntax-3); }
:host ::ng-deep .code-block__controls { :host ::ng-deep .code-block__controls {
display: flex; display: flex;
align-items: center; align-items: center;
@ -480,54 +544,39 @@
:host ::ng-deep .code-block__actions { :host ::ng-deep .code-block__actions {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.75rem; gap: 0.5rem;
margin-left: auto; margin-left: auto;
} }
:host ::ng-deep .code-block__language-badge { :host ::ng-deep .code-block__language-badge {
font-family: "JetBrains Mono", "Fira Code", "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; font-family: "JetBrains Mono", "Fira Code", "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
font-size: 0.75rem; font-size: 0.72rem;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.08em; letter-spacing: 0.08em;
padding: 0.3rem 0.9rem; padding: 0.25rem 0.7rem;
border-radius: 999px; border-radius: 999px;
background: rgba(15, 118, 110, 0.2); background: color-mix(in oklab, var(--md-pre-bg) 75%, transparent);
color: #0f766e; color: var(--md-pre-fg);
border: 1px solid rgba(13, 148, 136, 0.35); border: 1px solid color-mix(in oklab, var(--md-pre-border) 70%, transparent);
cursor: pointer; cursor: pointer;
transition: background 0.2s ease, color 0.2s ease, transform 0.2s ease; transition: background 0.2s ease, color 0.2s ease, transform 0.2s ease;
} }
:host-context(.dark) ::ng-deep .code-block__language-badge {
background: rgba(45, 212, 191, 0.2);
border-color: rgba(34, 197, 94, 0.4);
color: #5eead4;
}
:host ::ng-deep .code-block__language-badge:hover, :host ::ng-deep .code-block__language-badge:hover,
:host ::ng-deep .code-block__language-badge:focus-visible { :host ::ng-deep .code-block__language-badge:focus-visible {
transform: translateY(-1px); transform: translateY(-1px);
background: rgba(15, 118, 110, 0.3); background: color-mix(in oklab, var(--md-pre-bg) 85%, transparent);
}
:host-context(.dark) ::ng-deep .code-block__language-badge:hover,
:host-context(.dark) ::ng-deep .code-block__language-badge:focus-visible {
background: rgba(45, 212, 191, 0.35);
} }
:host ::ng-deep .code-block__copy-feedback { :host ::ng-deep .code-block__copy-feedback {
font-size: 0.68rem; font-size: 0.68rem;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.12em; letter-spacing: 0.12em;
color: #0f766e; color: var(--md-pre-fg);
opacity: 0; opacity: 0;
transition: opacity 0.2s ease; transition: opacity 0.2s ease;
} }
:host-context(.dark) ::ng-deep .code-block__copy-feedback {
color: #5eead4;
}
:host ::ng-deep .code-block.copied { :host ::ng-deep .code-block.copied {
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 26px 60px -28px rgba(15, 23, 42, 0.55); box-shadow: 0 26px 60px -28px rgba(15, 23, 42, 0.55);
@ -538,223 +587,31 @@
} }
:host ::ng-deep .code-block__body { :host ::ng-deep .code-block__body {
background: rgba(15, 23, 42, 0.05); background: var(--md-pre-bg);
margin: 0; margin: 0;
padding: 1.2rem 1.4rem; padding: 0.9rem 1rem;
overflow: auto; overflow: auto;
max-width: 100%; max-width: 100%;
} }
:host-context(.dark) ::ng-deep .code-block__body {
background: rgba(15, 23, 42, 0.75);
}
:host ::ng-deep .code-block__body code { :host ::ng-deep .code-block__body code {
display: block; display: block;
font-family: "JetBrains Mono", "Fira Code", "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; font-family: "JetBrains Mono", "Fira Code", "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
font-size: 0.92rem; font-size: 0.9rem;
line-height: 1.65; line-height: 1.6;
color: #0f172a; color: var(--md-pre-fg);
min-width: max-content; min-width: max-content;
} }
:host-context(.dark) ::ng-deep .code-block__body code {
color: #e2e8f0;
}
:host ::ng-deep .code-block__body::-webkit-scrollbar { :host ::ng-deep .code-block__body::-webkit-scrollbar {
height: 10px; height: 10px;
} }
:host ::ng-deep .code-block__body::-webkit-scrollbar-thumb { :host ::ng-deep .code-block__body::-webkit-scrollbar-thumb {
background: rgba(15, 118, 110, 0.35); background: color-mix(in oklab, var(--md-pre-border) 30%, transparent);
border-radius: 999px; border-radius: 999px;
} }
:host-context(.dark) ::ng-deep .code-block__body::-webkit-scrollbar-thumb {
background: rgba(45, 212, 191, 0.4);
}
:host ::ng-deep .hljs-keyword,
:host ::ng-deep .hljs-selector-tag,
:host ::ng-deep .hljs-literal,
:host ::ng-deep .hljs-section,
:host ::ng-deep .hljs-link {
color: #2563eb;
}
:host ::ng-deep .hljs-function .hljs-title,
:host ::ng-deep .hljs-title,
:host ::ng-deep .hljs-name {
color: #0f766e;
}
:host ::ng-deep .hljs-subst,
:host ::ng-deep .hljs-attribute,
:host ::ng-deep .hljs-number,
:host ::ng-deep .hljs-symbol {
color: #a16207;
}
:host ::ng-deep .hljs-string,
:host ::ng-deep .hljs-title.class_,
:host ::ng-deep .hljs-meta-string {
color: #be123c;
}
:host ::ng-deep .hljs-comment,
:host ::ng-deep .hljs-quote {
color: rgba(100, 116, 139, 0.9);
}
:host-context(.dark) ::ng-deep .hljs-comment,
:host-context(.dark) ::ng-deep .hljs-quote {
color: rgba(148, 163, 184, 0.75);
}
:host ::ng-deep .markdown-table {
display: inline-block;
margin: 1.5rem auto;
border-radius: 0.75rem;
border: 1px solid rgba(71, 85, 105, 0.25);
overflow: hidden;
background: rgba(248, 250, 252, 0.75);
box-shadow: 0 18px 45px -30px rgba(15, 23, 42, 0.45);
width: fit-content;
max-width: 100%;
}
:host-context(.dark) ::ng-deep .markdown-table {
background: rgba(15, 23, 42, 0.85);
border-color: rgba(148, 163, 184, 0.35);
box-shadow: 0 25px 60px -40px rgba(15, 23, 42, 0.85);
}
:host ::ng-deep .markdown-table table {
border-collapse: collapse;
font-size: 0.95rem;
width: auto;
table-layout: auto;
max-width: 100%;
}
:host ::ng-deep .markdown-table thead {
background: rgba(15, 118, 110, 0.18);
color: #0f172a;
text-transform: uppercase;
letter-spacing: 0.08em;
}
:host-context(.dark) ::ng-deep .markdown-table thead {
background: rgba(45, 212, 191, 0.25);
color: #e2e8f0;
}
:host ::ng-deep .markdown-table th,
:host ::ng-deep .markdown-table td {
padding: 0.8rem 1.1rem;
border-bottom: 1px solid rgba(148, 163, 184, 0.25);
vertical-align: top;
white-space: nowrap;
}
:host-context(.dark) ::ng-deep .markdown-table th,
:host-context(.dark) ::ng-deep .markdown-table td {
border-bottom-color: rgba(148, 163, 184, 0.35);
}
:host ::ng-deep .markdown-table tbody tr:nth-child(odd) {
background: rgba(15, 118, 110, 0.08);
}
:host-context(.dark) ::ng-deep .markdown-table tbody tr:nth-child(odd) {
background: rgba(20, 184, 166, 0.2);
}
:host ::ng-deep .markdown-table tbody tr:last-child td {
border-bottom: none;
}
:host ::ng-deep .markdown-table code.inline-code {
background: rgba(15, 118, 110, 0.15);
}
:host-context(.dark) ::ng-deep .markdown-table code.inline-code {
background: rgba(45, 212, 191, 0.22);
}
:host ::ng-deep .md-heading {
font-family: "Inter", "Segoe UI", system-ui, -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif;
font-weight: 700;
line-height: 1.25;
letter-spacing: -0.015em;
}
:host ::ng-deep .md-heading-1 {
font-size: clamp(2.6rem, 3.6vw, 3.1rem);
margin-top: 0.55rem;
margin-bottom: 0.55rem;
color: #0f172a;
}
:host ::ng-deep .md-heading-2 {
font-size: clamp(2.1rem, 3vw, 2.5rem);
margin-top: 0.35rem;
margin-bottom: 0.35rem;
color: #0f766e;
}
:host ::ng-deep .md-heading-3 {
font-size: clamp(1.7rem, 2.6vw, 2rem);
margin-top: 0.3rem;
margin-bottom: 0.3rem;
color: #0f766e;
}
:host ::ng-deep .md-heading-4 {
font-size: clamp(1.45rem, 2.1vw, 1.7rem);
margin-top: 0.25rem;
margin-bottom: 0.25rem;
color: #0f766e;
}
:host ::ng-deep .md-heading-5 {
font-size: clamp(1.25rem, 1.7vw, 1.35rem);
margin-top: 0.2rem;
margin-bottom: 0.22rem;
color: #0f766e;
}
:host ::ng-deep .md-heading-6 {
font-size: clamp(1.1rem, 1.5vw, 1.2rem);
margin-top: 0.2rem;
margin-bottom: 0.2rem;
color: #0f766e;
}
:host ::ng-deep .note-content-area p {
margin-top: 0.35rem;
margin-bottom: 0.35rem;
}
:host-context(.dark) ::ng-deep .md-heading-1 {
font-weight: 600;
letter-spacing: 0.01em;
text-transform: uppercase;
color: #e2e8f0;
}
:host-context(.dark) ::ng-deep .md-heading-2,
:host-context(.dark) ::ng-deep .md-heading-3,
:host-context(.dark) ::ng-deep .md-heading-4,
:host-context(.dark) ::ng-deep .md-heading-5,
:host-context(.dark) ::ng-deep .md-heading-6 {
color: #5eead4;
}
:host ::ng-deep .prose :where(p, li, blockquote):not(:where(.not-prose *)) {
line-height: 1.55;
}
:host ::ng-deep .metadata-panel { :host ::ng-deep .metadata-panel {
margin-bottom: 2.2rem; margin-bottom: 2.2rem;

View File

@ -28,6 +28,7 @@
(searchTermChange)="onSidebarSearchTermChange($event)" (searchTermChange)="onSidebarSearchTermChange($event)"
(searchOptionsChange)="onHeaderSearchOptionsChange($event)" (searchOptionsChange)="onHeaderSearchOptionsChange($event)"
(markdownPlaygroundSelected)="setView('markdown-playground')" (markdownPlaygroundSelected)="setView('markdown-playground')"
(parametersOpened)="setView('parameters')"
></app-shell-nimbus-layout> ></app-shell-nimbus-layout>
} @else { } @else {
<main class="relative flex min-h-screen flex-col bg-bg-main text-text-main lg:flex-row lg:h-screen lg:overflow-hidden"> <main class="relative flex min-h-screen flex-col bg-bg-main text-text-main lg:flex-row lg:h-screen lg:overflow-hidden">
@ -532,6 +533,8 @@
<div class="h-[calc(100vh-180px)] lg:h-[calc(100vh-140px)]"> <div class="h-[calc(100vh-180px)] lg:h-[calc(100vh-140px)]">
<app-markdown-playground></app-markdown-playground> <app-markdown-playground></app-markdown-playground>
</div> </div>
} @else if (activeView() === 'parameters') {
<app-parameters></app-parameters>
} @else { } @else {
@if (activeView() === 'drawings') { @if (activeView() === 'drawings') {
@if (currentDrawingPath()) { @if (currentDrawingPath()) {

View File

@ -34,6 +34,7 @@ import { SearchIndexService } from './core/search/search-index.service';
import { SearchOrchestratorService } from './core/search/search-orchestrator.service'; import { SearchOrchestratorService } from './core/search/search-orchestrator.service';
import { LayoutModule } from '@angular/cdk/layout'; import { LayoutModule } from '@angular/cdk/layout';
import { ToastContainerComponent } from './app/shared/toast/toast-container.component'; import { ToastContainerComponent } from './app/shared/toast/toast-container.component';
import { ParametersPage } from './app/features/parameters/parameters.page';
// Types // Types
import { FileMetadata, Note, TagInfo, VaultNode } from './types'; import { FileMetadata, Note, TagInfo, VaultNode } from './types';
@ -65,6 +66,7 @@ interface TocEntry {
AppShellNimbusLayoutComponent, AppShellNimbusLayoutComponent,
MarkdownPlaygroundComponent, MarkdownPlaygroundComponent,
ToastContainerComponent, ToastContainerComponent,
ParametersPage,
], ],
templateUrl: './app.component.simple.html', templateUrl: './app.component.simple.html',
styleUrls: ['./app.component.css'], styleUrls: ['./app.component.css'],
@ -90,7 +92,7 @@ export class AppComponent implements OnInit, OnDestroy {
isSidebarOpen = signal<boolean>(true); isSidebarOpen = signal<boolean>(true);
isOutlineOpen = signal<boolean>(false); isOutlineOpen = signal<boolean>(false);
outlineTab = signal<'outline' | 'settings'>('outline'); outlineTab = signal<'outline' | 'settings'>('outline');
activeView = signal<'files' | 'graph' | 'tags' | 'search' | 'calendar' | 'bookmarks' | 'drawings' | 'markdown-playground'>('files'); activeView = signal<'files' | 'graph' | 'tags' | 'search' | 'calendar' | 'bookmarks' | 'drawings' | 'markdown-playground' | 'parameters'>('files');
currentDrawingPath = signal<string | null>(null); currentDrawingPath = signal<string | null>(null);
selectedNoteId = signal<string>(''); selectedNoteId = signal<string>('');
sidebarSearchTerm = signal<string>(''); sidebarSearchTerm = signal<string>('');
@ -295,7 +297,7 @@ export class AppComponent implements OnInit, OnDestroy {
const end = m.index + m[0].length; const end = m.index + m[0].length;
if (start > lastIndex) fragments.push(nodeText.slice(lastIndex, start)); if (start > lastIndex) fragments.push(nodeText.slice(lastIndex, start));
const mark = document.createElement('mark'); const mark = document.createElement('mark');
mark.className = 'doc-highlight bg-yellow-200 dark:bg-yellow-600 text-gray-900 dark:text-gray-900 px-0.5 rounded'; mark.className = 'doc-highlight bg-yellow-200 dark:bg-yellow-600 text-main dark:text-main px-0.5 rounded';
mark.textContent = nodeText.slice(start, end); mark.textContent = nodeText.slice(start, end);
fragments.push(mark); fragments.push(mark);
lastIndex = end; lastIndex = end;
@ -654,6 +656,9 @@ export class AppComponent implements OnInit, OnDestroy {
} }
ngOnInit(): void { ngOnInit(): void {
// Initialize theme from storage
this.themeService.initFromStorage();
// Log app start // Log app start
this.logService.log('APP_START', { this.logService.log('APP_START', {
viewport: { viewport: {
@ -860,7 +865,7 @@ export class AppComponent implements OnInit, OnDestroy {
handle?.addEventListener('lostpointercapture', cleanup); handle?.addEventListener('lostpointercapture', cleanup);
} }
setView(view: 'files' | 'graph' | 'tags' | 'search' | 'calendar' | 'bookmarks' | 'drawings' | 'markdown-playground'): void { setView(view: 'files' | 'graph' | 'tags' | 'search' | 'calendar' | 'bookmarks' | 'drawings' | 'markdown-playground' | 'parameters'): void {
const previousView = this.activeView(); const previousView = this.activeView();
this.activeView.set(view); this.activeView.set(view);
this.sidebarSearchTerm.set(''); this.sidebarSearchTerm.set('');

View File

@ -0,0 +1,460 @@
import { Extension } from '@codemirror/state';
import { EditorView } from '@codemirror/view';
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
import { tags } from '@lezer/highlight';
import type { ThemeId } from './services/theme.service';
/**
* Génère les extensions CodeMirror 6 pour un thème donné
* @param themeId - L'identifiant du thème
* @param mode - 'light' ou 'dark'
* @returns Extensions CodeMirror 6
*/
export function cm6ThemeFor(themeId: ThemeId, mode: 'light' | 'dark'): Extension[] {
switch (themeId) {
case 'obsidian':
return createObsidianTheme(mode);
case 'nord':
return createNordTheme(mode);
case 'notion':
return createNotionTheme(mode);
case 'github':
return createGitHubTheme(mode);
case 'discord':
return createDiscordTheme(mode);
case 'monokai':
return createMonokaiTheme(mode);
case 'dark':
return createDefaultDarkTheme();
case 'light':
default:
return createDefaultLightTheme();
}
}
// ============================================================================
// THÈME PAR DÉFAUT LIGHT
// ============================================================================
function createDefaultLightTheme(): Extension[] {
const theme = EditorView.theme({
'&': {
backgroundColor: '#ffffff',
color: '#111827',
},
'.cm-content': {
caretColor: '#3b82f6',
},
'.cm-cursor, .cm-dropCursor': {
borderLeftColor: '#3b82f6',
},
'&.cm-focused .cm-selectionBackground, ::selection': {
backgroundColor: '#bfdbfe',
},
'.cm-activeLine': {
backgroundColor: '#f1f5f9',
},
'.cm-gutters': {
backgroundColor: '#f8fafc',
color: '#94a3b8',
border: 'none',
},
'.cm-activeLineGutter': {
backgroundColor: '#e2e8f0',
},
}, { dark: false });
const highlightStyle = HighlightStyle.define([
{ tag: tags.keyword, color: '#2563eb', fontWeight: 'bold' },
{ tag: tags.comment, color: '#6b7280', fontStyle: 'italic' },
{ tag: tags.string, color: '#22c55e' },
{ tag: tags.number, color: '#f59e0b' },
{ tag: tags.bool, color: '#2563eb' },
{ tag: tags.variableName, color: '#111827' },
{ tag: tags.function(tags.variableName), color: '#3b82f6' },
{ tag: tags.typeName, color: '#8b5cf6' },
{ tag: tags.tagName, color: '#2563eb' },
{ tag: tags.attributeName, color: '#14b8a6' },
{ tag: tags.heading, color: '#111827', fontWeight: 'bold' },
{ tag: tags.link, color: '#2563eb', textDecoration: 'underline' },
{ tag: tags.emphasis, fontStyle: 'italic' },
{ tag: tags.strong, fontWeight: 'bold' },
]);
return [theme, syntaxHighlighting(highlightStyle)];
}
// ============================================================================
// THÈME PAR DÉFAUT DARK
// ============================================================================
function createDefaultDarkTheme(): Extension[] {
const theme = EditorView.theme({
'&': {
backgroundColor: '#0f172a',
color: '#e5e7eb',
},
'.cm-content': {
caretColor: '#60a5fa',
},
'.cm-cursor, .cm-dropCursor': {
borderLeftColor: '#60a5fa',
},
'&.cm-focused .cm-selectionBackground, ::selection': {
backgroundColor: '#1e3a8a',
},
'.cm-activeLine': {
backgroundColor: '#1e293b',
},
'.cm-gutters': {
backgroundColor: '#0b1220',
color: '#64748b',
border: 'none',
},
'.cm-activeLineGutter': {
backgroundColor: '#1e293b',
},
}, { dark: true });
const highlightStyle = HighlightStyle.define([
{ tag: tags.keyword, color: '#60a5fa', fontWeight: 'bold' },
{ tag: tags.comment, color: '#94a3b8', fontStyle: 'italic' },
{ tag: tags.string, color: '#34d399' },
{ tag: tags.number, color: '#fbbf24' },
{ tag: tags.bool, color: '#60a5fa' },
{ tag: tags.variableName, color: '#e5e7eb' },
{ tag: tags.function(tags.variableName), color: '#60a5fa' },
{ tag: tags.typeName, color: '#a78bfa' },
{ tag: tags.tagName, color: '#60a5fa' },
{ tag: tags.attributeName, color: '#2dd4bf' },
{ tag: tags.heading, color: '#f3f4f6', fontWeight: 'bold' },
{ tag: tags.link, color: '#93c5fd', textDecoration: 'underline' },
{ tag: tags.emphasis, fontStyle: 'italic' },
{ tag: tags.strong, fontWeight: 'bold' },
]);
return [theme, syntaxHighlighting(highlightStyle)];
}
// ============================================================================
// OBSIDIAN
// ============================================================================
function createObsidianTheme(mode: 'light' | 'dark'): Extension[] {
if (mode === 'light') {
const theme = EditorView.theme({
'&': { backgroundColor: '#ffffff', color: '#2e3338' },
'.cm-content': { caretColor: '#a89984' },
'.cm-cursor, .cm-dropCursor': { borderLeftColor: '#a89984' },
'&.cm-focused .cm-selectionBackground, ::selection': { backgroundColor: '#d4c5aa' },
'.cm-activeLine': { backgroundColor: '#f5f3ef' },
'.cm-gutters': { backgroundColor: '#fafaf8', color: '#888', border: 'none' },
'.cm-activeLineGutter': { backgroundColor: '#f0ede6' },
}, { dark: false });
const highlightStyle = HighlightStyle.define([
{ tag: tags.keyword, color: '#7c6f64', fontWeight: 'bold' },
{ tag: tags.comment, color: '#a89984', fontStyle: 'italic' },
{ tag: tags.string, color: '#427b58' },
{ tag: tags.number, color: '#b16286' },
{ tag: tags.bool, color: '#7c6f64' },
{ tag: tags.heading, color: '#2e3338', fontWeight: 'bold' },
{ tag: tags.link, color: '#076678', textDecoration: 'underline' },
]);
return [theme, syntaxHighlighting(highlightStyle)];
} else {
const theme = EditorView.theme({
'&': { backgroundColor: '#1e1e1e', color: '#e0e0e0' },
'.cm-content': { caretColor: '#a89984' },
'.cm-cursor, .cm-dropCursor': { borderLeftColor: '#a89984' },
'&.cm-focused .cm-selectionBackground, ::selection': { backgroundColor: '#3e3e3e' },
'.cm-activeLine': { backgroundColor: '#252525' },
'.cm-gutters': { backgroundColor: '#1a1a1a', color: '#666', border: 'none' },
'.cm-activeLineGutter': { backgroundColor: '#2a2a2a' },
}, { dark: true });
const highlightStyle = HighlightStyle.define([
{ tag: tags.keyword, color: '#a89984', fontWeight: 'bold' },
{ tag: tags.comment, color: '#7c6f64', fontStyle: 'italic' },
{ tag: tags.string, color: '#7aa2f7' },
{ tag: tags.number, color: '#d3869b' },
{ tag: tags.heading, color: '#e0e0e0', fontWeight: 'bold' },
{ tag: tags.link, color: '#7aa2f7', textDecoration: 'underline' },
]);
return [theme, syntaxHighlighting(highlightStyle)];
}
}
// ============================================================================
// NORD
// ============================================================================
function createNordTheme(mode: 'light' | 'dark'): Extension[] {
if (mode === 'light') {
const theme = EditorView.theme({
'&': { backgroundColor: '#ffffff', color: '#2e3440' },
'.cm-content': { caretColor: '#5e81ac' },
'.cm-cursor, .cm-dropCursor': { borderLeftColor: '#5e81ac' },
'&.cm-focused .cm-selectionBackground, ::selection': { backgroundColor: 'rgba(136, 192, 208, 0.3)' },
'.cm-activeLine': { backgroundColor: '#eceff4' },
'.cm-gutters': { backgroundColor: '#eceff4', color: '#5e81ac', border: 'none' },
'.cm-activeLineGutter': { backgroundColor: '#e5e9f0' },
}, { dark: false });
const highlightStyle = HighlightStyle.define([
{ tag: tags.keyword, color: '#5e81ac', fontWeight: 'bold' },
{ tag: tags.comment, color: '#4c566a', fontStyle: 'italic' },
{ tag: tags.string, color: '#a3be8c' },
{ tag: tags.number, color: '#b48ead' },
{ tag: tags.bool, color: '#5e81ac' },
{ tag: tags.variableName, color: '#2e3440' },
{ tag: tags.function(tags.variableName), color: '#5e81ac' },
{ tag: tags.typeName, color: '#8fbcbb' },
{ tag: tags.heading, color: '#2e3440', fontWeight: 'bold' },
{ tag: tags.link, color: '#5e81ac', textDecoration: 'underline' },
]);
return [theme, syntaxHighlighting(highlightStyle)];
} else {
const theme = EditorView.theme({
'&': { backgroundColor: '#2e3440', color: '#e5e9f0' },
'.cm-content': { caretColor: '#88c0d0' },
'.cm-cursor, .cm-dropCursor': { borderLeftColor: '#88c0d0' },
'&.cm-focused .cm-selectionBackground, ::selection': { backgroundColor: 'rgba(67, 76, 94, 0.8)' },
'.cm-activeLine': { backgroundColor: '#3b4252' },
'.cm-gutters': { backgroundColor: '#2e3440', color: '#4c566a', border: 'none' },
'.cm-activeLineGutter': { backgroundColor: '#434c5e' },
}, { dark: true });
const highlightStyle = HighlightStyle.define([
{ tag: tags.keyword, color: '#81a1c1', fontWeight: 'bold' },
{ tag: tags.comment, color: '#616e88', fontStyle: 'italic' },
{ tag: tags.string, color: '#a3be8c' },
{ tag: tags.number, color: '#b48ead' },
{ tag: tags.bool, color: '#81a1c1' },
{ tag: tags.variableName, color: '#d8dee9' },
{ tag: tags.function(tags.variableName), color: '#88c0d0' },
{ tag: tags.typeName, color: '#8fbcbb' },
{ tag: tags.heading, color: '#e5e9f0', fontWeight: 'bold' },
{ tag: tags.link, color: '#88c0d0', textDecoration: 'underline' },
]);
return [theme, syntaxHighlighting(highlightStyle)];
}
}
// ============================================================================
// NOTION
// ============================================================================
function createNotionTheme(mode: 'light' | 'dark'): Extension[] {
if (mode === 'light') {
const theme = EditorView.theme({
'&': { backgroundColor: '#ffffff', color: '#2f3437' },
'.cm-content': { caretColor: '#2f3437' },
'.cm-cursor, .cm-dropCursor': { borderLeftColor: '#2f3437' },
'&.cm-focused .cm-selectionBackground, ::selection': { backgroundColor: 'rgba(37, 99, 235, 0.2)' },
'.cm-activeLine': { backgroundColor: '#f7f7f5' },
'.cm-gutters': { backgroundColor: '#f7f7f5', color: '#9b9a97', border: 'none' },
'.cm-activeLineGutter': { backgroundColor: '#eeeeec' },
}, { dark: false });
const highlightStyle = HighlightStyle.define([
{ tag: tags.keyword, color: '#2f3437', fontWeight: 'bold' },
{ tag: tags.comment, color: '#6b7280', fontStyle: 'italic' },
{ tag: tags.string, color: '#14b8a6' },
{ tag: tags.number, color: '#f59e0b' },
{ tag: tags.bool, color: '#2563eb' },
{ tag: tags.heading, color: '#2f3437', fontWeight: 'bold' },
{ tag: tags.link, color: '#2563eb', textDecoration: 'underline' },
]);
return [theme, syntaxHighlighting(highlightStyle)];
} else {
const theme = EditorView.theme({
'&': { backgroundColor: '#1b1b1b', color: '#e7e7e5' },
'.cm-content': { caretColor: '#cfcfc9' },
'.cm-cursor, .cm-dropCursor': { borderLeftColor: '#cfcfc9' },
'&.cm-focused .cm-selectionBackground, ::selection': { backgroundColor: 'rgba(147, 197, 253, 0.25)' },
'.cm-activeLine': { backgroundColor: '#171717' },
'.cm-gutters': { backgroundColor: '#171717', color: '#9b9a97', border: 'none' },
'.cm-activeLineGutter': { backgroundColor: '#111111' },
}, { dark: true });
const highlightStyle = HighlightStyle.define([
{ tag: tags.keyword, color: '#cfcfc9', fontWeight: 'bold' },
{ tag: tags.comment, color: '#9b9a97', fontStyle: 'italic' },
{ tag: tags.string, color: '#2dd4bf' },
{ tag: tags.number, color: '#fbbf24' },
{ tag: tags.bool, color: '#93c5fd' },
{ tag: tags.heading, color: '#e7e7e5', fontWeight: 'bold' },
{ tag: tags.link, color: '#93c5fd', textDecoration: 'underline' },
]);
return [theme, syntaxHighlighting(highlightStyle)];
}
}
// ============================================================================
// GITHUB
// ============================================================================
function createGitHubTheme(mode: 'light' | 'dark'): Extension[] {
if (mode === 'light') {
const theme = EditorView.theme({
'&': { backgroundColor: '#ffffff', color: '#24292f' },
'.cm-content': { caretColor: '#0969da' },
'.cm-cursor, .cm-dropCursor': { borderLeftColor: '#0969da' },
'&.cm-focused .cm-selectionBackground, ::selection': { backgroundColor: 'rgba(9, 105, 218, 0.15)' },
'.cm-activeLine': { backgroundColor: '#f6f8fa' },
'.cm-gutters': { backgroundColor: '#f6f8fa', color: '#57606a', border: 'none' },
'.cm-activeLineGutter': { backgroundColor: '#eaeef2' },
}, { dark: false });
const highlightStyle = HighlightStyle.define([
{ tag: tags.keyword, color: '#cf222e', fontWeight: 'bold' },
{ tag: tags.comment, color: '#57606a', fontStyle: 'italic' },
{ tag: tags.string, color: '#0a3069' },
{ tag: tags.number, color: '#0550ae' },
{ tag: tags.bool, color: '#0550ae' },
{ tag: tags.variableName, color: '#24292f' },
{ tag: tags.function(tags.variableName), color: '#8250df' },
{ tag: tags.heading, color: '#24292f', fontWeight: 'bold' },
{ tag: tags.link, color: '#0969da', textDecoration: 'underline' },
]);
return [theme, syntaxHighlighting(highlightStyle)];
} else {
const theme = EditorView.theme({
'&': { backgroundColor: '#0d1117', color: '#c9d1d9' },
'.cm-content': { caretColor: '#58a6ff' },
'.cm-cursor, .cm-dropCursor': { borderLeftColor: '#58a6ff' },
'&.cm-focused .cm-selectionBackground, ::selection': { backgroundColor: 'rgba(88, 166, 255, 0.25)' },
'.cm-activeLine': { backgroundColor: '#161b22' },
'.cm-gutters': { backgroundColor: '#0d1117', color: '#8b949e', border: 'none' },
'.cm-activeLineGutter': { backgroundColor: '#21262d' },
}, { dark: true });
const highlightStyle = HighlightStyle.define([
{ tag: tags.keyword, color: '#ff7b72', fontWeight: 'bold' },
{ tag: tags.comment, color: '#8b949e', fontStyle: 'italic' },
{ tag: tags.string, color: '#a5d6ff' },
{ tag: tags.number, color: '#79c0ff' },
{ tag: tags.bool, color: '#79c0ff' },
{ tag: tags.variableName, color: '#c9d1d9' },
{ tag: tags.function(tags.variableName), color: '#d2a8ff' },
{ tag: tags.heading, color: '#c9d1d9', fontWeight: 'bold' },
{ tag: tags.link, color: '#58a6ff', textDecoration: 'underline' },
]);
return [theme, syntaxHighlighting(highlightStyle)];
}
}
// ============================================================================
// DISCORD
// ============================================================================
function createDiscordTheme(mode: 'light' | 'dark'): Extension[] {
if (mode === 'light') {
const theme = EditorView.theme({
'&': { backgroundColor: '#ffffff', color: '#232428' },
'.cm-content': { caretColor: '#5865f2' },
'.cm-cursor, .cm-dropCursor': { borderLeftColor: '#5865f2' },
'&.cm-focused .cm-selectionBackground, ::selection': { backgroundColor: 'rgba(88, 101, 242, 0.2)' },
'.cm-activeLine': { backgroundColor: '#f6f7f9' },
'.cm-gutters': { backgroundColor: '#f6f7f9', color: '#80848e', border: 'none' },
'.cm-activeLineGutter': { backgroundColor: '#eef0f5' },
}, { dark: false });
const highlightStyle = HighlightStyle.define([
{ tag: tags.keyword, color: '#5865f2', fontWeight: 'bold' },
{ tag: tags.comment, color: '#80848e', fontStyle: 'italic' },
{ tag: tags.string, color: '#23a55a' },
{ tag: tags.number, color: '#eb459e' },
{ tag: tags.bool, color: '#5865f2' },
{ tag: tags.variableName, color: '#232428' },
{ tag: tags.function(tags.variableName), color: '#3e63ff' },
{ tag: tags.heading, color: '#232428', fontWeight: 'bold' },
{ tag: tags.link, color: '#3e63ff', textDecoration: 'underline' },
]);
return [theme, syntaxHighlighting(highlightStyle)];
} else {
const theme = EditorView.theme({
'&': { backgroundColor: '#313338', color: '#f2f3f5' },
'.cm-content': { caretColor: '#5865f2' },
'.cm-cursor, .cm-dropCursor': { borderLeftColor: '#5865f2' },
'&.cm-focused .cm-selectionBackground, ::selection': { backgroundColor: 'rgba(64, 66, 73, 0.8)' },
'.cm-activeLine': { backgroundColor: '#383a40' },
'.cm-gutters': { backgroundColor: '#232428', color: '#80848e', border: 'none' },
'.cm-activeLineGutter': { backgroundColor: '#2b2d31' },
}, { dark: true });
const highlightStyle = HighlightStyle.define([
{ tag: tags.keyword, color: '#5865f2', fontWeight: 'bold' },
{ tag: tags.comment, color: '#80848e', fontStyle: 'italic' },
{ tag: tags.string, color: '#23a55a' },
{ tag: tags.number, color: '#eb459e' },
{ tag: tags.bool, color: '#5865f2' },
{ tag: tags.variableName, color: '#dbdee1' },
{ tag: tags.function(tags.variableName), color: '#71a1ff' },
{ tag: tags.heading, color: '#f2f3f5', fontWeight: 'bold' },
{ tag: tags.link, color: '#00a8fc', textDecoration: 'underline' },
]);
return [theme, syntaxHighlighting(highlightStyle)];
}
}
// ============================================================================
// MONOKAI
// ============================================================================
function createMonokaiTheme(mode: 'light' | 'dark'): Extension[] {
if (mode === 'light') {
const theme = EditorView.theme({
'&': { backgroundColor: '#ffffff', color: '#2b2b2b' },
'.cm-content': { caretColor: '#a6e22e' },
'.cm-cursor, .cm-dropCursor': { borderLeftColor: '#a6e22e' },
'&.cm-focused .cm-selectionBackground, ::selection': { backgroundColor: 'rgba(166, 226, 46, 0.2)' },
'.cm-activeLine': { backgroundColor: '#fbfbf7' },
'.cm-gutters': { backgroundColor: '#fbfbf7', color: '#8a8a8a', border: 'none' },
'.cm-activeLineGutter': { backgroundColor: '#f2f2ea' },
}, { dark: false });
const highlightStyle = HighlightStyle.define([
{ tag: tags.keyword, color: '#c7254e', fontWeight: 'bold' },
{ tag: tags.comment, color: '#8a8a8a', fontStyle: 'italic' },
{ tag: tags.string, color: '#d4c74e' },
{ tag: tags.number, color: '#ae81ff' },
{ tag: tags.bool, color: '#ae81ff' },
{ tag: tags.variableName, color: '#2b2b2b' },
{ tag: tags.function(tags.variableName), color: '#75aa1f' },
{ tag: tags.typeName, color: '#2b7b8e' },
{ tag: tags.heading, color: '#2b2b2b', fontWeight: 'bold' },
{ tag: tags.link, color: '#2b7b8e', textDecoration: 'underline' },
]);
return [theme, syntaxHighlighting(highlightStyle)];
} else {
const theme = EditorView.theme({
'&': { backgroundColor: '#272822', color: '#f8f8f2' },
'.cm-content': { caretColor: '#f8f8f0' },
'.cm-cursor, .cm-dropCursor': { borderLeftColor: '#f8f8f0' },
'&.cm-focused .cm-selectionBackground, ::selection': { backgroundColor: 'rgba(73, 72, 62, 0.8)' },
'.cm-activeLine': { backgroundColor: '#3e3d32' },
'.cm-gutters': { backgroundColor: '#23241f', color: '#90908a', border: 'none' },
'.cm-activeLineGutter': { backgroundColor: '#2d2e26' },
}, { dark: true });
const highlightStyle = HighlightStyle.define([
{ tag: tags.keyword, color: '#f92672', fontWeight: 'bold' },
{ tag: tags.comment, color: '#75715e', fontStyle: 'italic' },
{ tag: tags.string, color: '#e6db74' },
{ tag: tags.number, color: '#ae81ff' },
{ tag: tags.bool, color: '#ae81ff' },
{ tag: tags.variableName, color: '#f8f8f2' },
{ tag: tags.function(tags.variableName), color: '#a6e22e' },
{ tag: tags.typeName, color: '#66d9ef' },
{ tag: tags.heading, color: '#f8f8f2', fontWeight: 'bold' },
{ tag: tags.link, color: '#66d9ef', textDecoration: 'underline' },
]);
return [theme, syntaxHighlighting(highlightStyle)];
}
}

View File

@ -1,23 +1,46 @@
import { DOCUMENT } from '@angular/common'; import { DOCUMENT } from '@angular/common';
import { DestroyRef, Inject, Injectable, effect, signal, computed, inject } from '@angular/core'; import { DestroyRef, Inject, Injectable, effect, signal, computed, inject } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { LogService } from '../../../core/logging/log.service'; import { LogService } from '../../../core/logging/log.service';
import { cm6ThemeFor } from '../codemirror-themes';
export type ThemeName = 'light' | 'dark'; export type ThemeMode = 'system' | 'light' | 'dark';
export type ThemeId = 'light' | 'dark' | 'obsidian' | 'nord' | 'notion' | 'github' | 'discord' | 'monokai';
export type Language = 'fr' | 'en';
export interface ThemePrefs {
mode: ThemeMode;
theme: ThemeId;
language: Language;
}
const DEFAULT_PREFS: ThemePrefs = {
mode: 'system',
theme: 'light',
language: 'fr'
};
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class ThemeService { export class ThemeService {
private readonly logService = inject(LogService); private readonly logService = inject(LogService);
private static readonly STORAGE_KEY = 'obsiwatcher.theme'; private static readonly STORAGE_KEY = 'obsiviewer.preferences.v1';
private readonly document = this.doc; private readonly document = this.doc;
private readonly prefersDarkQuery = typeof window !== 'undefined' private readonly prefersDarkQuery = typeof window !== 'undefined'
? window.matchMedia('(prefers-color-scheme: dark)') ? window.matchMedia('(prefers-color-scheme: dark)')
: null; : null;
private readonly currentTheme = signal<ThemeName>(this.detectSystemTheme()); private prefs: ThemePrefs = { ...DEFAULT_PREFS };
private prefs$ = new BehaviorSubject<ThemePrefs>(this.prefs);
public readonly onPrefs$ = this.prefs$.asObservable();
private readonly currentTheme = signal<ThemeId>(this.prefs.theme);
private readonly currentMode = signal<ThemeMode>(this.prefs.mode);
readonly theme = computed(() => this.currentTheme()); readonly theme = computed(() => this.currentTheme());
readonly isDark = computed(() => this.currentTheme() === 'dark'); readonly mode = computed(() => this.currentMode());
readonly isDark = computed(() => this.resolveDark());
readonly language = computed(() => this.prefs.language);
constructor( constructor(
@Inject(DOCUMENT) private readonly doc: Document, @Inject(DOCUMENT) private readonly doc: Document,
@ -25,14 +48,15 @@ export class ThemeService {
) { ) {
effect(() => { effect(() => {
const theme = this.currentTheme(); const theme = this.currentTheme();
this.applyTheme(theme); const mode = this.currentMode();
this.persist(theme); this.applyToDom();
this.persist();
}); });
if (this.prefersDarkQuery) { if (this.prefersDarkQuery) {
const listener = (event: MediaQueryListEvent) => { const listener = (event: MediaQueryListEvent) => {
if (!this.getStoredTheme()) { if (this.prefs.mode === 'system') {
this.currentTheme.set(event.matches ? 'dark' : 'light'); this.applyToDom();
} }
}; };
this.prefersDarkQuery.addEventListener('change', listener); this.prefersDarkQuery.addEventListener('change', listener);
@ -43,19 +67,48 @@ export class ThemeService {
} }
initFromStorage(): void { initFromStorage(): void {
const stored = this.getStoredTheme(); try {
if (stored) { if (typeof window === 'undefined' || !window.localStorage) {
this.currentTheme.set(stored); this.applyToDom();
} else { return;
this.currentTheme.set(this.detectSystemTheme()); }
const raw = window.localStorage.getItem(ThemeService.STORAGE_KEY);
if (raw) {
this.prefs = { ...DEFAULT_PREFS, ...JSON.parse(raw) };
this.currentTheme.set(this.prefs.theme);
this.currentMode.set(this.prefs.mode);
}
} catch {
// Ignore storage errors
}
this.applyToDom();
this.prefs$.next(this.prefs);
}
setMode(mode: ThemeMode): void {
const previousMode = this.prefs.mode;
this.prefs.mode = mode;
this.currentMode.set(mode);
this.persist();
this.applyToDom();
this.prefs$.next(this.prefs);
if (previousMode !== mode) {
this.logService.log('THEME_MODE_CHANGE', {
from: previousMode,
to: mode,
});
} }
} }
setTheme(theme: ThemeName): void { setTheme(theme: ThemeId): void {
const previousTheme = this.currentTheme(); const previousTheme = this.prefs.theme;
this.prefs.theme = theme;
this.currentTheme.set(theme); this.currentTheme.set(theme);
this.persist();
this.applyToDom();
this.prefs$.next(this.prefs);
// Log theme change
if (previousTheme !== theme) { if (previousTheme !== theme) {
this.logService.log('THEME_CHANGE', { this.logService.log('THEME_CHANGE', {
from: previousTheme, from: previousTheme,
@ -64,57 +117,57 @@ export class ThemeService {
} }
} }
toggleTheme(): void { setLanguage(language: Language): void {
const previousTheme = this.currentTheme(); const previousLanguage = this.prefs.language;
this.currentTheme.update(theme => (theme === 'light' ? 'dark' : 'light')); this.prefs.language = language;
this.persist();
this.prefs$.next(this.prefs);
// Log theme change if (previousLanguage !== language) {
const newTheme = this.currentTheme(); this.logService.log('LANGUAGE_CHANGE', {
if (previousTheme !== newTheme) { from: previousLanguage,
this.logService.log('THEME_CHANGE', { to: language,
from: previousTheme,
to: newTheme,
}); });
} }
} }
private detectSystemTheme(): ThemeName { toggleTheme(): void {
if (typeof window === 'undefined') { const isDark = this.resolveDark();
return 'light'; this.setTheme(isDark ? 'light' : 'dark');
}
return window.matchMedia?.('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
} }
private applyTheme(theme: ThemeName): void { private resolveDark(): boolean {
if (this.prefs.mode === 'dark') return true;
if (this.prefs.mode === 'light') return false;
// mode === 'system'
return typeof window !== 'undefined' && window.matchMedia?.('(prefers-color-scheme: dark)').matches || false;
}
applyToDom(): void {
const root = this.document.documentElement; const root = this.document.documentElement;
root.setAttribute('data-theme', theme); const isDark = this.resolveDark();
if (theme === 'dark') {
root.classList.add('dark'); root.classList.toggle('dark', isDark);
} else { root.setAttribute('data-theme', this.prefs.theme);
root.classList.remove('dark');
}
} }
private persist(theme: ThemeName): void { private persist(): void {
try { try {
if (typeof window === 'undefined' || !window.localStorage) { if (typeof window === 'undefined' || !window.localStorage) {
return; return;
} }
window.localStorage.setItem(ThemeService.STORAGE_KEY, theme); window.localStorage.setItem(ThemeService.STORAGE_KEY, JSON.stringify(this.prefs));
} catch { } catch {
// Ignore storage failures (private browsing, etc.) // Ignore storage failures (private browsing, etc.)
} }
} }
private getStoredTheme(): ThemeName | null { get prefsValue(): ThemePrefs {
try { return { ...this.prefs };
if (typeof window === 'undefined' || !window.localStorage) {
return null;
}
const stored = window.localStorage.getItem(ThemeService.STORAGE_KEY) as ThemeName | null;
return stored === 'light' || stored === 'dark' ? stored : null;
} catch {
return null;
} }
getCodeMirrorExtensions(): any[] {
const isDark = this.resolveDark();
return cm6ThemeFor(this.prefs.theme, isDark ? 'dark' : 'light');
} }
} }

View File

@ -7,10 +7,10 @@ import { MobileNavService } from '../../shared/services/mobile-nav.service';
standalone: true, standalone: true,
imports: [CommonModule], imports: [CommonModule],
template: ` template: `
<nav class="fixed bottom-0 left-0 right-0 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-800 h-16 flex justify-around items-center z-50 safe-area-inset-bottom"> <nav class="fixed bottom-0 left-0 right-0 bg-card dark:bg-main border-t border-border dark:border-gray-800 h-16 flex justify-around items-center z-50 safe-area-inset-bottom">
<button *ngFor="let tab of tabs" <button *ngFor="let tab of tabs"
(click)="setActiveTab(tab.id)" (click)="setActiveTab(tab.id)"
class="relative flex-1 flex flex-col items-center justify-center gap-1 text-xs py-2 px-1 text-gray-600 dark:text-gray-400 transition-all duration-200 active:scale-95 transform" class="relative flex-1 flex flex-col items-center justify-center gap-1 text-xs py-2 px-1 text-muted dark:text-muted transition-all duration-200 active:scale-95 transform"
[class.text-nimbus-500]="mobileNav.activeTab() === tab.id" [class.text-nimbus-500]="mobileNav.activeTab() === tab.id"
[class.font-semibold]="mobileNav.activeTab() === tab.id"> [class.font-semibold]="mobileNav.activeTab() === tab.id">
<!-- Active indicator --> <!-- Active indicator -->

View File

@ -24,7 +24,7 @@
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
[class.text-red-500]="dirty() && !isSaving()" [class.text-red-500]="dirty() && !isSaving()"
[class.text-gray-400]="!dirty() && !isSaving()" [class.text-muted]="!dirty() && !isSaving()"
[class.text-yellow-500]="isSaving()" [class.text-yellow-500]="isSaving()"
[class.animate-pulse]="isSaving()" [class.animate-pulse]="isSaving()"
> >
@ -32,7 +32,7 @@
<polyline points="17 21 17 13 7 13 7 21"></polyline> <polyline points="17 21 17 13 7 13 7 21"></polyline>
<polyline points="7 3 7 8 15 8"></polyline> <polyline points="7 3 7 8 15 8"></polyline>
</svg> </svg>
<span class="text-xs font-medium" [class.text-red-500]="dirty() && !isSaving()" [class.text-gray-400]="!dirty() && !isSaving()" [class.text-yellow-500]="isSaving()"> <span class="text-xs font-medium" [class.text-red-500]="dirty() && !isSaving()" [class.text-muted]="!dirty() && !isSaving()" [class.text-yellow-500]="isSaving()">
{{isSaving() ? 'Sauvegarde...' : dirty() ? 'Non sauvegardé' : 'Sauvegardé'}} {{isSaving() ? 'Sauvegarde...' : dirty() ? 'Non sauvegardé' : 'Sauvegardé'}}
</span> </span>
</button> </button>

View File

@ -15,13 +15,15 @@ import { EditorView, keymap, lineNumbers, highlightActiveLine, drawSelection, dr
import { EditorState, Compartment } from '@codemirror/state'; import { EditorState, Compartment } from '@codemirror/state';
import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands'; import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands';
import { markdown, markdownLanguage } from '@codemirror/lang-markdown'; import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
import { syntaxHighlighting, defaultHighlightStyle, bracketMatching, foldGutter } from '@codemirror/language'; import { syntaxHighlighting, defaultHighlightStyle, bracketMatching, foldGutter, LanguageDescription } from '@codemirror/language';
import { searchKeymap, highlightSelectionMatches } from '@codemirror/search'; import { searchKeymap, highlightSelectionMatches } from '@codemirror/search';
import { autocompletion, completionKeymap, closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete'; import { autocompletion, completionKeymap, closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete';
import { lintKeymap } from '@codemirror/lint'; import { lintKeymap } from '@codemirror/lint';
import { VaultService } from '../../../services/vault.service'; import { VaultService } from '../../../services/vault.service';
import { EditorStateService } from '../../../services/editor-state.service'; import { EditorStateService } from '../../../services/editor-state.service';
import { ToastService } from '../../shared/toast/toast.service'; import { ToastService } from '../../shared/toast/toast.service';
import { ThemeService } from '../../core/services/theme.service';
import { EditorHighlightService } from '../../shared/editor/editor-highlight.service';
/** /**
* Composant d'édition Markdown avec CodeMirror 6 * Composant d'édition Markdown avec CodeMirror 6
@ -52,8 +54,7 @@ import { ToastService } from '../../shared/toast/toast.service';
<!-- Save Button --> <!-- Save Button -->
<button <button
type="button" type="button"
class="btn-editor" class="btn btn-solid btn-sm"
[class.btn-editor--primary]="isDirty()"
[disabled]="isSaving()" [disabled]="isSaving()"
(click)="save()" (click)="save()"
[attr.aria-label]="'Save (Ctrl+S)'"> [attr.aria-label]="'Save (Ctrl+S)'">
@ -71,8 +72,7 @@ import { ToastService } from '../../shared/toast/toast.service';
<!-- Wrap Toggle --> <!-- Wrap Toggle -->
<button <button
type="button" type="button"
class="btn-editor" class="btn btn-outline btn-sm"
[class.btn-editor--active]="wordWrap()"
(click)="toggleWordWrap()" (click)="toggleWordWrap()"
[attr.aria-label]="'Toggle word wrap'"> [attr.aria-label]="'Toggle word wrap'">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
@ -86,7 +86,7 @@ import { ToastService } from '../../shared/toast/toast.service';
<!-- Undo --> <!-- Undo -->
<button <button
type="button" type="button"
class="btn-editor hidden sm:flex" class="btn btn-ghost btn-sm hidden sm:flex"
(click)="undo()" (click)="undo()"
[attr.aria-label]="'Undo (Ctrl+Z)'"> [attr.aria-label]="'Undo (Ctrl+Z)'">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
@ -98,7 +98,7 @@ import { ToastService } from '../../shared/toast/toast.service';
<!-- Redo --> <!-- Redo -->
<button <button
type="button" type="button"
class="btn-editor hidden sm:flex" class="btn btn-ghost btn-sm hidden sm:flex"
(click)="redo()" (click)="redo()"
[attr.aria-label]="'Redo (Ctrl+Y)'"> [attr.aria-label]="'Redo (Ctrl+Y)'">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
@ -110,7 +110,7 @@ import { ToastService } from '../../shared/toast/toast.service';
<!-- Close/Cancel --> <!-- Close/Cancel -->
<button <button
type="button" type="button"
class="btn-editor" class="btn btn-outline btn-sm"
(click)="close()" (click)="close()"
[attr.aria-label]="'Close editor (Esc)'"> [attr.aria-label]="'Close editor (Esc)'">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
@ -171,49 +171,7 @@ import { ToastService } from '../../shared/toast/toast.service';
flex-wrap: wrap; flex-wrap: wrap;
} }
.btn-editor {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
font-weight: 500;
color: var(--text-muted);
background: transparent;
border: 1px solid var(--border);
border-radius: 0.375rem;
cursor: pointer;
transition: all 0.15s ease;
white-space: nowrap;
}
.btn-editor:hover:not(:disabled) {
background: var(--bg-hover);
border-color: var(--border-hover);
color: var(--text);
}
.btn-editor:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-editor--primary {
background: var(--brand);
color: white;
border-color: var(--brand);
}
.btn-editor--primary:hover:not(:disabled) {
background: var(--brand-hover);
border-color: var(--brand-hover);
}
.btn-editor--active {
background: var(--brand);
color: white;
border-color: var(--brand);
}
.markdown-editor__container { .markdown-editor__container {
flex: 1; flex: 1;
@ -289,6 +247,7 @@ export class MarkdownEditorComponent implements OnInit, OnDestroy {
private vaultService = inject(VaultService); private vaultService = inject(VaultService);
private editorStateService = inject(EditorStateService); private editorStateService = inject(EditorStateService);
private toastService = inject(ToastService); private toastService = inject(ToastService);
private themeService = inject(ThemeService);
// Signals (public pour utilisation dans le template) // Signals (public pour utilisation dans le template)
filePath = signal<string>(''); filePath = signal<string>('');
@ -310,13 +269,75 @@ export class MarkdownEditorComponent implements OnInit, OnDestroy {
// CodeMirror // CodeMirror
private editorView?: EditorView; private editorView?: EditorView;
private wrapCompartment = new Compartment(); private wrapCompartment = new Compartment();
private themeCompartment = new Compartment();
private occurrencesCompartment = new Compartment();
private highlightFacetCompartment = new Compartment();
private autosaveTimer?: ReturnType<typeof setTimeout>; private autosaveTimer?: ReturnType<typeof setTimeout>;
private themeSubscription?: any;
private highlightService = inject(EditorHighlightService);
private readonly markdownCodeLanguages = [
LanguageDescription.of({
name: 'javascript',
alias: ['js', 'node', 'jsx', 'mjs', 'cjs'],
load: () => import('@codemirror/lang-javascript').then(m => m.javascript())
}),
LanguageDescription.of({
name: 'typescript',
alias: ['ts', 'tsx'],
load: () => import('@codemirror/lang-javascript').then(m => m.javascript({ typescript: true, jsx: true }))
}),
LanguageDescription.of({
name: 'json',
load: () => import('@codemirror/lang-json').then(m => m.json())
}),
LanguageDescription.of({
name: 'html',
alias: ['xml', 'svg'],
load: () => import('@codemirror/lang-html').then(m => m.html())
}),
LanguageDescription.of({
name: 'css',
alias: ['scss', 'less'],
load: () => import('@codemirror/lang-css').then(m => m.css())
}),
LanguageDescription.of({
name: 'python',
alias: ['py'],
load: () => import('@codemirror/lang-python').then(m => m.python())
}),
LanguageDescription.of({
name: 'go',
load: () => import('@codemirror/lang-go').then(m => m.go())
}),
LanguageDescription.of({
name: 'rust',
load: () => import('@codemirror/lang-rust').then(m => m.rust())
}),
LanguageDescription.of({
name: 'php',
load: () => import('@codemirror/lang-php').then(m => m.php())
}),
LanguageDescription.of({
name: 'sql',
load: () => import('@codemirror/lang-sql').then(m => m.sql())
}),
LanguageDescription.of({
name: 'java',
load: () => import('@codemirror/lang-java').then(m => m.java())
}),
LanguageDescription.of({
name: 'yaml',
alias: ['yml'],
load: () => import('@codemirror/lang-yaml').then(m => m.yaml())
})
];
constructor() { constructor() {
// Détecter le thème // S'abonner aux changements de thème
effect(() => { effect(() => {
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
const darkMode = document.documentElement.classList.contains('dark'); const darkMode = this.themeService.isDark();
this.isDarkTheme.set(darkMode); this.isDarkTheme.set(darkMode);
} }
}); });
@ -324,7 +345,7 @@ export class MarkdownEditorComponent implements OnInit, OnDestroy {
ngOnInit(): void { ngOnInit(): void {
this.initializeEditor(); this.initializeEditor();
this.setupThemeObserver(); this.subscribeToThemeChanges();
} }
ngOnDestroy(): void { ngOnDestroy(): void {
@ -353,9 +374,12 @@ export class MarkdownEditorComponent implements OnInit, OnDestroy {
autocompletion(), autocompletion(),
highlightSelectionMatches(), highlightSelectionMatches(),
foldGutter(), foldGutter(),
syntaxHighlighting(defaultHighlightStyle), markdown({ base: markdownLanguage, addKeymap: true, codeLanguages: this.markdownCodeLanguages }),
markdown({ base: markdownLanguage }), ...this.highlightService.extensions,
this.occurrencesCompartment.of([]),
this.wrapCompartment.of(this.wordWrap() ? EditorView.lineWrapping : []), this.wrapCompartment.of(this.wordWrap() ? EditorView.lineWrapping : []),
this.highlightFacetCompartment.of([]),
this.themeCompartment.of(this.themeService.getCodeMirrorExtensions()),
keymap.of([ keymap.of([
saveCommand, saveCommand,
...defaultKeymap, ...defaultKeymap,
@ -373,30 +397,6 @@ export class MarkdownEditorComponent implements OnInit, OnDestroy {
if (update.selectionSet) { if (update.selectionSet) {
this.updateCursorPosition(); this.updateCursorPosition();
} }
}),
EditorView.theme({
'&': {
height: '100%',
backgroundColor: this.isDarkTheme() ? '#1e293b' : '#ffffff'
},
'.cm-content': {
caretColor: this.isDarkTheme() ? '#60a5fa' : '#3b82f6',
color: this.isDarkTheme() ? '#e2e8f0' : '#1e293b'
},
'.cm-cursor': {
borderLeftColor: this.isDarkTheme() ? '#60a5fa' : '#3b82f6'
},
'.cm-activeLine': {
backgroundColor: this.isDarkTheme() ? '#334155' : '#f1f5f9'
},
'.cm-gutters': {
backgroundColor: this.isDarkTheme() ? '#1e293b' : '#f8fafc',
color: this.isDarkTheme() ? '#64748b' : '#94a3b8',
border: 'none'
},
'.cm-activeLineGutter': {
backgroundColor: this.isDarkTheme() ? '#334155' : '#e2e8f0'
}
}) })
] ]
}); });
@ -409,52 +409,21 @@ export class MarkdownEditorComponent implements OnInit, OnDestroy {
this.updateCursorPosition(); this.updateCursorPosition();
} }
private setupThemeObserver(): void { private subscribeToThemeChanges(): void {
if (typeof window === 'undefined') return; this.themeSubscription = this.themeService.onPrefs$.subscribe(() => {
this.updateEditorTheme();
const observer = new MutationObserver(() => {
const darkMode = document.documentElement.classList.contains('dark');
this.isDarkTheme.set(darkMode);
this.reconfigureTheme();
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class']
}); });
} }
private reconfigureTheme(): void { private updateEditorTheme(): void {
if (!this.editorView) return; if (!this.editorView) return;
const newTheme = EditorView.theme({ const newThemeExtensions = this.themeService.getCodeMirrorExtensions();
'&': {
height: '100%',
backgroundColor: this.isDarkTheme() ? '#1e293b' : '#ffffff'
},
'.cm-content': {
caretColor: this.isDarkTheme() ? '#60a5fa' : '#3b82f6',
color: this.isDarkTheme() ? '#e2e8f0' : '#1e293b'
},
'.cm-cursor': {
borderLeftColor: this.isDarkTheme() ? '#60a5fa' : '#3b82f6'
},
'.cm-activeLine': {
backgroundColor: this.isDarkTheme() ? '#334155' : '#f1f5f9'
},
'.cm-gutters': {
backgroundColor: this.isDarkTheme() ? '#1e293b' : '#f8fafc',
color: this.isDarkTheme() ? '#64748b' : '#94a3b8',
border: 'none'
},
'.cm-activeLineGutter': {
backgroundColor: this.isDarkTheme() ? '#334155' : '#e2e8f0'
}
});
this.editorView.dispatch({ this.editorView.dispatch({
effects: [] effects: this.themeCompartment.reconfigure(newThemeExtensions)
}); });
// trigger a light re-measure so CSS variable based highlights pick up instantly
this.editorView.requestMeasure({ read: () => null, write: () => {} });
} }
private onContentChange(newContent: string): void { private onContentChange(newContent: string): void {
@ -474,6 +443,28 @@ export class MarkdownEditorComponent implements OnInit, OnDestroy {
this.cursorCol.set(pos - line.from + 1); this.cursorCol.set(pos - line.from + 1);
} }
// Public API: highlight helpers
highlightOccurrences(pattern: string | RegExp): void {
if (!this.editorView) return;
this.highlightService.applyOccurrences(this.editorView, this.occurrencesCompartment, pattern);
}
setHighlights(ranges: { from: number; to: number }[]): void {
if (!this.editorView) return;
this.highlightService.setRanges(this.editorView, ranges);
}
clearHighlights(): void {
if (!this.editorView) return;
this.highlightService.clearRanges(this.editorView);
}
setHighlightColor(cssColor: string | null): void {
if (!this.editorView) return;
const ext = cssColor ? [ (this.highlightService.facet() as any).of(cssColor) ] : [];
this.editorView.dispatch({ effects: this.highlightFacetCompartment.reconfigure(ext) });
}
toggleWordWrap(): void { toggleWordWrap(): void {
this.wordWrap.update(v => !v); this.wordWrap.update(v => !v);
@ -559,7 +550,9 @@ export class MarkdownEditorComponent implements OnInit, OnDestroy {
if (this.autosaveTimer) { if (this.autosaveTimer) {
clearTimeout(this.autosaveTimer); clearTimeout(this.autosaveTimer);
} }
if (this.themeSubscription) {
this.themeSubscription.unsubscribe();
}
if (this.editorView) { if (this.editorView) {
this.editorView.destroy(); this.editorView.destroy();
} }

View File

@ -11,13 +11,13 @@ import { TagFilterStore } from '../../core/stores/tag-filter.store';
imports: [CommonModule, ScrollableOverlayDirective], imports: [CommonModule, ScrollableOverlayDirective],
template: ` template: `
<div class="h-full flex flex-col"> <div class="h-full flex flex-col">
<div class="p-2 border-b border-gray-200 dark:border-gray-800 space-y-2"> <div class="p-2 border-b border-border dark:border-gray-800 space-y-2">
<div *ngIf="activeTag() as t" class="flex items-center gap-2 text-xs"> <div *ngIf="activeTag() as t" class="flex items-center gap-2 text-xs">
<span class="inline-flex items-center gap-1 rounded-full bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-200 px-2 py-1"> <span class="inline-flex items-center gap-1 rounded-full bg-surface1 dark:bg-card text-main dark:text-main px-2 py-1">
<svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 7h.01"/><path d="M3 7h5a2 2 0 0 1 1.414.586l7 7a2 2 0 0 1 0 2.828l-3.172 3.172a2 2 0 0 1-2.828 0l-7-7A2 2 0 0 1 3 12V7Z"/></svg> <svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 7h.01"/><path d="M3 7h5a2 2 0 0 1 1.414.586l7 7a2 2 0 0 1 0 2.828l-3.172 3.172a2 2 0 0 1-2.828 0l-7-7A2 2 0 0 1 3 12V7Z"/></svg>
Filtre: #{{ t }} Filtre: #{{ t }}
</span> </span>
<button type="button" (click)="clearTagFilter()" class="rounded-full hover:bg-slate-500/10 dark:hover:bg-slate-200/10 w-6 h-6 inline-flex items-center justify-center" title="Effacer le filtre"> <button type="button" (click)="clearTagFilter()" class="rounded-full hover:bg-slate-500/10 dark:hover:bg-surface2/10 w-6 h-6 inline-flex items-center justify-center" title="Effacer le filtre">
<svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg> <svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
</button> </button>
</div> </div>
@ -25,13 +25,13 @@ import { TagFilterStore } from '../../core/stores/tag-filter.store';
[value]="query()" [value]="query()"
(input)="onQuery($any($event.target).value)" (input)="onQuery($any($event.target).value)"
placeholder="Rechercher..." placeholder="Rechercher..."
class="w-full rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 px-3 py-2 text-sm" /> class="w-full rounded border border-border dark:border-border bg-card dark:bg-main px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring" />
</div> </div>
<div class="flex-1 min-h-0 overflow-y-auto list-scroll" appScrollableOverlay> <div class="flex-1 min-h-0 overflow-y-auto list-scroll" appScrollableOverlay>
<ul class="divide-y divide-gray-100 dark:divide-gray-800"> <ul class="divide-y divide-gray-100 dark:divide-gray-800">
<li *ngFor="let n of filtered()" class="p-3 hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer" (click)="openNote.emit(n.id)"> <li *ngFor="let n of filtered()" class="p-3 hover:bg-surface1 dark:hover:bg-card cursor-pointer" (click)="openNote.emit(n.id)">
<div class="text-sm font-semibold truncate">{{ n.title }}</div> <div class="text-sm font-semibold truncate">{{ n.title }}</div>
<div class="text-xs text-gray-500 truncate">{{ n.filePath }}</div> <div class="text-xs text-muted truncate">{{ n.filePath }}</div>
</li> </li>
</ul> </ul>
</div> </div>

View File

@ -20,7 +20,7 @@ import { ScrollableOverlayDirective } from '../../shared/overlay-scrollbar/scrol
<!-- Bottom sheet --> <!-- Bottom sheet -->
<div <div
class="fixed inset-x-0 bottom-0 z-[81] max-h-[85vh] rounded-t-2xl bg-white dark:bg-slate-900 shadow-2xl class="fixed inset-x-0 bottom-0 z-[81] max-h-[85vh] rounded-t-2xl bg-card dark:bg-main shadow-2xl
transition-transform duration-300 ease-out transition-transform duration-300 ease-out
sm:left-auto sm:right-4 sm:bottom-4 sm:top-auto sm:w-[420px] sm:rounded-2xl sm:max-h-[80vh]" sm:left-auto sm:right-4 sm:bottom-4 sm:top-auto sm:w-[420px] sm:rounded-2xl sm:max-h-[80vh]"
[class.translate-y-full]="!isVisible" [class.translate-y-full]="!isVisible"
@ -31,20 +31,20 @@ import { ScrollableOverlayDirective } from '../../shared/overlay-scrollbar/scrol
(click)="$event.stopPropagation()"> (click)="$event.stopPropagation()">
<div class="flex items-center justify-center py-3 sm:py-2"> <div class="flex items-center justify-center py-3 sm:py-2">
<div class="h-1.5 w-12 rounded-full bg-slate-300 dark:bg-slate-700"></div> <div class="h-1.5 w-12 rounded-full bg-muted dark:bg-surface2"></div>
</div> </div>
<div class="px-4 pb-4 overflow-y-auto max-h-[75vh] sm:max-h-[70vh]" appScrollableOverlay> <div class="px-4 pb-4 overflow-y-auto max-h-[75vh] sm:max-h-[70vh]" appScrollableOverlay>
<h2 class="sr-only">Sommaire</h2> <h2 class="sr-only">Sommaire</h2>
<ul class="space-y-1 text-sm text-slate-800 dark:text-slate-200"> <ul class="space-y-1 text-sm text-main dark:text-main">
<li *ngFor="let h of headings; let i = index"> <li *ngFor="let h of headings; let i = index">
<button <button
type="button" type="button"
(click)="onGo(h.id)" (click)="onGo(h.id)"
class="w-full text-left block px-3 py-2 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition active:scale-[0.98]" class="w-full text-left block px-3 py-2 rounded-lg hover:bg-surface1 dark:hover:bg-card transition active:scale-[0.98]"
[style.paddingLeft.rem]="(h.level - 1) * 0.75 + 0.75" [style.paddingLeft.rem]="(h.level - 1) * 0.75 + 0.75"
[attr.data-index]="i"> [attr.data-index]="i">
<span class="text-slate-400 dark:text-slate-500 mr-2 text-xs">{{ getLevelIndicator(h.level) }}</span> <span class="text-muted dark:text-muted mr-2 text-xs">{{ getLevelIndicator(h.level) }}</span>
<span>{{ h.text }}</span> <span>{{ h.text }}</span>
</button> </button>
</li> </li>

View File

@ -1,7 +1,7 @@
<header class="note-header flex flex-col gap-2 min-w-0"> <header class="note-header flex flex-col gap-2 min-w-0">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button type="button" <button type="button"
class="flex-shrink-0 inline-flex items-center justify-center w-8 h-8 rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-700 hover:text-slate-900 dark:hover:text-slate-200 transition-all duration-150 shadow-sm" class="flex-shrink-0 inline-flex items-center justify-center w-8 h-8 rounded-lg border border-border dark:border-border bg-card dark:bg-card text-muted dark:text-muted hover:bg-surface1 dark:hover:bg-surface2 hover:text-main dark:hover:text-main transition-all duration-150 shadow-sm"
aria-label="Copier le chemin" aria-label="Copier le chemin"
title="Copier le chemin" title="Copier le chemin"
(click)="copyRequested.emit()"> (click)="copyRequested.emit()">

View File

@ -0,0 +1,285 @@
.parameters-page {
min-height: 100vh;
background: var(--bg-main);
padding: 2rem 1rem;
}
.parameters-container {
max-width: 56rem;
margin: 0 auto;
}
/* Header */
.parameters-header {
margin-bottom: 2.5rem;
}
.parameters-title {
font-size: 2rem;
font-weight: 700;
color: var(--text-main);
margin: 0 0 0.5rem 0;
}
.parameters-subtitle {
font-size: 1rem;
color: var(--text-muted);
margin: 0;
}
/* Sections */
.parameters-section {
background: var(--card);
border: 1px solid var(--border);
border-radius: 0.75rem;
padding: 1.75rem;
margin-bottom: 1.5rem;
transition: all var(--transition-base);
}
.section-title {
display: flex;
align-items: center;
gap: 0.625rem;
font-size: 1.25rem;
font-weight: 600;
color: var(--text-main);
margin: 0 0 1.5rem 0;
}
.section-icon {
color: var(--primary);
flex-shrink: 0;
}
/* Grid Layout */
.grid-layout {
display: grid;
gap: 2rem;
}
.setting-group {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.setting-group.full-width {
grid-column: 1 / -1;
}
.setting-label {
font-size: 0.875rem;
font-weight: 600;
color: var(--text-main);
display: block;
}
.setting-hint {
font-size: 0.8125rem;
color: var(--text-muted);
margin: 0;
line-height: 1.5;
}
/* Button Group */
.button-group {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.mode-button {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.625rem 1rem;
background: var(--bg-muted);
border: 1.5px solid var(--border);
border-radius: 0.5rem;
color: var(--text-main);
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all var(--transition-base);
min-width: 6.5rem;
justify-content: center;
}
.mode-button:hover {
background: var(--bg-main);
border-color: var(--primary);
transform: translateY(-1px);
}
.mode-button.active {
background: var(--primary);
border-color: var(--primary);
color: white;
}
.button-icon {
flex-shrink: 0;
}
.button-text {
flex-shrink: 0;
}
/* Theme Grid */
.theme-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
gap: 1rem;
}
.theme-card {
display: flex;
flex-direction: column;
background: var(--bg-muted);
border: 2px solid var(--border);
border-radius: 0.625rem;
overflow: hidden;
cursor: pointer;
transition: all var(--transition-base);
padding: 0;
}
.theme-card:hover {
border-color: var(--primary);
transform: translateY(-2px);
box-shadow: 0 4px 12px var(--shadow-color);
}
.theme-card.active {
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.theme-preview {
height: 4rem;
width: 100%;
border-bottom: 1px solid var(--border);
}
.theme-info {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 1rem;
gap: 0.5rem;
}
.theme-name {
font-size: 0.875rem;
font-weight: 500;
color: var(--text-main);
}
.theme-check {
color: var(--primary);
flex-shrink: 0;
}
/* Stats Placeholder */
.stats-placeholder {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: 3rem 1.5rem;
background: var(--bg-muted);
border: 1px dashed var(--border);
border-radius: 0.75rem;
}
.stats-icon {
color: var(--text-muted);
margin-bottom: 1rem;
opacity: 0.5;
}
.stats-title {
font-size: 1.125rem;
font-weight: 600;
color: var(--text-main);
margin: 0 0 0.5rem 0;
}
.stats-description {
font-size: 0.875rem;
color: var(--text-muted);
margin: 0 0 1.5rem 0;
max-width: 32rem;
line-height: 1.6;
}
.stats-button {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.625rem 1.25rem;
background: var(--bg-main);
border: 1px solid var(--border);
border-radius: 0.5rem;
color: var(--text-muted);
font-size: 0.875rem;
font-weight: 500;
cursor: not-allowed;
opacity: 0.6;
}
/* Footer */
.parameters-footer {
margin-top: 2rem;
padding-top: 1.5rem;
border-top: 1px solid var(--border);
}
.footer-note {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.8125rem;
color: var(--text-muted);
margin: 0;
}
.footer-note svg {
flex-shrink: 0;
opacity: 0.7;
}
/* Responsive */
@media (min-width: 640px) {
.parameters-page {
padding: 3rem 2rem;
}
.theme-grid {
grid-template-columns: repeat(4, 1fr);
}
}
@media (max-width: 639px) {
.parameters-title {
font-size: 1.5rem;
}
.parameters-section {
padding: 1.25rem;
}
.section-title {
font-size: 1.125rem;
}
.theme-grid {
grid-template-columns: repeat(2, 1fr);
}
.mode-button {
flex: 1;
min-width: auto;
}
}

View File

@ -0,0 +1,156 @@
<div class="parameters-page">
<div class="parameters-container">
<!-- Header -->
<header class="parameters-header">
<h1 class="parameters-title">Parameters</h1>
<p class="parameters-subtitle">Customize your ObsiViewer experience</p>
</header>
<!-- Appearance Section -->
<section class="parameters-section">
<h2 class="section-title">
<svg class="section-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="5"/>
<line x1="12" y1="1" x2="12" y2="3"/>
<line x1="12" y1="21" x2="12" y2="23"/>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
<line x1="1" y1="12" x2="3" y2="12"/>
<line x1="21" y1="12" x2="23" y2="12"/>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
</svg>
Appearance
</h2>
<div class="grid-layout">
<!-- Mode Selection -->
<div class="setting-group">
<label class="setting-label">Display Mode</label>
<div class="button-group">
<button
*ngFor="let m of modes"
(click)="setMode(m)"
class="mode-button"
[class.active]="prefs().mode === m"
[attr.aria-label]="'Set mode to ' + m">
<svg *ngIf="m === 'system'" class="button-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/>
<line x1="8" y1="21" x2="16" y2="21"/>
<line x1="12" y1="17" x2="12" y2="21"/>
</svg>
<svg *ngIf="m === 'light'" class="button-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="5"/>
<line x1="12" y1="1" x2="12" y2="3"/>
<line x1="12" y1="21" x2="12" y2="23"/>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
<line x1="1" y1="12" x2="3" y2="12"/>
<line x1="21" y1="12" x2="23" y2="12"/>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
</svg>
<svg *ngIf="m === 'dark'" class="button-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
</svg>
<span class="button-text">{{ m | titlecase }}</span>
</button>
</div>
<p class="setting-hint">Choose how ObsiViewer determines light or dark appearance</p>
</div>
<!-- Theme Selection -->
<div class="setting-group full-width">
<label class="setting-label">Color Theme</label>
<div class="theme-grid">
<button
*ngFor="let t of themes"
(click)="setTheme(t)"
class="theme-card"
[class.active]="prefs().theme === t"
[attr.aria-label]="'Set theme to ' + themeLabels[t]">
<div class="theme-preview" [style.background]="getThemePreview(t)"></div>
<div class="theme-info">
<span class="theme-name">{{ themeLabels[t] }}</span>
<svg *ngIf="prefs().theme === t" class="theme-check" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="20 6 9 17 4 12"/>
</svg>
</div>
</button>
</div>
<p class="setting-hint">Select a color scheme for the entire application including the editor</p>
</div>
</div>
</section>
<!-- Language Section -->
<section class="parameters-section">
<h2 class="section-title">
<svg class="section-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/>
<line x1="2" y1="12" x2="22" y2="12"/>
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
</svg>
Language
</h2>
<div class="setting-group">
<label class="setting-label">Interface Language</label>
<div class="button-group">
<button
*ngFor="let l of languages"
(click)="setLanguage(l)"
class="mode-button"
[class.active]="prefs().language === l"
[attr.aria-label]="'Set language to ' + l">
<span class="button-text">{{ l === 'fr' ? 'Français' : 'English' }}</span>
</button>
</div>
<p class="setting-hint">Interface language (i18n implementation coming soon)</p>
</div>
</section>
<!-- Vault Stats Section (Placeholder) -->
<section class="parameters-section">
<h2 class="section-title">
<svg class="section-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="20" x2="18" y2="10"/>
<line x1="12" y1="20" x2="12" y2="4"/>
<line x1="6" y1="20" x2="6" y2="14"/>
</svg>
Vault Statistics
</h2>
<div class="stats-placeholder">
<svg class="stats-icon" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 3v18h18"/>
<path d="M18 17V9"/>
<path d="M13 17V5"/>
<path d="M8 17v-3"/>
</svg>
<h3 class="stats-title">Vault Analytics</h3>
<p class="stats-description">Coming soon: View detailed statistics about your vault including note count, tag usage, file sizes, and more.</p>
<button class="stats-button" disabled>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="20" x2="18" y2="10"/>
<line x1="12" y1="20" x2="12" y2="4"/>
<line x1="6" y1="20" x2="6" y2="14"/>
</svg>
View Stats (Coming Soon)
</button>
</div>
</section>
<!-- Footer -->
<footer class="parameters-footer">
<p class="footer-note">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/>
<line x1="12" y1="16" x2="12" y2="12"/>
<line x1="12" y1="8" x2="12.01" y2="8"/>
</svg>
Changes are saved automatically and applied immediately
</p>
</footer>
</div>
</div>

View File

@ -0,0 +1,80 @@
import { Component, inject, signal, effect } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ThemeService, ThemeMode, ThemeId, Language } from '../../core/services/theme.service';
import { ToastService } from '../../shared/toast/toast.service';
@Component({
standalone: true,
selector: 'app-parameters',
imports: [CommonModule],
templateUrl: './parameters.page.html',
styleUrls: ['./parameters.page.css']
})
export class ParametersPage {
private themeService = inject(ThemeService);
private toastService = inject(ToastService);
// Reactive prefs
prefs = signal(this.themeService.prefsValue);
modes: ThemeMode[] = ['system', 'light', 'dark'];
themes: ThemeId[] = ['light', 'dark', 'obsidian', 'nord', 'notion', 'github', 'discord', 'monokai'];
languages: Language[] = ['fr', 'en'];
// Labels pour les thèmes
themeLabels: Record<ThemeId, string> = {
light: 'Pure White',
dark: 'Blue',
obsidian: 'Obsidian',
nord: 'Nord',
notion: 'Notion',
github: 'GitHub',
discord: 'Discord',
monokai: 'Monokai'
};
constructor() {
// S'abonner aux changements de préférences
effect(() => {
const subscription = this.themeService.onPrefs$.subscribe(prefs => {
this.prefs.set(prefs);
});
return () => subscription.unsubscribe();
});
}
setMode(mode: ThemeMode): void {
this.themeService.setMode(mode);
this.showToast('Mode changed successfully');
}
setTheme(theme: ThemeId): void {
this.themeService.setTheme(theme);
this.showToast('Theme changed successfully');
}
setLanguage(language: Language): void {
this.themeService.setLanguage(language);
this.showToast('Language changed successfully');
}
private showToast(message: string): void {
this.toastService.success(message, 2000);
}
getThemePreview(themeId: ThemeId): string {
// Couleurs de preview pour chaque thème
const previews: Record<ThemeId, string> = {
light: 'linear-gradient(135deg, #ffffff 0%, #f7f7f7 100%)',
dark: 'linear-gradient(135deg, #0f172a 0%, #1e293b 100%)',
obsidian: 'linear-gradient(135deg, #fafaf8 0%, #a89984 100%)',
nord: 'linear-gradient(135deg, #2e3440 0%, #88c0d0 100%)',
notion: 'linear-gradient(135deg, #ffffff 0%, #f7f7f5 100%)',
github: 'linear-gradient(135deg, #ffffff 0%, #0969da 100%)',
discord: 'linear-gradient(135deg, #313338 0%, #5865f2 100%)',
monokai: 'linear-gradient(135deg, #272822 0%, #a6e22e 100%)'
};
return previews[themeId];
}
}

View File

@ -22,49 +22,49 @@ interface QuickLinkCountsUi {
<div class="p-3"> <div class="p-3">
<ul class="text-sm space-y-1"> <ul class="text-sm space-y-1">
<li> <li>
<button (click)="select('all')" class="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-500/10 dark:hover:bg-slate-200/10 transition w-full text-left"> <button (click)="select('all')" class="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-500/10 dark:hover:bg-surface2/10 transition w-full text-left">
<span class="flex items-center gap-2"><span>🗂</span> <span>All Pages</span></span> <span class="flex items-center gap-2"><span>🗂</span> <span>All Pages</span></span>
<app-badge-count class="ml-auto" [count]="counts().all" color="slate"></app-badge-count> <app-badge-count class="ml-auto" [count]="counts().all" color="slate"></app-badge-count>
</button> </button>
</li> </li>
<li> <li>
<button (click)="select('favorites')" class="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-500/10 dark:hover:bg-slate-200/10 transition w-full text-left"> <button (click)="select('favorites')" class="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-500/10 dark:hover:bg-surface2/10 transition w-full text-left">
<span class="flex items-center gap-2"><span></span> <span>Favoris</span></span> <span class="flex items-center gap-2"><span></span> <span>Favoris</span></span>
<app-badge-count class="ml-auto" [count]="counts().favorites" color="rose"></app-badge-count> <app-badge-count class="ml-auto" [count]="counts().favorites" color="rose"></app-badge-count>
</button> </button>
</li> </li>
<li> <li>
<button (click)="select('publish')" class="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-500/10 dark:hover:bg-slate-200/10 transition w-full text-left"> <button (click)="select('publish')" class="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-500/10 dark:hover:bg-surface2/10 transition w-full text-left">
<span class="flex items-center gap-2"><span>🌐</span> <span>Publish</span></span> <span class="flex items-center gap-2"><span>🌐</span> <span>Publish</span></span>
<app-badge-count class="ml-auto" [count]="counts().publish" color="green"></app-badge-count> <app-badge-count class="ml-auto" [count]="counts().publish" color="green"></app-badge-count>
</button> </button>
</li> </li>
<li> <li>
<button (click)="select('drafts')" class="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-500/10 dark:hover:bg-slate-200/10 transition w-full text-left"> <button (click)="select('drafts')" class="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-500/10 dark:hover:bg-surface2/10 transition w-full text-left">
<span class="flex items-center gap-2"><span>📝</span> <span>Draft</span></span> <span class="flex items-center gap-2"><span>📝</span> <span>Draft</span></span>
<app-badge-count class="ml-auto" [count]="counts().drafts" color="emerald"></app-badge-count> <app-badge-count class="ml-auto" [count]="counts().drafts" color="emerald"></app-badge-count>
</button> </button>
</li> </li>
<li> <li>
<button (click)="select('templates')" class="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-500/10 dark:hover:bg-slate-200/10 transition w-full text left"> <button (click)="select('templates')" class="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-500/10 dark:hover:bg-surface2/10 transition w-full text left">
<span class="flex items-center gap-2"><span>📑</span> <span>Template</span></span> <span class="flex items-center gap-2"><span>📑</span> <span>Template</span></span>
<app-badge-count class="ml-auto" [count]="counts().templates" color="amber"></app-badge-count> <app-badge-count class="ml-auto" [count]="counts().templates" color="amber"></app-badge-count>
</button> </button>
</li> </li>
<li> <li>
<button (click)="select('tasks')" class="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-500/10 dark:hover:bg-slate-200/10 transition w-full text-left"> <button (click)="select('tasks')" class="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-500/10 dark:hover:bg-surface2/10 transition w-full text-left">
<span class="flex items-center gap-2"><span>🗒</span> <span>Task</span></span> <span class="flex items-center gap-2"><span>🗒</span> <span>Task</span></span>
<app-badge-count class="ml-auto" [count]="counts().tasks" color="indigo"></app-badge-count> <app-badge-count class="ml-auto" [count]="counts().tasks" color="indigo"></app-badge-count>
</button> </button>
</li> </li>
<li> <li>
<button (click)="select('private')" class="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-500/10 dark:hover:bg-slate-200/10 transition w-full text-left"> <button (click)="select('private')" class="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-500/10 dark:hover:bg-surface2/10 transition w-full text-left">
<span class="flex items-center gap-2"><span>🔒</span> <span>Private</span></span> <span class="flex items-center gap-2"><span>🔒</span> <span>Private</span></span>
<app-badge-count class="ml-auto" [count]="counts().private" color="purple"></app-badge-count> <app-badge-count class="ml-auto" [count]="counts().private" color="purple"></app-badge-count>
</button> </button>
</li> </li>
<li> <li>
<button (click)="select('archive')" class="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-500/10 dark:hover:bg-slate-200/10 transition w-full text-left"> <button (click)="select('archive')" class="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-500/10 dark:hover:bg-surface2/10 transition w-full text-left">
<span class="flex items-center gap-2"><span>🗃</span> <span>Archive</span></span> <span class="flex items-center gap-2"><span>🗃</span> <span>Archive</span></span>
<app-badge-count class="ml-auto" [count]="counts().archive" color="stone"></app-badge-count> <app-badge-count class="ml-auto" [count]="counts().archive" color="stone"></app-badge-count>
</button> </button>

View File

@ -14,15 +14,15 @@ import { TrashExplorerComponent } from '../../layout/sidebar/trash/trash-explore
standalone: true, standalone: true,
imports: [CommonModule, FileExplorerComponent, QuickLinksComponent, ScrollableOverlayDirective, TrashExplorerComponent], imports: [CommonModule, FileExplorerComponent, QuickLinksComponent, ScrollableOverlayDirective, TrashExplorerComponent],
template: ` template: `
<aside class="fixed left-0 top-0 bottom-0 w-80 max-w-[80vw] bg-white dark:bg-gray-900 shadow-2xl z-40 transform transition-all duration-300 ease-out flex flex-col" <aside class="fixed left-0 top-0 bottom-0 w-80 max-w-[80vw] bg-card dark:bg-main shadow-2xl z-40 transform transition-all duration-300 ease-out flex flex-col"
[class.-translate-x-full]="!mobileNav.sidebarOpen()" [class.-translate-x-full]="!mobileNav.sidebarOpen()"
[class.translate-x-0]="mobileNav.sidebarOpen()"> [class.translate-x-0]="mobileNav.sidebarOpen()">
<!-- Header --> <!-- Header -->
<div class="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-800 bg-gradient-to-r from-nimbus-50 to-transparent dark:from-nimbus-900/20"> <div class="flex items-center justify-between p-4 border-b border-border dark:border-gray-800 bg-gradient-to-r from-nimbus-50 to-transparent dark:from-nimbus-900/20">
<h2 class="text-lg font-semibold truncate">{{ vaultName || 'ObsiViewer' }}</h2> <h2 class="text-lg font-semibold truncate">{{ vaultName || 'ObsiViewer' }}</h2>
<button <button
(click)="mobileNav.toggleSidebar()" (click)="mobileNav.toggleSidebar()"
class="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 transition-all active:scale-95 transform flex-shrink-0"> class="p-2 rounded-full hover:bg-surface1 dark:hover:bg-card transition-all active:scale-95 transform flex-shrink-0">
</button> </button>
</div> </div>
@ -30,27 +30,27 @@ import { TrashExplorerComponent } from '../../layout/sidebar/trash/trash-explore
<!-- Scrollable Content --> <!-- Scrollable Content -->
<div class="flex-1 overflow-y-auto min-h-0" appScrollableOverlay> <div class="flex-1 overflow-y-auto min-h-0" appScrollableOverlay>
<!-- Section Tests (dev-only) --> <!-- Section Tests (dev-only) -->
<section *ngIf="env.features.showTestSection" class="border-b border-gray-200 dark:border-gray-800"> <section *ngIf="env.features.showTestSection" class="border-b border-border dark:border-gray-800">
<button class="w-full flex items-center justify-between px-4 py-3 text-sm font-semibold hover:bg-gray-100 dark:hover:bg-gray-800 active:bg-gray-200 dark:active:bg-gray-700 transition-colors" <button class="w-full flex items-center justify-between px-4 py-3 text-sm font-semibold hover:bg-surface1 dark:hover:bg-card active:bg-surface2 dark:active:bg-gray-700 transition-colors"
(click)="open.tests = !open.tests"> (click)="open.tests = !open.tests">
<span>Section Tests</span> <span>Section Tests</span>
<span class="text-xs text-gray-500 transition-transform duration-200" [class.rotate-90]="!open.tests">{{ open.tests ? '▾' : '▸' }}</span> <span class="text-xs text-muted transition-transform duration-200" [class.rotate-90]="!open.tests">{{ open.tests ? '▾' : '▸' }}</span>
</button> </button>
<div *ngIf="open.tests" class="px-3 py-2"> <div *ngIf="open.tests" class="px-3 py-2">
<button <button
(click)="onMarkdownPlaygroundClick()" (click)="onMarkdownPlaygroundClick()"
class="w-full text-left block text-sm px-3 py-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 active:bg-gray-200 dark:active:bg-gray-700 text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 transition-all active:scale-[0.98] transform"> class="w-full text-left block text-sm px-3 py-2 rounded-lg hover:bg-surface1 dark:hover:bg-card active:bg-surface2 dark:active:bg-gray-700 text-main dark:text-main hover:text-main dark:hover:text-gray-100 transition-all active:scale-[0.98] transform">
🧪 Markdown Playground 🧪 Markdown Playground
</button> </button>
</div> </div>
</section> </section>
<!-- Quick Links accordion --> <!-- Quick Links accordion -->
<section class="border-b border-gray-200 dark:border-gray-800"> <section class="border-b border-border dark:border-gray-800">
<button class="w-full flex items-center justify-between px-4 py-3 text-sm font-semibold hover:bg-gray-100 dark:hover:bg-gray-800 active:bg-gray-200 dark:active:bg-gray-700 transition-colors" <button class="w-full flex items-center justify-between px-4 py-3 text-sm font-semibold hover:bg-surface1 dark:hover:bg-card active:bg-surface2 dark:active:bg-gray-700 transition-colors"
(click)="open.quick = !open.quick"> (click)="open.quick = !open.quick">
<span>Quick Links</span> <span>Quick Links</span>
<span class="text-xs text-gray-500 transition-transform duration-200" [class.rotate-90]="!open.quick">{{ open.quick ? '▾' : '▸' }}</span> <span class="text-xs text-muted transition-transform duration-200" [class.rotate-90]="!open.quick">{{ open.quick ? '▾' : '▸' }}</span>
</button> </button>
<div *ngIf="open.quick" class="pt-1"> <div *ngIf="open.quick" class="pt-1">
<app-quick-links (quickLinkSelected)="onQuickLink($event)"></app-quick-links> <app-quick-links (quickLinkSelected)="onQuickLink($event)"></app-quick-links>
@ -58,11 +58,11 @@ import { TrashExplorerComponent } from '../../layout/sidebar/trash/trash-explore
</section> </section>
<!-- Folders accordion --> <!-- Folders accordion -->
<section class="border-b border-gray-200 dark:border-gray-800"> <section class="border-b border-border dark:border-gray-800">
<button class="w-full flex items-center justify-between px-4 py-3 text-sm font-semibold hover:bg-gray-100 dark:hover:bg-gray-800 active:bg-gray-200 dark:active:bg-gray-700 transition-colors" <button class="w-full flex items-center justify-between px-4 py-3 text-sm font-semibold hover:bg-surface1 dark:hover:bg-card active:bg-surface2 dark:active:bg-gray-700 transition-colors"
(click)="open.folders = !open.folders"> (click)="open.folders = !open.folders">
<span>Folders</span> <span>Folders</span>
<span class="text-xs text-gray-500 transition-transform duration-200" [class.rotate-90]="!open.folders">{{ open.folders ? '▾' : '▸' }}</span> <span class="text-xs text-muted transition-transform duration-200" [class.rotate-90]="!open.folders">{{ open.folders ? '▾' : '▸' }}</span>
</button> </button>
<div *ngIf="open.folders" class="px-1 py-1"> <div *ngIf="open.folders" class="px-1 py-1">
<app-file-explorer [nodes]="nodes" [selectedNoteId]="selectedNoteId" [foldersOnly]="true" (folderSelected)="onFolder($event)" (fileSelected)="onSelect($event)"></app-file-explorer> <app-file-explorer [nodes]="nodes" [selectedNoteId]="selectedNoteId" [foldersOnly]="true" (folderSelected)="onFolder($event)" (fileSelected)="onSelect($event)"></app-file-explorer>
@ -70,45 +70,45 @@ import { TrashExplorerComponent } from '../../layout/sidebar/trash/trash-explore
</section> </section>
<!-- Tags accordion --> <!-- Tags accordion -->
<section class="border-b border-gray-200 dark:border-gray-800"> <section class="border-b border-border dark:border-gray-800">
<button class="w-full flex items-center justify-between px-4 py-3 text-sm font-semibold hover:bg-gray-100 dark:hover:bg-gray-800 active:bg-gray-200 dark:active:bg-gray-700 transition-colors" <button class="w-full flex items-center justify-between px-4 py-3 text-sm font-semibold hover:bg-surface1 dark:hover:bg-card active:bg-surface2 dark:active:bg-gray-700 transition-colors"
(click)="open.tags = !open.tags"> (click)="open.tags = !open.tags">
<span>Tags</span> <span>Tags</span>
<span class="text-xs text-gray-500 transition-transform duration-200" [class.rotate-90]="!open.tags">{{ open.tags ? '▾' : '▸' }}</span> <span class="text-xs text-muted transition-transform duration-200" [class.rotate-90]="!open.tags">{{ open.tags ? '▾' : '▸' }}</span>
</button> </button>
<div *ngIf="open.tags" class="px-2 py-2"> <div *ngIf="open.tags" class="px-2 py-2">
<ul class="space-y-1 text-sm"> <ul class="space-y-1 text-sm">
<li *ngFor="let t of tags" class="flex items-center gap-2"> <li *ngFor="let t of tags" class="flex items-center gap-2">
<button (click)="onTagSelected(t.name)" class="flex-1 text-left px-3 py-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 active:bg-gray-200 dark:active:bg-gray-700 transition-all active:scale-[0.98] transform truncate"> <button (click)="onTagSelected(t.name)" class="flex-1 text-left px-3 py-2 rounded-lg hover:bg-surface1 dark:hover:bg-card active:bg-surface2 dark:active:bg-gray-700 transition-all active:scale-[0.98] transform truncate">
<span>🏷</span> <span>🏷</span>
<span class="ml-1">{{ t.name }}</span> <span class="ml-1">{{ t.name }}</span>
</button> </button>
<span class="text-xs text-gray-500 font-medium min-w-[2rem] text-right">{{ t.count }}</span> <span class="text-xs text-muted font-medium min-w-[2rem] text-right">{{ t.count }}</span>
</li> </li>
</ul> </ul>
</div> </div>
</section> </section>
<!-- Trash accordion --> <!-- Trash accordion -->
<section class="border-b border-gray-200 dark:border-gray-800"> <section class="border-b border-border dark:border-gray-800">
<button class="w-full flex items-center justify-between px-4 py-3 text-sm font-semibold hover:bg-gray-100 dark:hover:bg-gray-800 active:bg-gray-200 dark:active:bg-gray-700 transition-colors" <button class="w-full flex items-center justify-between px-4 py-3 text-sm font-semibold hover:bg-surface1 dark:hover:bg-card active:bg-surface2 dark:active:bg-gray-700 transition-colors"
(click)="open.trash = !open.trash; onFolder('.trash')"> (click)="open.trash = !open.trash; onFolder('.trash')">
<span class="flex items-center gap-2">Trash</span> <span class="flex items-center gap-2">Trash</span>
<span class="text-xs text-gray-500 transition-transform duration-200" [class.rotate-90]="!open.trash">{{ open.trash ? '▾' : '▸' }}</span> <span class="text-xs text-muted transition-transform duration-200" [class.rotate-90]="!open.trash">{{ open.trash ? '▾' : '▸' }}</span>
</button> </button>
<div *ngIf="open.trash" class="px-1 py-2"> <div *ngIf="open.trash" class="px-1 py-2">
<ng-container *ngIf="trashHasContent(); else emptyTrash"> <ng-container *ngIf="trashHasContent(); else emptyTrash">
<app-trash-explorer (folderSelected)="onFolder($event)"></app-trash-explorer> <app-trash-explorer (folderSelected)="onFolder($event)"></app-trash-explorer>
</ng-container> </ng-container>
<ng-template #emptyTrash> <ng-template #emptyTrash>
<div class="px-3 py-2 text-slate-400 text-sm">La corbeille est vide</div> <div class="px-3 py-2 text-muted text-sm">La corbeille est vide</div>
</ng-template> </ng-template>
</div> </div>
</section> </section>
</div> </div>
<!-- Footer --> <!-- Footer -->
<div class="h-14 border-t border-gray-200 dark:border-gray-800 flex items-center justify-between px-4 text-xs text-gray-500 bg-gray-50 dark:bg-gray-900/50"> <div class="h-14 border-t border-border dark:border-gray-800 flex items-center justify-between px-4 text-xs text-muted bg-surface1 dark:bg-main/50">
<span>ObsiViewer</span> <span>ObsiViewer</span>
<span class="text-[10px] opacity-60">v1.0</span> <span class="text-[10px] opacity-60">v1.0</span>
</div> </div>

View File

@ -16,35 +16,35 @@ import { VaultService } from '../../../services/vault.service';
template: ` template: `
<div class="h-full flex flex-col overflow-hidden select-none"> <div class="h-full flex flex-col overflow-hidden select-none">
<!-- Header --> <!-- Header -->
<div class="h-12 flex items-center justify-between px-3 border-b border-gray-200 dark:border-gray-800"> <div class="h-12 flex items-center justify-between px-3 border-b border-border dark:border-gray-800">
<div class="text-sm font-semibold truncate">{{ vaultName }} - ObsiViewer</div> <div class="text-sm font-semibold truncate">{{ vaultName }} - ObsiViewer</div>
<button (click)="toggleSidebarRequest.emit()" class="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-800" title="Hide Sidebar"></button> <button (click)="toggleSidebarRequest.emit()" class="p-2 rounded hover:bg-surface1 dark:hover:bg-card" title="Hide Sidebar"></button>
</div> </div>
<!-- Content (scroll) --> <!-- Content (scroll) -->
<div class="flex-1 overflow-y-auto min-h-0" appScrollableOverlay> <div class="flex-1 overflow-y-auto min-h-0" appScrollableOverlay>
<!-- Section Tests (dev-only) --> <!-- Section Tests (dev-only) -->
<section *ngIf="env.features.showTestSection" class="border-b border-gray-200 dark:border-gray-800"> <section *ngIf="env.features.showTestSection" class="border-b border-border dark:border-gray-800">
<button class="w-full flex items-center justify-between px-3 py-2 text-sm font-semibold hover:bg-gray-100 dark:hover:bg-gray-800" <button class="w-full flex items-center justify-between px-3 py-2 text-sm font-semibold hover:bg-surface1 dark:hover:bg-card"
(click)="open.tests = !open.tests"> (click)="open.tests = !open.tests">
<span>Section Tests</span> <span>Section Tests</span>
<span class="text-xs text-gray-500">{{ open.tests ? '▾' : '▸' }}</span> <span class="text-xs text-muted">{{ open.tests ? '▾' : '▸' }}</span>
</button> </button>
<div *ngIf="open.tests" class="px-3 py-2"> <div *ngIf="open.tests" class="px-3 py-2">
<button <button
(click)="onMarkdownPlaygroundClick()" (click)="onMarkdownPlaygroundClick()"
class="w-full text-left block text-sm px-2 py-1.5 rounded hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100"> class="w-full text-left block text-sm px-2 py-1.5 rounded hover:bg-surface1 dark:hover:bg-card text-main dark:text-main hover:text-main dark:hover:text-gray-100">
Markdown Playground Markdown Playground
</button> </button>
</div> </div>
</section> </section>
<!-- Quick Links accordion --> <!-- Quick Links accordion -->
<section class="border-b border-gray-200 dark:border-gray-800"> <section class="border-b border-border dark:border-gray-800">
<button class="w-full flex items-center justify-between px-3 py-2 text-sm font-semibold hover:bg-gray-100 dark:hover:bg-gray-800" <button class="w-full flex items-center justify-between px-3 py-2 text-sm font-semibold hover:bg-surface1 dark:hover:bg-card"
(click)="open.quick = !open.quick"> (click)="open.quick = !open.quick">
<span>Quick Links</span> <span>Quick Links</span>
<span class="text-xs text-gray-500">{{ open.quick ? '▾' : '▸' }}</span> <span class="text-xs text-muted">{{ open.quick ? '▾' : '▸' }}</span>
</button> </button>
<div *ngIf="open.quick" class="pt-1"> <div *ngIf="open.quick" class="pt-1">
<app-quick-links (quickLinkSelected)="onQuickLink($event)"></app-quick-links> <app-quick-links (quickLinkSelected)="onQuickLink($event)"></app-quick-links>
@ -52,11 +52,11 @@ import { VaultService } from '../../../services/vault.service';
</section> </section>
<!-- Folders accordion --> <!-- Folders accordion -->
<section class="border-b border-gray-200 dark:border-gray-800"> <section class="border-b border-border dark:border-gray-800">
<button class="w-full flex items-center justify-between px-3 py-2 text-sm font-semibold hover:bg-gray-100 dark:hover:bg-gray-800" <button class="w-full flex items-center justify-between px-3 py-2 text-sm font-semibold hover:bg-surface1 dark:hover:bg-card"
(click)="toggleFoldersSection()"> (click)="toggleFoldersSection()">
<span>Folders</span> <span>Folders</span>
<span class="text-xs text-gray-500">{{ open.folders ? '▾' : '▸' }}</span> <span class="text-xs text-muted">{{ open.folders ? '▾' : '▸' }}</span>
</button> </button>
<div *ngIf="open.folders" class="px-1 py-1"> <div *ngIf="open.folders" class="px-1 py-1">
<app-file-explorer <app-file-explorer
@ -70,31 +70,31 @@ import { VaultService } from '../../../services/vault.service';
</section> </section>
<!-- Tags accordion --> <!-- Tags accordion -->
<section class="border-b border-gray-200 dark:border-gray-800"> <section class="border-b border-border dark:border-gray-800">
<button class="w-full flex items-center justify-between px-3 py-2 text-sm font-semibold hover:bg-gray-100 dark:hover:bg-gray-800" <button class="w-full flex items-center justify-between px-3 py-2 text-sm font-semibold hover:bg-surface1 dark:hover:bg-card"
(click)="open.tags = !open.tags"> (click)="open.tags = !open.tags">
<span>Tags</span> <span>Tags</span>
<span class="text-xs text-gray-500">{{ open.tags ? '▾' : '▸' }}</span> <span class="text-xs text-muted">{{ open.tags ? '▾' : '▸' }}</span>
</button> </button>
<div *ngIf="open.tags" class="px-2 py-2"> <div *ngIf="open.tags" class="px-2 py-2">
<ul class="space-y-0.5 text-sm"> <ul class="space-y-0.5 text-sm">
<li *ngFor="let t of tags" class="flex items-center gap-2"> <li *ngFor="let t of tags" class="flex items-center gap-2">
<button (click)="tagSelected.emit(t.name)" class="flex-1 text-left px-2 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 truncate"> <button (click)="tagSelected.emit(t.name)" class="flex-1 text-left px-2 py-1 rounded hover:bg-surface1 dark:hover:bg-card truncate">
<span>🏷</span> <span>🏷</span>
<span class="ml-1">{{ t.name }}</span> <span class="ml-1">{{ t.name }}</span>
</button> </button>
<span class="text-xs text-gray-500">{{ t.count }}</span> <span class="text-xs text-muted">{{ t.count }}</span>
</li> </li>
</ul> </ul>
</div> </div>
</section> </section>
<!-- Trash accordion --> <!-- Trash accordion -->
<section class="border-b border-gray-200 dark:border-gray-800"> <section class="border-b border-border dark:border-gray-800">
<button class="w-full flex items-center justify-between px-3 py-2 text-sm font-semibold hover:bg-gray-100 dark:hover:bg-gray-800" <button class="w-full flex items-center justify-between px-3 py-2 text-sm font-semibold hover:bg-surface1 dark:hover:bg-card"
(click)="toggleTrashSection()"> (click)="toggleTrashSection()">
<span class="flex items-center gap-2">Trash</span> <span class="flex items-center gap-2">Trash</span>
<span class="text-xs text-gray-500">{{ open.trash ? '▾' : '▸' }}</span> <span class="text-xs text-muted">{{ open.trash ? '▾' : '▸' }}</span>
</button> </button>
<div *ngIf="open.trash" class="px-1 py-2"> <div *ngIf="open.trash" class="px-1 py-2">
<ng-container *ngIf="trashHasContent(); else emptyTrash"> <ng-container *ngIf="trashHasContent(); else emptyTrash">
@ -108,14 +108,14 @@ import { VaultService } from '../../../services/vault.service';
</app-file-explorer> </app-file-explorer>
</ng-container> </ng-container>
<ng-template #emptyTrash> <ng-template #emptyTrash>
<div class="px-3 py-2 text-slate-400 text-sm">La corbeille est vide</div> <div class="px-3 py-2 text-muted text-sm">La corbeille est vide</div>
</ng-template> </ng-template>
</div> </div>
</section> </section>
</div> </div>
<!-- Footer placeholder --> <!-- Footer placeholder -->
<div class="h-14 border-t border-gray-200 dark:border-gray-800 flex items-center px-3 text-xs text-gray-500">ObsiViewer</div> <div class="h-14 border-t border-border dark:border-gray-800 flex items-center px-3 text-xs text-muted">ObsiViewer</div>
</div> </div>
` `
}) })

View File

@ -13,11 +13,11 @@ const DEFAULT_MD_PATH_ABS = '/assets/samples/markdown-playground.md';
standalone: true, standalone: true,
imports: [CommonModule, FormsModule, HttpClientModule, MarkdownViewerComponent], imports: [CommonModule, FormsModule, HttpClientModule, MarkdownViewerComponent],
template: ` template: `
<div class="h-full flex flex-col bg-gray-50 dark:bg-gray-900"> <div class="h-full flex flex-col bg-surface1 dark:bg-main">
<!-- Header --> <!-- Header -->
<header class="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 px-6 py-4"> <header class="bg-card dark:bg-card border-b border-border dark:border-border px-6 py-4">
<h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100">Markdown Playground</h1> <h1 class="text-2xl font-semibold text-main dark:text-gray-100">Markdown Playground</h1>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1"> <p class="text-sm text-muted dark:text-muted mt-1">
Page de test interne pour valider tous les formatages Markdown supportés par ObsiViewer. Page de test interne pour valider tous les formatages Markdown supportés par ObsiViewer.
</p> </p>
</header> </header>
@ -25,34 +25,34 @@ const DEFAULT_MD_PATH_ABS = '/assets/samples/markdown-playground.md';
<!-- Content --> <!-- Content -->
<div class="flex-1 flex gap-4 p-4 overflow-hidden"> <div class="flex-1 flex gap-4 p-4 overflow-hidden">
<!-- Editor Panel --> <!-- Editor Panel -->
<div class="flex-1 flex flex-col bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden"> <div class="flex-1 flex flex-col bg-card dark:bg-card rounded-lg border border-border dark:border-border overflow-hidden">
<div class="px-4 py-2 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-750"> <div class="px-4 py-2 border-b border-border dark:border-border bg-surface1 dark:bg-gray-750">
<h2 class="text-sm font-semibold text-gray-700 dark:text-gray-300">Markdown Source</h2> <h2 class="text-sm font-semibold text-main dark:text-main">Markdown Source</h2>
</div> </div>
<textarea <textarea
[ngModel]="sample()" [ngModel]="sample()"
(ngModelChange)="sample.set($event)" (ngModelChange)="sample.set($event)"
class="flex-1 p-4 font-mono text-sm bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 resize-none focus:outline-none" class="flex-1 p-4 font-mono text-sm bg-card dark:bg-card text-main dark:text-gray-100 resize-none focus:outline-none"
placeholder="Entrez votre Markdown ici..." placeholder="Entrez votre Markdown ici..."
spellcheck="false" spellcheck="false"
></textarea> ></textarea>
</div> </div>
<!-- Preview Panel --> <!-- Preview Panel -->
<div class="flex-1 flex flex-col bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden"> <div class="flex-1 flex flex-col bg-card dark:bg-card rounded-lg border border-border dark:border-border overflow-hidden">
<div class="px-4 py-2 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-750 flex items-center justify-between"> <div class="px-4 py-2 border-b border-border dark:border-border bg-surface1 dark:bg-gray-750 flex items-center justify-between">
<h2 class="text-sm font-semibold text-gray-700 dark:text-gray-300">Preview</h2> <h2 class="text-sm font-semibold text-main dark:text-main">Preview</h2>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button <button
(click)="toggleViewMode()" (click)="toggleViewMode()"
class="text-xs px-3 py-1 rounded bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300" class="text-xs px-3 py-1 rounded bg-surface2 dark:bg-surface2 hover:bg-muted dark:hover:bg-gray-600 text-main dark:text-main"
title="Toggle between inline and component view" title="Toggle between inline and component view"
> >
{{ useComponentView() ? 'Inline View' : 'Component View' }} {{ useComponentView() ? 'Inline View' : 'Component View' }}
</button> </button>
<button <button
(click)="resetToDefault()" (click)="resetToDefault()"
class="text-xs px-3 py-1 rounded bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300" class="text-xs px-3 py-1 rounded bg-surface2 dark:bg-surface2 hover:bg-muted dark:hover:bg-gray-600 text-main dark:text-main"
> >
Reset Reset
</button> </button>
@ -71,7 +71,7 @@ const DEFAULT_MD_PATH_ABS = '/assets/samples/markdown-playground.md';
<!-- Inline View --> <!-- Inline View -->
<div <div
*ngIf="!useComponentView()" *ngIf="!useComponentView()"
class="prose prose-slate dark:prose-invert max-w-none" class="md-view"
[innerHTML]="renderedHtml()" [innerHTML]="renderedHtml()"
></div> ></div>
</div> </div>

View File

@ -37,7 +37,7 @@ interface TooltipData {
imports: [CommonModule], imports: [CommonModule],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
template: ` template: `
<div class="relative w-full h-full bg-gray-50 dark:bg-gray-900"> <div class="relative w-full h-full bg-surface1 dark:bg-main">
<canvas <canvas
#canvas #canvas
class="w-full h-full cursor-grab active:cursor-grabbing" class="w-full h-full cursor-grab active:cursor-grabbing"
@ -52,13 +52,13 @@ interface TooltipData {
<!-- Tooltip --> <!-- Tooltip -->
@if (tooltip()) { @if (tooltip()) {
<div <div
class="absolute pointer-events-none bg-gray-800 dark:bg-gray-700 text-white text-xs px-3 py-2 rounded-lg shadow-lg z-50" class="absolute pointer-events-none bg-card dark:bg-surface2 text-white text-xs px-3 py-2 rounded-lg shadow-lg z-50"
[style.left.px]="tooltip()!.x + 10" [style.left.px]="tooltip()!.x + 10"
[style.top.px]="tooltip()!.y - 10"> [style.top.px]="tooltip()!.y - 10">
<div class="font-semibold">{{ tooltip()!.node.title }}</div> <div class="font-semibold">{{ tooltip()!.node.title }}</div>
<div class="text-gray-300 text-xs">{{ tooltip()!.node.path }}</div> <div class="text-gray-300 text-xs">{{ tooltip()!.node.path }}</div>
@if (tooltip()!.node.tags.length > 0) { @if (tooltip()!.node.tags.length > 0) {
<div class="text-gray-400 text-xs mt-1"> <div class="text-muted text-xs mt-1">
{{ tooltip()!.node.tags.join(', ') }} {{ tooltip()!.node.tags.join(', ') }}
</div> </div>
} }
@ -66,7 +66,7 @@ interface TooltipData {
} }
<!-- Info overlay --> <!-- Info overlay -->
<div class="absolute top-4 left-4 bg-white dark:bg-gray-800 px-3 py-2 rounded-lg shadow-md text-xs text-gray-700 dark:text-gray-300 pointer-events-none"> <div class="absolute top-4 left-4 bg-card dark:bg-card px-3 py-2 rounded-lg shadow-md text-xs text-main dark:text-main pointer-events-none">
<div><strong>Nodes:</strong> {{ nodes().length }}</div> <div><strong>Nodes:</strong> {{ nodes().length }}</div>
<div><strong>Links:</strong> {{ links().length }}</div> <div><strong>Links:</strong> {{ links().length }}</div>
</div> </div>

View File

@ -14,7 +14,7 @@ import { GroupLegendItem } from './graph-data.types';
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
template: ` template: `
@if (items().length > 0) { @if (items().length > 0) {
<div class="flex flex-wrap gap-2 p-3 bg-white dark:bg-gray-800 rounded-lg shadow-md"> <div class="flex flex-wrap gap-2 p-3 bg-card dark:bg-card rounded-lg shadow-md">
@for (item of items(); track item.groupIndex) { @for (item of items(); track item.groupIndex) {
<button <button
type="button" type="button"
@ -22,7 +22,7 @@ import { GroupLegendItem } from './graph-data.types';
[class.opacity-50]="!item.active" [class.opacity-50]="!item.active"
[class.ring-2]="!item.active" [class.ring-2]="!item.active"
[class.ring-gray-400]="!item.active" [class.ring-gray-400]="!item.active"
class="flex items-center gap-2 px-3 py-1.5 rounded-full bg-gray-50 dark:bg-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 transition-all cursor-pointer group"> class="flex items-center gap-2 px-3 py-1.5 rounded-full bg-surface1 dark:bg-surface2 hover:bg-surface1 dark:hover:bg-gray-600 transition-all cursor-pointer group">
<!-- Color chip --> <!-- Color chip -->
<div <div
@ -31,12 +31,12 @@ import { GroupLegendItem } from './graph-data.types';
</div> </div>
<!-- Query text --> <!-- Query text -->
<span class="text-xs font-medium text-gray-700 dark:text-gray-300 group-hover:text-gray-900 dark:group-hover:text-gray-100"> <span class="text-xs font-medium text-main dark:text-main group-hover:text-main dark:group-hover:text-gray-100">
{{ item.query }} {{ item.query }}
</span> </span>
<!-- Count badge --> <!-- Count badge -->
<span class="text-xs font-semibold px-1.5 py-0.5 rounded bg-gray-200 dark:bg-gray-600 text-gray-700 dark:text-gray-300"> <span class="text-xs font-semibold px-1.5 py-0.5 rounded bg-surface2 dark:bg-gray-600 text-main dark:text-main">
{{ item.count }} {{ item.count }}
</span> </span>
</button> </button>

View File

@ -16,15 +16,15 @@ import { GraphConfig, GRAPH_CONFIG_BOUNDS } from '../../graph-settings.types';
type="checkbox" type="checkbox"
[checked]="config().showArrow" [checked]="config().showArrow"
(change)="onToggleChange('showArrow', $event)" (change)="onToggleChange('showArrow', $event)"
class="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 cursor-pointer"> class="w-4 h-4 rounded border-border text-blue-600 focus:ring-blue-500 dark:border-border dark:bg-surface2 cursor-pointer">
<span class="text-sm text-gray-700 dark:text-gray-300 group-hover:text-gray-900 dark:group-hover:text-gray-100 select-none">Arrows</span> <span class="text-sm text-main dark:text-main group-hover:text-main dark:group-hover:text-gray-100 select-none">Arrows</span>
</label> </label>
<!-- Text fade threshold slider --> <!-- Text fade threshold slider -->
<div> <div>
<label class="flex items-center justify-between text-sm text-gray-700 dark:text-gray-300 mb-2"> <label class="flex items-center justify-between text-sm text-main dark:text-main mb-2">
<span>Text fade threshold</span> <span>Text fade threshold</span>
<span class="font-mono text-xs bg-gray-100 dark:bg-gray-800 px-2 py-0.5 rounded">{{ formatNumber(config().textFadeMultiplier, 1) }}</span> <span class="font-mono text-xs bg-surface1 dark:bg-card px-2 py-0.5 rounded">{{ formatNumber(config().textFadeMultiplier, 1) }}</span>
</label> </label>
<input <input
type="range" type="range"
@ -33,8 +33,8 @@ import { GraphConfig, GRAPH_CONFIG_BOUNDS } from '../../graph-settings.types';
[step]="bounds.textFadeMultiplier.step" [step]="bounds.textFadeMultiplier.step"
[value]="config().textFadeMultiplier" [value]="config().textFadeMultiplier"
(input)="onSliderChange('textFadeMultiplier', $event)" (input)="onSliderChange('textFadeMultiplier', $event)"
class="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer accent-blue-600"> class="w-full h-2 bg-surface2 dark:bg-surface2 rounded-lg appearance-none cursor-pointer accent-blue-600">
<div class="flex justify-between text-xs text-gray-500 dark:text-gray-400 mt-1"> <div class="flex justify-between text-xs text-muted dark:text-muted mt-1">
<span>{{ bounds.textFadeMultiplier.min }}</span> <span>{{ bounds.textFadeMultiplier.min }}</span>
<span>{{ bounds.textFadeMultiplier.max }}</span> <span>{{ bounds.textFadeMultiplier.max }}</span>
</div> </div>
@ -42,9 +42,9 @@ import { GraphConfig, GRAPH_CONFIG_BOUNDS } from '../../graph-settings.types';
<!-- Node size slider --> <!-- Node size slider -->
<div> <div>
<label class="flex items-center justify-between text-sm text-gray-700 dark:text-gray-300 mb-2"> <label class="flex items-center justify-between text-sm text-main dark:text-main mb-2">
<span>Node size</span> <span>Node size</span>
<span class="font-mono text-xs bg-gray-100 dark:bg-gray-800 px-2 py-0.5 rounded">{{ formatNumber(config().nodeSizeMultiplier, 2) }}</span> <span class="font-mono text-xs bg-surface1 dark:bg-card px-2 py-0.5 rounded">{{ formatNumber(config().nodeSizeMultiplier, 2) }}</span>
</label> </label>
<input <input
type="range" type="range"
@ -53,8 +53,8 @@ import { GraphConfig, GRAPH_CONFIG_BOUNDS } from '../../graph-settings.types';
[step]="bounds.nodeSizeMultiplier.step" [step]="bounds.nodeSizeMultiplier.step"
[value]="config().nodeSizeMultiplier" [value]="config().nodeSizeMultiplier"
(input)="onSliderChange('nodeSizeMultiplier', $event)" (input)="onSliderChange('nodeSizeMultiplier', $event)"
class="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer accent-blue-600"> class="w-full h-2 bg-surface2 dark:bg-surface2 rounded-lg appearance-none cursor-pointer accent-blue-600">
<div class="flex justify-between text-xs text-gray-500 dark:text-gray-400 mt-1"> <div class="flex justify-between text-xs text-muted dark:text-muted mt-1">
<span>{{ bounds.nodeSizeMultiplier.min }}</span> <span>{{ bounds.nodeSizeMultiplier.min }}</span>
<span>{{ bounds.nodeSizeMultiplier.max }}</span> <span>{{ bounds.nodeSizeMultiplier.max }}</span>
</div> </div>
@ -62,9 +62,9 @@ import { GraphConfig, GRAPH_CONFIG_BOUNDS } from '../../graph-settings.types';
<!-- Link thickness slider --> <!-- Link thickness slider -->
<div> <div>
<label class="flex items-center justify-between text-sm text-gray-700 dark:text-gray-300 mb-2"> <label class="flex items-center justify-between text-sm text-main dark:text-main mb-2">
<span>Link thickness</span> <span>Link thickness</span>
<span class="font-mono text-xs bg-gray-100 dark:bg-gray-800 px-2 py-0.5 rounded">{{ formatNumber(config().lineSizeMultiplier, 2) }}</span> <span class="font-mono text-xs bg-surface1 dark:bg-card px-2 py-0.5 rounded">{{ formatNumber(config().lineSizeMultiplier, 2) }}</span>
</label> </label>
<input <input
type="range" type="range"
@ -73,8 +73,8 @@ import { GraphConfig, GRAPH_CONFIG_BOUNDS } from '../../graph-settings.types';
[step]="bounds.lineSizeMultiplier.step" [step]="bounds.lineSizeMultiplier.step"
[value]="config().lineSizeMultiplier" [value]="config().lineSizeMultiplier"
(input)="onSliderChange('lineSizeMultiplier', $event)" (input)="onSliderChange('lineSizeMultiplier', $event)"
class="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer accent-blue-600"> class="w-full h-2 bg-surface2 dark:bg-surface2 rounded-lg appearance-none cursor-pointer accent-blue-600">
<div class="flex justify-between text-xs text-gray-500 dark:text-gray-400 mt-1"> <div class="flex justify-between text-xs text-muted dark:text-muted mt-1">
<span>{{ bounds.lineSizeMultiplier.min }}</span> <span>{{ bounds.lineSizeMultiplier.min }}</span>
<span>{{ bounds.lineSizeMultiplier.max }}</span> <span>{{ bounds.lineSizeMultiplier.max }}</span>
</div> </div>

View File

@ -31,8 +31,8 @@ import { GraphConfig } from '../../graph-settings.types';
type="checkbox" type="checkbox"
[checked]="config().showTags" [checked]="config().showTags"
(change)="onToggleChange('showTags', $event)" (change)="onToggleChange('showTags', $event)"
class="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 cursor-pointer"> class="w-4 h-4 rounded border-border text-blue-600 focus:ring-ring dark:border-border dark:bg-surface2 cursor-pointer">
<span class="text-sm text-gray-700 dark:text-gray-300 group-hover:text-gray-900 dark:group-hover:text-gray-100 select-none">Tags</span> <span class="text-sm text-main dark:text-main group-hover:text-main dark:group-hover:text-gray-100 select-none">Tags</span>
</label> </label>
<label class="flex items-center gap-2 cursor-pointer group"> <label class="flex items-center gap-2 cursor-pointer group">
@ -40,8 +40,8 @@ import { GraphConfig } from '../../graph-settings.types';
type="checkbox" type="checkbox"
[checked]="config().showAttachments" [checked]="config().showAttachments"
(change)="onToggleChange('showAttachments', $event)" (change)="onToggleChange('showAttachments', $event)"
class="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 cursor-pointer"> class="w-4 h-4 rounded border-border text-blue-600 focus:ring-ring dark:border-border dark:bg-surface2 cursor-pointer">
<span class="text-sm text-gray-700 dark:text-gray-300 group-hover:text-gray-900 dark:group-hover:text-gray-100 select-none">Attachments</span> <span class="text-sm text-main dark:text-main group-hover:text-main dark:group-hover:text-gray-100 select-none">Attachments</span>
</label> </label>
<label class="flex items-center gap-2 cursor-pointer group"> <label class="flex items-center gap-2 cursor-pointer group">
@ -49,8 +49,8 @@ import { GraphConfig } from '../../graph-settings.types';
type="checkbox" type="checkbox"
[checked]="config().hideUnresolved" [checked]="config().hideUnresolved"
(change)="onToggleChange('hideUnresolved', $event)" (change)="onToggleChange('hideUnresolved', $event)"
class="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 cursor-pointer"> class="w-4 h-4 rounded border-border text-blue-600 focus:ring-ring dark:border-border dark:bg-surface2 cursor-pointer">
<span class="text-sm text-gray-700 dark:text-gray-300 group-hover:text-gray-900 dark:group-hover:text-gray-100 select-none">Existing files only</span> <span class="text-sm text-main dark:text-main group-hover:text-main dark:group-hover:text-gray-100 select-none">Existing files only</span>
</label> </label>
<label class="flex items-center gap-2 cursor-pointer group"> <label class="flex items-center gap-2 cursor-pointer group">
@ -58,8 +58,8 @@ import { GraphConfig } from '../../graph-settings.types';
type="checkbox" type="checkbox"
[checked]="config().showOrphans" [checked]="config().showOrphans"
(change)="onToggleChange('showOrphans', $event)" (change)="onToggleChange('showOrphans', $event)"
class="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 cursor-pointer"> class="w-4 h-4 rounded border-border text-blue-600 focus:ring-ring dark:border-border dark:bg-surface2 cursor-pointer">
<span class="text-sm text-gray-700 dark:text-gray-300 group-hover:text-gray-900 dark:group-hover:text-gray-100 select-none">Orphans</span> <span class="text-sm text-main dark:text-main group-hover:text-main dark:group-hover:text-gray-100 select-none">Orphans</span>
</label> </label>
</div> </div>
</div> </div>

View File

@ -12,9 +12,9 @@ import { GraphConfig, GRAPH_CONFIG_BOUNDS } from '../../graph-settings.types';
<div class="space-y-4"> <div class="space-y-4">
<!-- Center force slider --> <!-- Center force slider -->
<div> <div>
<label class="flex items-center justify-between text-sm text-gray-700 dark:text-gray-300 mb-2"> <label class="flex items-center justify-between text-sm text-main dark:text-main mb-2">
<span>Center force</span> <span>Center force</span>
<span class="font-mono text-xs bg-gray-100 dark:bg-gray-800 px-2 py-0.5 rounded">{{ formatNumber(config().centerStrength, 2) }}</span> <span class="font-mono text-xs bg-surface1 dark:bg-card px-2 py-0.5 rounded">{{ formatNumber(config().centerStrength, 2) }}</span>
</label> </label>
<input <input
type="range" type="range"
@ -23,8 +23,8 @@ import { GraphConfig, GRAPH_CONFIG_BOUNDS } from '../../graph-settings.types';
[step]="bounds.centerStrength.step" [step]="bounds.centerStrength.step"
[value]="config().centerStrength" [value]="config().centerStrength"
(input)="onSliderChange('centerStrength', $event)" (input)="onSliderChange('centerStrength', $event)"
class="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer accent-blue-600"> class="w-full h-2 bg-surface2 dark:bg-surface2 rounded-lg appearance-none cursor-pointer accent-blue-600">
<div class="flex justify-between text-xs text-gray-500 dark:text-gray-400 mt-1"> <div class="flex justify-between text-xs text-muted dark:text-muted mt-1">
<span>{{ bounds.centerStrength.min }}</span> <span>{{ bounds.centerStrength.min }}</span>
<span>{{ bounds.centerStrength.max }}</span> <span>{{ bounds.centerStrength.max }}</span>
</div> </div>
@ -32,9 +32,9 @@ import { GraphConfig, GRAPH_CONFIG_BOUNDS } from '../../graph-settings.types';
<!-- Repel force slider --> <!-- Repel force slider -->
<div> <div>
<label class="flex items-center justify-between text-sm text-gray-700 dark:text-gray-300 mb-2"> <label class="flex items-center justify-between text-sm text-main dark:text-main mb-2">
<span>Repel force</span> <span>Repel force</span>
<span class="font-mono text-xs bg-gray-100 dark:bg-gray-800 px-2 py-0.5 rounded">{{ formatNumber(config().repelStrength, 1) }}</span> <span class="font-mono text-xs bg-surface1 dark:bg-card px-2 py-0.5 rounded">{{ formatNumber(config().repelStrength, 1) }}</span>
</label> </label>
<input <input
type="range" type="range"
@ -43,8 +43,8 @@ import { GraphConfig, GRAPH_CONFIG_BOUNDS } from '../../graph-settings.types';
[step]="bounds.repelStrength.step" [step]="bounds.repelStrength.step"
[value]="config().repelStrength" [value]="config().repelStrength"
(input)="onSliderChange('repelStrength', $event)" (input)="onSliderChange('repelStrength', $event)"
class="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer accent-blue-600"> class="w-full h-2 bg-surface2 dark:bg-surface2 rounded-lg appearance-none cursor-pointer accent-blue-600">
<div class="flex justify-between text-xs text-gray-500 dark:text-gray-400 mt-1"> <div class="flex justify-between text-xs text-muted dark:text-muted mt-1">
<span>{{ bounds.repelStrength.min }}</span> <span>{{ bounds.repelStrength.min }}</span>
<span>{{ bounds.repelStrength.max }}</span> <span>{{ bounds.repelStrength.max }}</span>
</div> </div>
@ -52,9 +52,9 @@ import { GraphConfig, GRAPH_CONFIG_BOUNDS } from '../../graph-settings.types';
<!-- Link force slider --> <!-- Link force slider -->
<div> <div>
<label class="flex items-center justify-between text-sm text-gray-700 dark:text-gray-300 mb-2"> <label class="flex items-center justify-between text-sm text-main dark:text-main mb-2">
<span>Link force</span> <span>Link force</span>
<span class="font-mono text-xs bg-gray-100 dark:bg-gray-800 px-2 py-0.5 rounded">{{ formatNumber(config().linkStrength, 2) }}</span> <span class="font-mono text-xs bg-surface1 dark:bg-card px-2 py-0.5 rounded">{{ formatNumber(config().linkStrength, 2) }}</span>
</label> </label>
<input <input
type="range" type="range"
@ -63,8 +63,8 @@ import { GraphConfig, GRAPH_CONFIG_BOUNDS } from '../../graph-settings.types';
[step]="bounds.linkStrength.step" [step]="bounds.linkStrength.step"
[value]="config().linkStrength" [value]="config().linkStrength"
(input)="onSliderChange('linkStrength', $event)" (input)="onSliderChange('linkStrength', $event)"
class="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer accent-blue-600"> class="w-full h-2 bg-surface2 dark:bg-surface2 rounded-lg appearance-none cursor-pointer accent-blue-600">
<div class="flex justify-between text-xs text-gray-500 dark:text-gray-400 mt-1"> <div class="flex justify-between text-xs text-muted dark:text-muted mt-1">
<span>{{ bounds.linkStrength.min }}</span> <span>{{ bounds.linkStrength.min }}</span>
<span>{{ bounds.linkStrength.max }}</span> <span>{{ bounds.linkStrength.max }}</span>
</div> </div>
@ -72,9 +72,9 @@ import { GraphConfig, GRAPH_CONFIG_BOUNDS } from '../../graph-settings.types';
<!-- Link distance slider --> <!-- Link distance slider -->
<div> <div>
<label class="flex items-center justify-between text-sm text-gray-700 dark:text-gray-300 mb-2"> <label class="flex items-center justify-between text-sm text-main dark:text-main mb-2">
<span>Link distance</span> <span>Link distance</span>
<span class="font-mono text-xs bg-gray-100 dark:bg-gray-800 px-2 py-0.5 rounded">{{ formatNumber(config().linkDistance, 0) }}px</span> <span class="font-mono text-xs bg-surface1 dark:bg-card px-2 py-0.5 rounded">{{ formatNumber(config().linkDistance, 0) }}px</span>
</label> </label>
<input <input
type="range" type="range"
@ -83,8 +83,8 @@ import { GraphConfig, GRAPH_CONFIG_BOUNDS } from '../../graph-settings.types';
[step]="bounds.linkDistance.step" [step]="bounds.linkDistance.step"
[value]="config().linkDistance" [value]="config().linkDistance"
(input)="onSliderChange('linkDistance', $event)" (input)="onSliderChange('linkDistance', $event)"
class="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer accent-blue-600"> class="w-full h-2 bg-surface2 dark:bg-surface2 rounded-lg appearance-none cursor-pointer accent-blue-600">
<div class="flex justify-between text-xs text-gray-500 dark:text-gray-400 mt-1"> <div class="flex justify-between text-xs text-muted dark:text-muted mt-1">
<span>{{ bounds.linkDistance.min }}</span> <span>{{ bounds.linkDistance.min }}</span>
<span>{{ bounds.linkDistance.max }}</span> <span>{{ bounds.linkDistance.max }}</span>
</div> </div>

View File

@ -14,7 +14,7 @@ import { GraphConfig, GraphColorGroup, intToHex, createGraphColor } from '../../
@if (config().colorGroups.length > 0) { @if (config().colorGroups.length > 0) {
<div class="space-y-2"> <div class="space-y-2">
@for (group of config().colorGroups; track $index) { @for (group of config().colorGroups; track $index) {
<div class="flex items-center gap-2 p-2 rounded-lg bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700"> <div class="flex items-center gap-2 p-2 rounded-lg bg-surface1 dark:bg-card border border-border dark:border-border">
<!-- Color picker --> <!-- Color picker -->
<input <input
type="color" type="color"
@ -29,7 +29,7 @@ import { GraphConfig, GraphColorGroup, intToHex, createGraphColor } from '../../
[value]="group.query" [value]="group.query"
(input)="onQueryChange($index, $event)" (input)="onQueryChange($index, $event)"
placeholder="tag:#example" placeholder="tag:#example"
class="flex-1 px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"> class="flex-1 px-2 py-1 text-sm border border-border dark:border-border rounded bg-card dark:bg-surface2 text-main dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500">
<!-- Actions --> <!-- Actions -->
<button <button
@ -65,11 +65,11 @@ import { GraphConfig, GraphColorGroup, intToHex, createGraphColor } from '../../
</button> </button>
<!-- Help text --> <!-- Help text -->
<div class="text-xs text-gray-500 dark:text-gray-400 space-y-1"> <div class="text-xs text-muted dark:text-muted space-y-1">
<div><strong>Examples:</strong></div> <div><strong>Examples:</strong></div>
<div> <code class="px-1 py-0.5 bg-gray-100 dark:bg-gray-800 rounded">tag:#markdown</code></div> <div> <code class="px-1 py-0.5 bg-surface1 dark:bg-card rounded">tag:#markdown</code></div>
<div> <code class="px-1 py-0.5 bg-gray-100 dark:bg-gray-800 rounded">file:test</code></div> <div> <code class="px-1 py-0.5 bg-surface1 dark:bg-card rounded">file:test</code></div>
<div> <code class="px-1 py-0.5 bg-gray-100 dark:bg-gray-800 rounded">path:folder</code></div> <div> <code class="px-1 py-0.5 bg-surface1 dark:bg-card rounded">path:folder</code></div>
</div> </div>
</div> </div>
`, `,

View File

@ -19,7 +19,7 @@ import { GraphSettingsAccordionComponent } from '../../../components/graph-setti
<div class="panel-content"> <div class="panel-content">
<div class="panel-header"> <div class="panel-header">
<h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Graph settings</h2> <h2 class="text-lg font-semibold text-main dark:text-gray-100">Graph settings</h2>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<!-- Expand all --> <!-- Expand all -->
<button <button

View File

@ -16,16 +16,17 @@ import { NimbusSidebarComponent } from '../../features/sidebar/nimbus-sidebar.co
import { QuickLinksComponent } from '../../features/quick-links/quick-links.component'; import { QuickLinksComponent } from '../../features/quick-links/quick-links.component';
import { ScrollableOverlayDirective } from '../../shared/overlay-scrollbar/scrollable-overlay.directive'; import { ScrollableOverlayDirective } from '../../shared/overlay-scrollbar/scrollable-overlay.directive';
import { MarkdownPlaygroundComponent } from '../../features/tests/markdown-playground/markdown-playground.component'; import { MarkdownPlaygroundComponent } from '../../features/tests/markdown-playground/markdown-playground.component';
import { ParametersPage } from '../../features/parameters/parameters.page';
@Component({ @Component({
selector: 'app-shell-nimbus-layout', selector: 'app-shell-nimbus-layout',
standalone: true, standalone: true,
imports: [CommonModule, FileExplorerComponent, NoteViewerComponent, AppBottomNavigationComponent, AppSidebarDrawerComponent, AppTocOverlayComponent, SwipeNavDirective, NotesListComponent, NimbusSidebarComponent, QuickLinksComponent, ScrollableOverlayDirective, MarkdownPlaygroundComponent], imports: [CommonModule, FileExplorerComponent, NoteViewerComponent, AppBottomNavigationComponent, AppSidebarDrawerComponent, AppTocOverlayComponent, SwipeNavDirective, NotesListComponent, NimbusSidebarComponent, QuickLinksComponent, ScrollableOverlayDirective, MarkdownPlaygroundComponent, ParametersPage],
template: ` template: `
<div class="relative h-screen flex flex-col bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100"> <div class="relative h-screen flex flex-col bg-card dark:bg-main text-main dark:text-gray-100">
<!-- Fullscreen overlay for note --> <!-- Fullscreen overlay for note -->
<div *ngIf="noteFullScreen && selectedNote && activeView !== 'markdown-playground'" class="absolute inset-0 z-50 flex flex-col bg-white dark:bg-gray-900"> <div *ngIf="noteFullScreen && selectedNote && activeView !== 'markdown-playground'" class="absolute inset-0 z-50 flex flex-col bg-card dark:bg-main">
<div class="note-content-area flex-1 overflow-y-auto px-4 py-4 lg:px-12" appScrollableOverlay> <div class="note-content-area flex-1 overflow-y-auto px-4 py-4 lg:px-12" appScrollableOverlay>
<app-note-viewer <app-note-viewer
[note]="selectedNote || null" [note]="selectedNote || null"
@ -37,6 +38,7 @@ import { MarkdownPlaygroundComponent } from '../../features/tests/markdown-playg
[fullScreenActive]="noteFullScreen" [fullScreenActive]="noteFullScreen"
(fullScreenRequested)="toggleNoteFullScreen()" (fullScreenRequested)="toggleNoteFullScreen()"
(legacyRequested)="ui.toggleUIMode()" (legacyRequested)="ui.toggleUIMode()"
(parametersRequested)="onParametersOpen()"
(showToc)="toggleOutlineRequest.emit()" (showToc)="toggleOutlineRequest.emit()"
(directoryClicked)="onFolderSelected($event)" (directoryClicked)="onFolderSelected($event)"
[tocOpen]="isOutlineOpen" [tocOpen]="isOutlineOpen"
@ -48,7 +50,7 @@ import { MarkdownPlaygroundComponent } from '../../features/tests/markdown-playg
<div *ngIf="responsive.isDesktop() && !noteFullScreen" class="flex-1 flex overflow-hidden relative"> <div *ngIf="responsive.isDesktop() && !noteFullScreen" class="flex-1 flex overflow-hidden relative">
<!-- Left: full sidebar or collapsed rail --> <!-- Left: full sidebar or collapsed rail -->
<ng-container *ngIf="isSidebarOpen; else collapsedRail"> <ng-container *ngIf="isSidebarOpen; else collapsedRail">
<aside class="flex flex-col border-r border-gray-200 dark:border-gray-800 min-h-0" [style.width.px]="leftSidebarWidth"> <aside class="flex flex-col border-r border-border dark:border-gray-800 min-h-0" [style.width.px]="leftSidebarWidth">
<app-nimbus-sidebar <app-nimbus-sidebar
[vaultName]="vaultName" [vaultName]="vaultName"
[effectiveFileTree]="effectiveFileTree" [effectiveFileTree]="effectiveFileTree"
@ -64,19 +66,19 @@ import { MarkdownPlaygroundComponent } from '../../features/tests/markdown-playg
</aside> </aside>
</ng-container> </ng-container>
<ng-template #collapsedRail> <ng-template #collapsedRail>
<aside class="border-r border-gray-200 dark:border-gray-800 h-full w-14 flex flex-col items-center py-3 gap-3"> <aside class="border-r border-border dark:border-gray-800 h-full w-14 flex flex-col items-center py-3 gap-3">
<button class="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-800" (click)="toggleSidebarRequest.emit()" title="Show Sidebar"></button> <button class="p-2 rounded hover:bg-surface1 dark:hover:bg-card" (click)="toggleSidebarRequest.emit()" title="Show Sidebar"></button>
<button class="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-800" (mouseenter)="openFlyout('quick')" (mouseleave)="scheduleCloseFlyout()" title="Quick Links"></button> <button class="p-2 rounded hover:bg-surface1 dark:hover:bg-card" (mouseenter)="openFlyout('quick')" (mouseleave)="scheduleCloseFlyout()" title="Quick Links"></button>
<button class="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-800" (mouseenter)="openFlyout('folders')" (mouseleave)="scheduleCloseFlyout()" title="Folders">📁</button> <button class="p-2 rounded hover:bg-surface1 dark:hover:bg-card" (mouseenter)="openFlyout('folders')" (mouseleave)="scheduleCloseFlyout()" title="Folders">📁</button>
<button class="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-800" (mouseenter)="openFlyout('tags')" (mouseleave)="scheduleCloseFlyout()" title="Tags">🏷</button> <button class="p-2 rounded hover:bg-surface1 dark:hover:bg-card" (mouseenter)="openFlyout('tags')" (mouseleave)="scheduleCloseFlyout()" title="Tags">🏷</button>
<button class="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-800" (mouseenter)="openFlyout('trash')" (mouseleave)="scheduleCloseFlyout()" title="Trash">🗑</button> <button class="p-2 rounded hover:bg-surface1 dark:hover:bg-card" (mouseenter)="openFlyout('trash')" (mouseleave)="scheduleCloseFlyout()" title="Trash">🗑</button>
</aside> </aside>
<!-- Flyouts --> <!-- Flyouts -->
<div class="absolute left-14 top-0 bottom-0 w-80 max-w-[70vw] bg-white dark:bg-gray-900 border-r border-gray-200 dark:border-gray-800 shadow-xl z-50" *ngIf="hoveredFlyout as f" (mouseenter)="cancelCloseFlyout()" (mouseleave)="scheduleCloseFlyout()"> <div class="absolute left-14 top-0 bottom-0 w-80 max-w-[70vw] bg-card dark:bg-main border-r border-border dark:border-gray-800 shadow-xl z-50" *ngIf="hoveredFlyout as f" (mouseenter)="cancelCloseFlyout()" (mouseleave)="scheduleCloseFlyout()">
<div class="h-12 flex items-center justify-between px-3 border-b border-gray-200 dark:border-gray-800"> <div class="h-12 flex items-center justify-between px-3 border-b border-border dark:border-gray-800">
<div class="text-sm font-semibold">{{ f === 'quick' ? 'Quick Links' : (f === 'folders' ? 'Folders' : (f === 'tags' ? 'Tags' : 'Trash')) }}</div> <div class="text-sm font-semibold">{{ f === 'quick' ? 'Quick Links' : (f === 'folders' ? 'Folders' : (f === 'tags' ? 'Tags' : 'Trash')) }}</div>
<button class="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-800" (click)="hoveredFlyout=null"></button> <button class="p-2 rounded hover:bg-surface1 dark:hover:bg-card" (click)="hoveredFlyout=null"></button>
</div> </div>
<div class="h-[calc(100%-3rem)] overflow-y-auto" appScrollableOverlay> <div class="h-[calc(100%-3rem)] overflow-y-auto" appScrollableOverlay>
<ng-container [ngSwitch]="f"> <ng-container [ngSwitch]="f">
@ -87,21 +89,21 @@ import { MarkdownPlaygroundComponent } from '../../features/tests/markdown-playg
<div *ngSwitchCase="'tags'" class="p-2"> <div *ngSwitchCase="'tags'" class="p-2">
<ul class="space-y-0.5 text-sm"> <ul class="space-y-0.5 text-sm">
<li *ngFor="let t of tags"> <li *ngFor="let t of tags">
<button (click)="onTagSelected(t.name)" class="w-full text-left px-2 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 truncate">🏷 {{ t.name }} <span class="text-xs text-gray-500">{{ t.count }}</span></button> <button (click)="onTagSelected(t.name)" class="w-full text-left px-2 py-1 rounded hover:bg-surface1 dark:hover:bg-card truncate">🏷 {{ t.name }} <span class="text-xs text-muted">{{ t.count }}</span></button>
</li> </li>
</ul> </ul>
</div> </div>
<div *ngSwitchDefault class="p-3 text-sm text-gray-500">Empty</div> <div *ngSwitchDefault class="p-3 text-sm text-muted">Empty</div>
</ng-container> </ng-container>
</div> </div>
</div> </div>
</ng-template> </ng-template>
<!-- Left Resizer --> <!-- Left Resizer -->
<div class="h-full w-1 cursor-col-resize hover:bg-gray-200/50 dark:hover:bg-gray-700/50" (pointerdown)="leftResizeStart.emit($event)" role="separator" aria-orientation="vertical" aria-label="Redimensionner la barre latérale gauche"></div> <div class="h-full w-1 cursor-col-resize hover:bg-surface2/50 dark:hover:bg-surface2/50" (pointerdown)="leftResizeStart.emit($event)" role="separator" aria-orientation="vertical" aria-label="Redimensionner la barre latérale gauche"></div>
<!-- Center List --> <!-- Center List -->
<section class="border-r border-gray-200 dark:border-gray-800 overflow-hidden" [style.width.px]="centerPanelWidth"> <section class="border-r border-border dark:border-gray-800 overflow-hidden" [style.width.px]="centerPanelWidth">
<div class="h-full flex flex-col"> <div class="h-full flex flex-col">
<app-notes-list class="flex-1" <app-notes-list class="flex-1"
[notes]="vault.allNotes()" [notes]="vault.allNotes()"
@ -116,13 +118,14 @@ import { MarkdownPlaygroundComponent } from '../../features/tests/markdown-playg
</section> </section>
<!-- Center Resizer (between list and note) --> <!-- Center Resizer (between list and note) -->
<div class="h-full w-1 cursor-col-resize hover:bg-gray-200/50 dark:hover:bg-gray-700/50" (pointerdown)="centerResizeStart.emit($event)" role="separator" aria-orientation="vertical" aria-label="Redimensionner la zone de liste"></div> <div class="h-full w-1 cursor-col-resize hover:bg-surface2/50 dark:hover:bg-surface2/50" (pointerdown)="centerResizeStart.emit($event)" role="separator" aria-orientation="vertical" aria-label="Redimensionner la zone de liste"></div>
<!-- Note View + ToC --> <!-- Note View + ToC -->
<section class="flex-1 relative min-w-0 flex"> <section class="flex-1 relative min-w-0 flex">
<div class="note-content-area flex-1 overflow-y-auto px-4 py-4 lg:px-8" appScrollableOverlay> <div class="note-content-area flex-1 overflow-y-auto px-4 py-4 lg:px-8" appScrollableOverlay>
<app-markdown-playground *ngIf="activeView === 'markdown-playground'"></app-markdown-playground> <app-markdown-playground *ngIf="activeView === 'markdown-playground'"></app-markdown-playground>
<app-note-viewer *ngIf="activeView !== 'markdown-playground'" <app-parameters *ngIf="activeView === 'parameters'"></app-parameters>
<app-note-viewer *ngIf="activeView !== 'markdown-playground' && activeView !== 'parameters'"
[note]="selectedNote || null" [note]="selectedNote || null"
[noteHtmlContent]="renderedNoteContent" [noteHtmlContent]="renderedNoteContent"
[allNotes]="vault.allNotes()" [allNotes]="vault.allNotes()"
@ -132,17 +135,18 @@ import { MarkdownPlaygroundComponent } from '../../features/tests/markdown-playg
[fullScreenActive]="noteFullScreen" [fullScreenActive]="noteFullScreen"
(fullScreenRequested)="toggleNoteFullScreen()" (fullScreenRequested)="toggleNoteFullScreen()"
(legacyRequested)="ui.toggleUIMode()" (legacyRequested)="ui.toggleUIMode()"
(parametersRequested)="onParametersOpen()"
(showToc)="toggleOutlineRequest.emit()" (showToc)="toggleOutlineRequest.emit()"
(directoryClicked)="onFolderSelected($event)" (directoryClicked)="onFolderSelected($event)"
[tocOpen]="isOutlineOpen" [tocOpen]="isOutlineOpen"
></app-note-viewer> ></app-note-viewer>
</div> </div>
<aside class="hidden xl:block border-l border-gray-200 dark:border-gray-800 overflow-y-auto transition-all duration-300 ease-in-out" appScrollableOverlay [style.width.px]="isOutlineOpen ? rightSidebarWidth : 0" [class.opacity-0]="!isOutlineOpen" [class.pointer-events-none]="!isOutlineOpen"> <aside class="hidden xl:block border-l border-border dark:border-gray-800 overflow-y-auto transition-all duration-300 ease-in-out" appScrollableOverlay [style.width.px]="isOutlineOpen ? rightSidebarWidth : 0" [class.opacity-0]="!isOutlineOpen" [class.pointer-events-none]="!isOutlineOpen">
<div class="p-3"> <div class="p-3">
<h2 class="text-sm font-semibold mb-2">Sommaire</h2> <h2 class="text-sm font-semibold mb-2">Sommaire</h2>
<ul class="space-y-1 text-sm text-gray-600 dark:text-gray-300"> <ul class="space-y-1 text-sm text-muted dark:text-main">
<li *ngFor="let h of tableOfContents"> <li *ngFor="let h of tableOfContents">
<a class="block truncate hover:text-gray-900 dark:hover:text-white cursor-pointer" (click)="navigateHeading.emit(h.id)" [style.paddingLeft.rem]="(h.level - 1) * 0.75">{{ h.text }}</a> <a class="block truncate hover:text-main dark:hover:text-white cursor-pointer" (click)="navigateHeading.emit(h.id)" [style.paddingLeft.rem]="(h.level - 1) * 0.75">{{ h.text }}</a>
</li> </li>
</ul> </ul>
</div> </div>
@ -152,7 +156,7 @@ import { MarkdownPlaygroundComponent } from '../../features/tests/markdown-playg
<!-- Tablet: simple tabbed areas --> <!-- Tablet: simple tabbed areas -->
<div *ngIf="responsive.isTablet() && !noteFullScreen" class="flex-1 flex flex-col overflow-hidden"> <div *ngIf="responsive.isTablet() && !noteFullScreen" class="flex-1 flex flex-col overflow-hidden">
<div class="h-12 border-b border-gray-200 dark:border-gray-800 flex items-center"> <div class="h-12 border-b border-border dark:border-gray-800 flex items-center">
<button class="flex-1 h-full text-sm" [class.text-nimbus-500]="mobileNav.activeTab() === 'sidebar'" (click)="mobileNav.setActiveTab('sidebar')">Sidebar</button> <button class="flex-1 h-full text-sm" [class.text-nimbus-500]="mobileNav.activeTab() === 'sidebar'" (click)="mobileNav.setActiveTab('sidebar')">Sidebar</button>
<button class="flex-1 h-full text-sm" [class.text-nimbus-500]="mobileNav.activeTab() === 'list'" (click)="mobileNav.setActiveTab('list')">Liste</button> <button class="flex-1 h-full text-sm" [class.text-nimbus-500]="mobileNav.activeTab() === 'list'" (click)="mobileNav.setActiveTab('list')">Liste</button>
<button class="flex-1 h-full text-sm" [class.text-nimbus-500]="mobileNav.activeTab() === 'page'" (click)="mobileNav.setActiveTab('page')">Page</button> <button class="flex-1 h-full text-sm" [class.text-nimbus-500]="mobileNav.activeTab() === 'page'" (click)="mobileNav.setActiveTab('page')">Page</button>
@ -165,7 +169,9 @@ import { MarkdownPlaygroundComponent } from '../../features/tests/markdown-playg
<app-notes-list [notes]="vault.allNotes()" [folderFilter]="folderFilter" [tagFilter]="tagFilter" [quickLinkFilter]="quickLinkFilter" [query]="listQuery" (queryChange)="listQuery=$event" (openNote)="onOpenNote($event)"></app-notes-list> <app-notes-list [notes]="vault.allNotes()" [folderFilter]="folderFilter" [tagFilter]="tagFilter" [quickLinkFilter]="quickLinkFilter" [query]="listQuery" (queryChange)="listQuery=$event" (openNote)="onOpenNote($event)"></app-notes-list>
</div> </div>
<div [hidden]="mobileNav.activeTab() !== 'page'" class="note-content-area h-full overflow-y-auto px-3 py-4" appScrollableOverlay> <div [hidden]="mobileNav.activeTab() !== 'page'" class="note-content-area h-full overflow-y-auto px-3 py-4" appScrollableOverlay>
<app-note-viewer [note]="selectedNote || null" [noteHtmlContent]="renderedNoteContent" [allNotes]="vault.allNotes()" (noteLinkClicked)="noteSelected.emit($event)" (tagClicked)="onTagSelected($event)" (wikiLinkActivated)="wikiLinkActivated.emit($event)" [fullScreenActive]="noteFullScreen" (fullScreenRequested)="toggleNoteFullScreen()" (legacyRequested)="ui.toggleUIMode()" (showToc)="mobileNav.toggleToc()" (directoryClicked)="onFolderSelected($event)" [tocOpen]="mobileNav.tocOpen()"></app-note-viewer> <app-markdown-playground *ngIf="activeView === 'markdown-playground'"></app-markdown-playground>
<app-parameters *ngIf="activeView === 'parameters'"></app-parameters>
<app-note-viewer *ngIf="activeView !== 'markdown-playground' && activeView !== 'parameters'" [note]="selectedNote || null" [noteHtmlContent]="renderedNoteContent" [allNotes]="vault.allNotes()" (noteLinkClicked)="noteSelected.emit($event)" (tagClicked)="onTagSelected($event)" (wikiLinkActivated)="wikiLinkActivated.emit($event)" [fullScreenActive]="noteFullScreen" (fullScreenRequested)="toggleNoteFullScreen()" (legacyRequested)="ui.toggleUIMode()" (parametersRequested)="onParametersOpen()" (showToc)="mobileNav.toggleToc()" (directoryClicked)="onFolderSelected($event)" [tocOpen]="mobileNav.tocOpen()"></app-note-viewer>
</div> </div>
</div> </div>
</div> </div>
@ -191,27 +197,37 @@ import { MarkdownPlaygroundComponent } from '../../features/tests/markdown-playg
} }
@if (mobileNav.activeTab() === 'page') { @if (mobileNav.activeTab() === 'page') {
@if (activeView === 'parameters') {
<div class="note-content-area h-full px-3 py-3 animate-fadeIn" style="overflow-y: auto !important;" appScrollableOverlay> <div class="note-content-area h-full px-3 py-3 animate-fadeIn" style="overflow-y: auto !important;" appScrollableOverlay>
<div class="flex items-center justify-between mb-3 sticky top-0 bg-white dark:bg-gray-900 py-2 -mt-2 z-10"> <app-parameters></app-parameters>
</div>
} @else if (activeView === 'markdown-playground') {
<div class="note-content-area h-full px-3 py-3 animate-fadeIn" style="overflow-y: auto !important;" appScrollableOverlay>
<app-markdown-playground></app-markdown-playground>
</div>
} @else {
<div class="note-content-area h-full px-3 py-3 animate-fadeIn" style="overflow-y: auto !important;" appScrollableOverlay>
<div class="flex items-center justify-between mb-3 sticky top-0 bg-card dark:bg-main py-2 -mt-2 z-10">
<h2 class="text-base font-semibold truncate">{{ selectedNote?.title || 'Aucune page' }}</h2> <h2 class="text-base font-semibold truncate">{{ selectedNote?.title || 'Aucune page' }}</h2>
<button <button
*ngIf="tableOfContents.length > 0" *ngIf="tableOfContents.length > 0"
(pointerdown)="$event.stopPropagation(); mobileNav.toggleToc()" (pointerdown)="$event.stopPropagation(); mobileNav.toggleToc()"
(click)="$event.preventDefault()" (click)="$event.preventDefault()"
class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 active:bg-gray-200 dark:active:bg-gray-700 transition-all active:scale-95 transform flex-shrink-0"> class="p-2 rounded-lg hover:bg-surface1 dark:hover:bg-card active:bg-surface2 dark:active:bg-gray-700 transition-all active:scale-95 transform flex-shrink-0">
📋 📋
</button> </button>
</div> </div>
@if (selectedNote) { @if (selectedNote) {
<app-note-viewer [note]="selectedNote || null" [noteHtmlContent]="renderedNoteContent" [allNotes]="vault.allNotes()" (noteLinkClicked)="noteSelected.emit($event)" (tagClicked)="tagClicked.emit($event)" (wikiLinkActivated)="wikiLinkActivated.emit($event)" (fullScreenRequested)="toggleNoteFullScreen()"></app-note-viewer> <app-note-viewer [note]="selectedNote || null" [noteHtmlContent]="renderedNoteContent" [allNotes]="vault.allNotes()" (noteLinkClicked)="noteSelected.emit($event)" (tagClicked)="tagClicked.emit($event)" (wikiLinkActivated)="wikiLinkActivated.emit($event)" (fullScreenRequested)="toggleNoteFullScreen()" (parametersRequested)="onParametersOpen()"></app-note-viewer>
} @else { } @else {
<div class="mt-10 text-center text-sm text-gray-500 dark:text-gray-400"> <div class="mt-10 text-center text-sm text-muted dark:text-muted">
<div class="text-4xl mb-3">📄</div> <div class="text-4xl mb-3">📄</div>
<p>Aucune page sélectionnée pour le moment.</p> <p>Aucune page sélectionnée pour le moment.</p>
</div> </div>
} }
</div> </div>
} }
}
<app-toc-overlay *ngIf="mobileNav.tocOpen()" [headings]="tableOfContents" (go)="onTocNavigate($event)" (close)="mobileNav.toggleToc()"></app-toc-overlay> <app-toc-overlay *ngIf="mobileNav.tocOpen()" [headings]="tableOfContents" (go)="onTocNavigate($event)" (close)="mobileNav.toggleToc()"></app-toc-overlay>
@ -255,6 +271,7 @@ export class AppShellNimbusLayoutComponent {
@Output() searchTermChange = new EventEmitter<string>(); @Output() searchTermChange = new EventEmitter<string>();
@Output() searchOptionsChange = new EventEmitter<any>(); @Output() searchOptionsChange = new EventEmitter<any>();
@Output() markdownPlaygroundSelected = new EventEmitter<void>(); @Output() markdownPlaygroundSelected = new EventEmitter<void>();
@Output() parametersOpened = new EventEmitter<void>();
folderFilter: string | null = null; folderFilter: string | null = null;
listQuery: string = ''; listQuery: string = '';
@ -443,6 +460,10 @@ export class AppShellNimbusLayoutComponent {
this.markdownPlaygroundSelected.emit(); this.markdownPlaygroundSelected.emit();
} }
onParametersOpen(): void {
this.parametersOpened.emit();
}
onTocNavigate(headingId: string): void { onTocNavigate(headingId: string): void {
// Ensure the page view is visible so the scroll container exists // Ensure the page view is visible so the scroll container exists
this.mobileNav.setActiveTab('page'); this.mobileNav.setActiveTab('page');

View File

@ -15,7 +15,7 @@ import { BadgeCountComponent } from '../../../../app/shared/ui/badge-count.compo
<div> <div>
<div <div
(click)="onFolderClick(folder)" (click)="onFolderClick(folder)"
class="flex items-center cursor-pointer px-3 py-2 rounded-lg my-0.5 text-sm hover:bg-slate-500/10 dark:hover:bg-slate-200/10 transition" class="flex items-center cursor-pointer px-3 py-2 rounded-lg my-0.5 text-sm hover:bg-slate-500/10 dark:hover:bg-surface2/10 transition"
> >
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2 transition-transform text-obs-l-text-muted dark:text-obs-d-text-muted" [class.rotate-90]="folder.isOpen" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2 transition-transform text-obs-l-text-muted dark:text-obs-d-text-muted" [class.rotate-90]="folder.isOpen" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
@ -45,7 +45,7 @@ import { BadgeCountComponent } from '../../../../app/shared/ui/badge-count.compo
[class.bg-obs-l-bg-main]="selectedNoteId() === file.id" [class.bg-obs-l-bg-main]="selectedNoteId() === file.id"
[class.dark:bg-obs-d-bg-main]="selectedNoteId() === file.id" [class.dark:bg-obs-d-bg-main]="selectedNoteId() === file.id"
[class.hover:bg-slate-500/10]="selectedNoteId() !== file.id" [class.hover:bg-slate-500/10]="selectedNoteId() !== file.id"
[class.dark:hover:bg-slate-200/10]="selectedNoteId() !== file.id" [class.dark:hover:bg-surface2/10]="selectedNoteId() !== file.id"
> >
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2 flex-shrink-0 text-obs-l-text-muted dark:text-obs-d-text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2 flex-shrink-0 text-obs-l-text-muted dark:text-obs-d-text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />

View File

@ -0,0 +1,31 @@
import { Injectable } from '@angular/core';
import { EditorView } from '@codemirror/view';
import { setHighlights, clearHighlights, HighlightRange } from './extensions/ranged-highlights.extension';
import { highlightOccurrences } from './extensions/highlight-occurrences.extension';
import { markdownThemeHighlightExt, highlightColorFacet } from './extensions/markdown-theme-highlight.extension';
import type { Extension, Compartment } from '@codemirror/state';
@Injectable({ providedIn: 'root' })
export class EditorHighlightService {
get extensions(): Extension[] {
return [markdownThemeHighlightExt()];
}
occurrencesExtension(pattern: string | RegExp): Extension {
return highlightOccurrences(pattern);
}
applyOccurrences(view: EditorView, compartment: Compartment, pattern: string | RegExp): void {
view.dispatch({ effects: compartment.reconfigure(this.occurrencesExtension(pattern)) });
}
setRanges(view: EditorView, ranges: HighlightRange[]): void {
view.dispatch({ effects: setHighlights.of(ranges) });
}
clearRanges(view: EditorView): void {
view.dispatch({ effects: clearHighlights.of(null) });
}
facet() { return highlightColorFacet; }
}

View File

@ -0,0 +1,49 @@
import type { Extension } from '@codemirror/state';
import { Decoration, DecorationSet, EditorView, ViewPlugin, ViewUpdate } from '@codemirror/view';
export interface OccurrenceOptions {
pattern: string | RegExp;
}
function buildRegex(pattern: string | RegExp): RegExp {
if (pattern instanceof RegExp) return pattern;
// default: case-insensitive word-like TODO/FIXME etc. if not a regex
const source = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
return new RegExp(source, 'gi');
}
function decorateDoc(view: EditorView, re: RegExp): DecorationSet {
const builder: any[] = [];
const deco = Decoration.mark({ class: 'cm-md-highlight' });
for (let { from, to } of view.visibleRanges) {
const text = view.state.doc.sliceString(from, to);
re.lastIndex = 0;
let m: RegExpExecArray | null;
while ((m = re.exec(text))) {
const start = from + m.index;
const end = start + m[0].length;
builder.push(deco.range(start, end));
// avoid infinite loops on zero-length matches
if (m[0].length === 0) re.lastIndex++;
}
}
return Decoration.set(builder, true);
}
export function highlightOccurrences(pattern: string | RegExp): Extension {
const re = buildRegex(pattern);
const plugin = ViewPlugin.fromClass(class {
decorations: DecorationSet;
constructor(view: EditorView) {
this.decorations = decorateDoc(view, re);
}
update(update: ViewUpdate) {
if (update.docChanged || update.viewportChanged) {
this.decorations = decorateDoc(update.view, re);
}
}
}, { decorations: v => v.decorations });
return [plugin];
}

View File

@ -0,0 +1,148 @@
import { Facet, Extension, RangeSetBuilder } from '@codemirror/state';
import { Decoration, DecorationSet, EditorView, ViewPlugin, ViewUpdate } from '@codemirror/view';
import { HighlightStyle, syntaxHighlighting, syntaxTree } from '@codemirror/language';
import { tags as t } from '@lezer/highlight';
import { rangedHighlightsExt } from './ranged-highlights.extension';
export const highlightColorFacet = Facet.define<string | null, string | null>({
combine: values => values.length ? values[values.length - 1] : null
});
const markdownBaseTheme = EditorView.baseTheme({
'.cm-inline-code': {
backgroundColor: 'var(--md-code-bg)',
color: 'var(--md-code-fg)',
borderRadius: 'var(--cm-hl-br, 3px)',
padding: '0 0.3rem',
fontFamily: 'var(--code-font, "JetBrains Mono", "Fira Code", monospace)',
},
'.cm-codeblock': {
backgroundColor: 'var(--md-pre-bg)',
color: 'var(--md-pre-fg)',
borderRadius: 'var(--cm-hl-br, 3px)',
padding: '0.4rem 0.6rem',
display: 'block',
},
'.cm-codeblock + .cm-codeblock': {
marginTop: '0.2rem',
},
'.cm-codeblock .cm-highlight': {
background: 'transparent !important',
},
'.cm-codeinfo': {
color: 'var(--md-muted)',
fontFamily: 'var(--code-font, "JetBrains Mono", "Fira Code", monospace)',
fontSize: '0.75rem',
textTransform: 'lowercase',
letterSpacing: '0.02em'
}
});
const codeBlockLineDeco = Decoration.line({ class: 'cm-codeblock' });
const codeInfoDeco = Decoration.mark({ class: 'cm-codeinfo' });
function codeBlockDecorations(view: EditorView): DecorationSet {
const builder = new RangeSetBuilder<Decoration>();
const tree = syntaxTree(view.state);
for (const range of view.visibleRanges) {
tree.iterate({
from: range.from,
to: range.to,
enter: node => {
const name = node.type.name;
if (name === 'FencedCode' || name === 'CodeBlock') {
let line = view.state.doc.lineAt(node.from);
const maxLine = view.state.doc.lineAt(node.to).number;
while (line.number <= maxLine) {
builder.add(line.from, line.from, codeBlockLineDeco);
if (line.number === maxLine) break;
line = view.state.doc.line(line.number + 1);
}
return false;
}
if (name === 'CodeInfo') {
builder.add(node.from, node.to, codeInfoDeco);
}
return undefined;
}
});
}
return builder.finish();
}
const codeBlockPlugin = ViewPlugin.fromClass(class {
decorations: DecorationSet;
constructor(view: EditorView) {
this.decorations = codeBlockDecorations(view);
}
update(update: ViewUpdate) {
if (update.docChanged || update.viewportChanged) {
this.decorations = codeBlockDecorations(update.view);
}
}
}, {
decorations: v => v.decorations
});
function createFacetApplier(): Extension {
return ViewPlugin.fromClass(class {
constructor(private view: EditorView) {
this.apply();
}
update(update: ViewUpdate) {
if (update.docChanged || update.viewportChanged || update.transactions.some(tr => tr.effects.length)) {
this.apply(update.view);
}
}
private apply(view: EditorView = this.view) {
const value = view.state.facet(highlightColorFacet);
const root = view.dom as HTMLElement;
if (value) {
root.style.setProperty('--cm-hl-bg', value);
} else {
root.style.removeProperty('--cm-hl-bg');
}
}
});
}
const markdownThemeTokens = HighlightStyle.define([
{ tag: t.heading1, color: 'var(--md-h1)', fontWeight: '700' },
{ tag: t.heading2, color: 'var(--md-h2)', fontWeight: '700' },
{ tag: t.heading3, color: 'var(--md-h3)', fontWeight: '600' },
{ tag: t.heading4, color: 'var(--md-h4)', fontWeight: '600' },
{ tag: t.heading5, color: 'var(--md-h5)', fontWeight: '600' },
{ tag: t.heading6, color: 'var(--md-h6)', fontWeight: '600' },
{ tag: t.heading, color: 'var(--md-heading-accent, var(--md-h2))', fontWeight: '700' },
{ tag: t.emphasis, color: 'var(--md-text)', fontStyle: 'italic' },
{ tag: t.strong, color: 'var(--md-text)', fontWeight: '700' },
{ tag: t.strikethrough, color: 'var(--md-text)', textDecoration: 'line-through' },
{ tag: t.comment, color: 'var(--md-muted)', fontStyle: 'italic' },
{ tag: t.meta, color: 'var(--md-muted)' },
{ tag: t.quote, color: 'var(--md-quote-fg)', fontStyle: 'italic' },
{ tag: t.list, color: 'var(--md-text)' },
{ tag: [t.keyword], color: 'var(--md-syntax-1)' },
{ tag: [t.typeName, t.className], color: 'var(--md-syntax-2)' },
{ tag: t.string, color: 'var(--md-syntax-3)' },
{ tag: [t.number, t.bool], color: 'var(--md-syntax-4)' },
{ tag: [t.punctuation, t.operator], color: 'var(--md-syntax-5)' },
{ tag: [t.link, t.url, t.escape], color: 'var(--md-link)', textDecoration: 'underline' },
{ tag: t.monospace, class: 'cm-inline-code' },
{ tag: t.literal, color: 'var(--md-text)' },
]);
export function markdownThemeHighlightExt(): Extension {
return [
rangedHighlightsExt(),
syntaxHighlighting(markdownThemeTokens),
markdownBaseTheme,
codeBlockPlugin,
createFacetApplier(),
];
}

View File

@ -0,0 +1,41 @@
import { Extension, Range, StateEffect, StateField, Transaction } from '@codemirror/state';
import { Decoration, DecorationSet, EditorView } from '@codemirror/view';
export interface HighlightRange { from: number; to: number }
export const setHighlights = StateEffect.define<HighlightRange[]>();
export const clearHighlights = StateEffect.define<null>();
function mapRanges(ranges: HighlightRange[], tr: Transaction): HighlightRange[] {
return ranges.map(r => ({ from: tr.changes.mapPos(r.from, 1), to: tr.changes.mapPos(r.to, 1) }));
}
function buildDecos(ranges: HighlightRange[]): DecorationSet {
const deco = Decoration.mark({ class: 'cm-md-highlight' });
const specs: Range<Decoration>[] = [] as any;
for (const r of ranges) {
if (r.to > r.from) specs.push(deco.range(r.from, r.to));
}
return Decoration.set(specs, true);
}
export const rangedHighlightsField = StateField.define<DecorationSet>({
create() { return Decoration.none; },
update(decos, tr) {
decos = decos.map(tr.changes);
for (const e of tr.effects) {
if (e.is(setHighlights)) {
const mapped = mapRanges(e.value, tr);
decos = buildDecos(mapped);
} else if (e.is(clearHighlights)) {
decos = Decoration.none;
}
}
return decos;
},
provide: f => EditorView.decorations.from(f)
});
export function rangedHighlightsExt(): Extension {
return [rangedHighlightsField];
}

View File

@ -7,14 +7,14 @@
<ng-container *ngIf="currentTags().length; else emptyTpl"> <ng-container *ngIf="currentTags().length; else emptyTpl">
<div class="flex flex-wrap gap-1.5 items-center"> <div class="flex flex-wrap gap-1.5 items-center">
<ng-container *ngFor="let t of currentTags()"> <ng-container *ngFor="let t of currentTags()">
<span class="inline-flex items-center gap-1 rounded-xl bg-slate-100 dark:bg-slate-800 px-2 py-1 text-xs font-medium text-slate-700 dark:text-slate-300 border border-slate-200 dark:border-slate-700 shadow-sm transition-all duration-150 hover:bg-slate-200 dark:hover:bg-slate-700"> <span class="inline-flex items-center gap-1 rounded-xl bg-surface1 dark:bg-card px-2 py-1 text-xs font-medium text-main dark:text-main border border-border dark:border-border shadow-sm transition-all duration-150 hover:bg-surface2 dark:hover:bg-surface2">
{{ t }} {{ t }}
</span> </span>
</ng-container> </ng-container>
</div> </div>
</ng-container> </ng-container>
<ng-template #emptyTpl> <ng-template #emptyTpl>
<span class="text-slate-500 dark:text-slate-400 text-xs italic">Cliquer pour ajouter des tags</span> <span class="text-muted dark:text-muted text-xs italic">Cliquer pour ajouter des tags</span>
</ng-template> </ng-template>
</div> </div>
</div> </div>
@ -22,21 +22,21 @@
<ng-template #editTpl> <ng-template #editTpl>
<div class="relative w-full animate-in fade-in duration-200"> <div class="relative w-full animate-in fade-in duration-200">
<!-- Carte d'édition avec backdrop blur --> <!-- Carte d'édition avec backdrop blur -->
<div class="rounded-2xl p-3 sm:p-4 shadow-md border border-slate-200 dark:border-slate-700 bg-white/60 dark:bg-slate-900/50 backdrop-blur"> <div class="rounded-2xl p-3 sm:p-4 shadow-md border border-border dark:border-border bg-card/60 dark:bg-main/50 backdrop-blur">
<div class="flex items-start gap-2"> <div class="flex items-start gap-2">
<!-- Zone des chips + input --> <!-- Zone des chips + input -->
<div class="flex-1 flex flex-wrap items-center gap-1.5 rounded-xl border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 px-3 py-2 focus-within:ring-2 focus-within:ring-sky-400 dark:focus-within:ring-sky-500 transition-all duration-200"> <div class="flex-1 flex flex-wrap items-center gap-1.5 rounded-xl border border-border dark:border-border bg-card dark:bg-card px-3 py-2 focus-within:ring-2 focus-within:ring-ring transition-all duration-200">
<ng-container *ngFor="let tagState of workingTags()"> <ng-container *ngFor="let tagState of workingTags()">
<span *ngIf="tagState.status !== 'removed'" <span *ngIf="tagState.status !== 'removed'"
class="inline-flex items-center gap-1 rounded-xl px-2 py-1 text-xs font-medium border shadow-sm transition-all duration-150" class="inline-flex items-center gap-1 rounded-xl px-2 py-1 text-xs font-medium border shadow-sm transition-all duration-150"
[ngClass]="{ [ngClass]="{
'border-emerald-300/70 bg-emerald-50 dark:bg-emerald-900/20 text-emerald-700 dark:text-emerald-300': tagState.status === 'added', 'border-emerald-300/70 bg-emerald-50 dark:bg-emerald-900/20 text-emerald-700 dark:text-emerald-300': tagState.status === 'added',
'border-slate-300 dark:border-slate-600 bg-slate-50 dark:bg-slate-700 text-slate-700 dark:text-slate-300': tagState.status === 'unchanged' 'border-border dark:border-border bg-surface1 dark:bg-surface2 text-main dark:text-main': tagState.status === 'unchanged'
}"> }">
<span *ngIf="tagState.status === 'added'" class="text-emerald-600 dark:text-emerald-400 font-bold">+</span> <span *ngIf="tagState.status === 'added'" class="text-emerald-600 dark:text-emerald-400 font-bold">+</span>
{{ tagState.value }} {{ tagState.value }}
<button type="button" <button type="button"
class="ml-0.5 opacity-60 hover:opacity-100 hover:text-rose-600 dark:hover:text-rose-400 transition-all" class="ml-0.5 btn btn-ghost btn-sm !px-1 !py-1 opacity-80 hover:opacity-100 hover:!text-rose-600 dark:hover:!text-rose-400 transition-all"
(click)="removeTag(tagState)" (click)="removeTag(tagState)"
aria-label="Supprimer"> aria-label="Supprimer">
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg> <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
@ -45,7 +45,7 @@
</ng-container> </ng-container>
<input data-tag-input <input data-tag-input
type="text" type="text"
class="min-w-[10ch] flex-1 bg-transparent text-sm outline-none placeholder:text-slate-400 dark:placeholder:text-slate-500 text-slate-900 dark:text-slate-100" class="min-w-[10ch] flex-1 bg-transparent text-sm outline-none placeholder:text-muted dark:placeholder:text-muted text-main dark:text-main"
[value]="inputValue()" [value]="inputValue()"
(input)="onInput($event)" (input)="onInput($event)"
(keydown)="onInputKeydown($event)" (keydown)="onInputKeydown($event)"
@ -55,7 +55,7 @@
<!-- Bouton Fermer l'édition --> <!-- Bouton Fermer l'édition -->
<button type="button" <button type="button"
class="flex-shrink-0 inline-flex items-center justify-center w-9 h-9 rounded-xl border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-700 hover:text-slate-900 dark:hover:text-slate-200 transition-all duration-150 shadow-sm" class="flex-shrink-0 btn btn-outline btn-sm w-9 h-9 !p-0"
(click)="exitEdit()" (click)="exitEdit()"
[disabled]="saving()" [disabled]="saving()"
aria-label="Fermer l'édition" aria-label="Fermer l'édition"
@ -67,25 +67,25 @@
</div> </div>
<!-- Suggestions dropdown --> <!-- Suggestions dropdown -->
<div *ngIf="menuOpen()" class="absolute left-0 top-[calc(100%+8px)] z-50 max-h-[280px] w-full min-w-[280px] overflow-auto rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 backdrop-blur-sm shadow-2xl animate-in fade-in slide-in-from-top-2 duration-200"> <div *ngIf="menuOpen()" class="absolute left-0 top-[calc(100%+8px)] z-50 max-h-[280px] w-full min-w-[280px] overflow-auto rounded-xl border border-border dark:border-border bg-card dark:bg-main backdrop-blur-sm shadow-2xl animate-in fade-in slide-in-from-top-2 duration-200">
<ng-container *ngIf="suggestions().length; else createTpl"> <ng-container *ngIf="suggestions().length; else createTpl">
<button type="button" <button type="button"
*ngFor="let s of suggestions(); let i = index" *ngFor="let s of suggestions(); let i = index"
(mousedown)="$event.preventDefault()" (mousedown)="$event.preventDefault()"
(click)="pickSuggestion(i)" (click)="pickSuggestion(i)"
class="block w-full text-left px-3 py-2 text-sm transition-all duration-150 hover:bg-slate-100 dark:hover:bg-slate-800 hover:pl-4 text-slate-900 dark:text-slate-100" class="block w-full text-left px-3 py-2 text-sm transition-all duration-150 hover:bg-surface1 dark:hover:bg-card hover:pl-4 text-main dark:text-main"
[ngClass]="{ [ngClass]="{
'bg-sky-50 dark:bg-sky-900/20 border-l-2 border-sky-500': menuIndex() === i, 'bg-sky-50 dark:bg-sky-900/20 border-l-2 border-sky-500': menuIndex() === i,
'opacity-40 cursor-not-allowed': isSelected(s) 'opacity-40 cursor-not-allowed': isSelected(s)
}" }"
[attr.aria-disabled]="isSelected(s)"> [attr.aria-disabled]="isSelected(s)">
<span class="font-medium">{{ s }}</span> <span class="font-medium">{{ s }}</span>
<span *ngIf="isSelected(s)" class="float-right text-xs text-slate-400 dark:text-slate-500 italic">✓ sélectionné</span> <span *ngIf="isSelected(s)" class="float-right text-xs text-muted dark:text-muted italic">✓ sélectionné</span>
</button> </button>
</ng-container> </ng-container>
<ng-template #createTpl> <ng-template #createTpl>
<button type="button" <button type="button"
class="block w-full text-left px-3 py-2 text-sm text-sky-600 dark:text-sky-400 hover:bg-slate-100 dark:hover:bg-slate-800 font-medium" class="block w-full text-left px-3 py-2 text-sm text-link hover:bg-surface1 dark:hover:bg-card font-medium"
(mousedown)="$event.preventDefault()" (mousedown)="$event.preventDefault()"
(click)="pickSuggestion(-1)"> (click)="pickSuggestion(-1)">
<span class="opacity-60">+</span> Créer « {{ inputValue().trim() }} » <span class="opacity-60">+</span> Créer « {{ inputValue().trim() }} »

View File

@ -1,23 +1,23 @@
<div class="fixed inset-0 z-50 flex items-center justify-center"> <div class="fixed inset-0 z-50 flex items-center justify-center">
<div class="absolute inset-0 bg-black/40"></div> <div class="absolute inset-0 bg-black/40"></div>
<div class="relative rounded-2xl shadow-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 p-4 md:p-5 w-[min(780px,92vw)]"> <div class="relative rounded-2xl shadow-xl border border-border dark:border-border bg-card dark:bg-main p-4 md:p-5 w-[min(780px,92vw)]">
<div class="flex items-center justify-between mb-3"> <div class="flex items-center justify-between mb-3">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<svg class="h-5 w-5 text-slate-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 7h.01"/><path d="M3 7h5a2 2 0 0 1 1.414.586l7 7a2 2 0 0 1 0 2.828l-3.172 3.172a2 2 0 0 1-2.828 0l-7-7A2 2 0 0 1 3 12V7Z"/></svg> <svg class="h-5 w-5 text-muted" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 7h.01"/><path d="M3 7h5a2 2 0 0 1 1.414.586l7 7a2 2 0 0 1 0 2.828l-3.172 3.172a2 2 0 0 1-2.828 0l-7-7A2 2 0 0 1 3 12V7Z"/></svg>
<h3 class="text-base font-semibold">Éditer les tags</h3> <h3 class="text-base font-semibold">Éditer les tags</h3>
<span class="text-sm text-slate-500">({{ count() }})</span> <span class="text-sm text-muted">({{ count() }})</span>
</div> </div>
<button type="button" class="inline-flex items-center justify-center w-8 h-8 rounded-full hover:bg-slate-500/10 dark:hover:bg-slate-200/10" (click)="close.emit()" aria-label="Fermer"> <button type="button" class="inline-flex items-center justify-center w-8 h-8 rounded-full hover:bg-surface1 dark:hover:bg-surface2" (click)="close.emit()" aria-label="Fermer">
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg> <svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
</button> </button>
</div> </div>
<div class="flex flex-col gap-3"> <div class="flex flex-col gap-3">
<div class="flex flex-wrap gap-2 rounded-xl border border-slate-200 dark:border-slate-700 p-3 min-h-[44px]"> <div class="flex flex-wrap gap-2 rounded-xl border border-border dark:border-border p-3 min-h-[44px]">
@for (t of working(); track t) { @for (t of working(); track t) {
<span class="inline-flex items-center gap-2 rounded-full px-3 py-1.5 text-sm font-medium bg-slate-100 dark:bg-slate-800"> <span class="inline-flex items-center gap-2 rounded-full px-3 py-1.5 text-sm font-medium bg-surface1 dark:bg-card">
{{ t }} {{ t }}
<button type="button" class="w-6 h-6 inline-flex items-center justify-center rounded-full hover:bg-slate-500/10 dark:hover:bg-slate-200/10" (click)="removeTag(t)" aria-label="Retirer"> <button type="button" class="w-6 h-6 inline-flex items-center justify-center rounded-full hover:bg-surface1 dark:hover:bg-surface2" (click)="removeTag(t)" aria-label="Retirer">
<svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg> <svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
</button> </button>
</span> </span>
@ -25,15 +25,15 @@
<input data-tag-input type="text" [value]="inputValue()" (input)="inputValue.set($any($event.target).value)" (keydown)="onKeydown($event)" placeholder="Ajouter un tag..." class="min-w-[200px] flex-1 bg-transparent outline-none px-2 py-1.5" /> <input data-tag-input type="text" [value]="inputValue()" (input)="inputValue.set($any($event.target).value)" (keydown)="onKeydown($event)" placeholder="Ajouter un tag..." class="min-w-[200px] flex-1 bg-transparent outline-none px-2 py-1.5" />
</div> </div>
<div class="rounded-xl border border-slate-200 dark:border-slate-700 overflow-hidden"> <div class="rounded-xl border border-border dark:border-border overflow-hidden">
<div class="max-h-64 overflow-y-auto"> <div class="max-h-64 overflow-y-auto">
@if (suggestions().length === 0) { @if (suggestions().length === 0) {
<div class="px-3 py-2 text-sm text-slate-500">Aucune suggestion</div> <div class="px-3 py-2 text-sm text-muted">Aucune suggestion</div>
} @else { } @else {
<ul> <ul>
@for (s of suggestions(); track s) { @for (s of suggestions(); track s) {
<li> <li>
<button type="button" class="w-full text-left px-3 py-2 hover:bg-slate-50 dark:hover:bg-slate-800" (click)="pickSuggestion(s)">{{ s }}</button> <button type="button" class="w-full text-left px-3 py-2 hover:bg-surface1 dark:hover:bg-card" (click)="pickSuggestion(s)">{{ s }}</button>
</li> </li>
} }
</ul> </ul>
@ -42,8 +42,8 @@
</div> </div>
<div class="flex items-center justify-end gap-2 pt-1"> <div class="flex items-center justify-end gap-2 pt-1">
<button type="button" class="px-3 py-1.5 rounded-lg border border-slate-300 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-800" (click)="close.emit()">Annuler</button> <button type="button" class="px-3 py-1.5 rounded-lg border border-border dark:border-border hover:bg-surface1 dark:hover:bg-card" (click)="close.emit()">Annuler</button>
<button type="button" class="px-3 py-1.5 rounded-lg bg-blue-600 hover:bg-blue-700 text-white disabled:opacity-60" [disabled]="saving()" (click)="save()">Enregistrer</button> <button type="button" class="px-3 py-1.5 rounded-lg bg-primary hover:bg-brand-700 text-white disabled:opacity-60" [disabled]="saving()" (click)="save()">Enregistrer</button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,7 +1,7 @@
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button <button
type="button" type="button"
class="inline-flex items-center justify-center w-9 h-9 md:w-9 md:h-9 sm:w-8 sm:h-8 rounded-full hover:bg-slate-500/10 dark:hover:bg-slate-200/10" class="inline-flex items-center justify-center w-9 h-9 md:w-9 md:h-9 sm:w-8 sm:h-8 rounded-full hover:bg-slate-500/10 dark:hover:bg-surface2/10"
aria-label="Modifier les tags" aria-label="Modifier les tags"
[attr.aria-pressed]="isEditing()" [attr.aria-pressed]="isEditing()"
(click)="toggleEditor()" (click)="toggleEditor()"
@ -12,7 +12,7 @@
<div class="flex flex-wrap items-center gap-1"> <div class="flex flex-wrap items-center gap-1">
<button <button
type="button" type="button"
class="px-3 py-1.5 rounded-full bg-slate-700/10 dark:bg-slate-300/10 text-sm font-medium hover:bg-slate-500/10 dark:hover:bg-slate-200/10" class="px-3 py-1.5 rounded-full bg-slate-700/10 dark:bg-muted/10 text-sm font-medium hover:bg-slate-500/10 dark:hover:bg-surface2/10"
*ngFor="let tag of normalizedTags()" *ngFor="let tag of normalizedTags()"
(click)="onChipClick(tag)" (click)="onChipClick(tag)"
[title]="'Voir les notes #'+tag"> [title]="'Voir les notes #'+tag">

View File

@ -2,7 +2,7 @@
<div class="pointer-events-none fixed bottom-4 right-4 z-[9999] flex w-[min(92vw,420px)] flex-col gap-2"> <div class="pointer-events-none fixed bottom-4 right-4 z-[9999] flex w-[min(92vw,420px)] flex-col gap-2">
<div <div
*ngFor="let t of toasts()" *ngFor="let t of toasts()"
class="pointer-events-auto overflow-hidden rounded-2xl border shadow-lg transition-all duration-300 ease-out data-[closing=true]:translate-y-2 data-[closing=true]:opacity-0 bg-slate-800/90 text-slate-50 border-slate-700" class="pointer-events-auto overflow-hidden rounded-2xl border shadow-lg transition-all duration-300 ease-out data-[closing=true]:translate-y-2 data-[closing=true]:opacity-0 bg-card text-main border-border"
[attr.data-closing]="t.closing ? 'true' : 'false'" [attr.data-closing]="t.closing ? 'true' : 'false'"
> >
<!-- Contenu --> <!-- Contenu -->
@ -14,17 +14,17 @@
<span *ngIf="t.type==='error'"></span> <span *ngIf="t.type==='error'"></span>
</div> </div>
<div class="flex-1 text-sm leading-relaxed">{{ t.message }}</div> <div class="flex-1 text-sm leading-relaxed">{{ t.message }}</div>
<button class="ml-1 rounded-md px-2 py-1 text-xs opacity-70 hover:opacity-100" (click)="dismiss(t.id)" aria-label="Fermer"></button> <button class="ml-1 rounded-md px-2 py-1 text-xs opacity-70 hover:opacity-100 text-muted hover:text-main" (click)="dismiss(t.id)" aria-label="Fermer"></button>
</div> </div>
<!-- Barre de progression en haut --> <!-- Barre de progression en haut -->
<div class="relative h-1 w-full bg-transparent"> <div class="relative h-1 w-full bg-transparent">
<div class="absolute left-0 top-0 h-1 transition-[width] duration-100 ease-linear" <div class="absolute left-0 top-0 h-1 transition-[width] duration-100 ease-linear"
[ngClass]="{ [ngClass]="{
'bg-sky-500': t.type==='info', 'bg-info': t.type==='info',
'bg-emerald-500': t.type==='success', 'bg-success': t.type==='success',
'bg-amber-500': t.type==='warning', 'bg-warning': t.type==='warning',
'bg-rose-500': t.type==='error' 'bg-danger': t.type==='error'
}" }"
[style.width.%]="t.progress * 100"></div> [style.width.%]="t.progress * 100"></div>
</div> </div>

View File

@ -1,13 +1,13 @@
<div class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4" (click)="onBackdropClick($event)"> <div class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4" (click)="onBackdropClick($event)">
<div class="bg-white dark:bg-gray-900 rounded-lg shadow-xl max-w-2xl w-full p-6" (click)="$event.stopPropagation()"> <div class="bg-card dark:bg-main rounded-lg shadow-xl max-w-2xl w-full p-6" (click)="$event.stopPropagation()">
<!-- Header --> <!-- Header -->
<div class="flex items-center justify-between mb-6"> <div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-semibold text-gray-900 dark:text-gray-100"> <h2 class="text-xl font-semibold text-main dark:text-gray-100">
{{ isEditMode() ? 'Edit bookmark' : 'Add bookmark' }} {{ isEditMode() ? 'Edit bookmark' : 'Add bookmark' }}
</h2> </h2>
<button <button
(click)="onCancel()" (click)="onCancel()"
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors" class="text-muted hover:text-muted dark:hover:text-gray-300 transition-colors"
aria-label="Close"> aria-label="Close">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
@ -19,41 +19,41 @@
<div class="space-y-4"> <div class="space-y-4">
<!-- Path --> <!-- Path -->
<div> <div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"> <label class="block text-sm font-medium text-main dark:text-main mb-2">
Path Path
</label> </label>
<input <input
type="text" type="text"
[value]="path()" [value]="path()"
(input)="onPathChange($any($event.target).value)" (input)="onPathChange($any($event.target).value)"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500" class="w-full px-3 py-2 border border-border dark:border-border rounded-md bg-card dark:bg-card text-main dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="e.g. Tests/Allo note.md" placeholder="e.g. Tests/Allo note.md"
/> />
</div> </div>
<!-- Title --> <!-- Title -->
<div> <div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"> <label class="block text-sm font-medium text-main dark:text-main mb-2">
Title Title
</label> </label>
<input <input
type="text" type="text"
[value]="title()" [value]="title()"
(input)="onTitleChange($any($event.target).value)" (input)="onTitleChange($any($event.target).value)"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500" class="w-full px-3 py-2 border border-border dark:border-border rounded-md bg-card dark:bg-card text-main dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Display name (optional)" placeholder="Display name (optional)"
/> />
</div> </div>
<!-- Bookmark group --> <!-- Bookmark group -->
<div> <div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"> <label class="block text-sm font-medium text-main dark:text-main mb-2">
Bookmark group Bookmark group
</label> </label>
<select <select
[value]="selectedGroupCtime() || ''" [value]="selectedGroupCtime() || ''"
(change)="onGroupChange($any($event.target).value)" (change)="onGroupChange($any($event.target).value)"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"> class="w-full px-3 py-2 border border-border dark:border-border rounded-md bg-card dark:bg-card text-main dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="">Root (no group)</option> <option value="">Root (no group)</option>
@for (group of groups(); track group.ctime) { @for (group of groups(); track group.ctime) {
<option [value]="group.ctime">{{ group.title }}</option> <option [value]="group.ctime">{{ group.title }}</option>
@ -79,7 +79,7 @@
<div class="flex gap-3"> <div class="flex gap-3">
<button <button
(click)="onCancel()" (click)="onCancel()"
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-md transition-colors"> class="px-4 py-2 text-sm font-medium text-main dark:text-main bg-surface1 dark:bg-surface2 hover:bg-surface2 dark:hover:bg-gray-600 rounded-md transition-colors">
Cancel Cancel
</button> </button>
<button <button

View File

@ -1,11 +1,11 @@
<div class="bookmark-node"> <div class="bookmark-node">
<div <div
class="relative flex items-center gap-2 px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md cursor-pointer transition-colors" class="relative flex items-center gap-2 px-3 py-2 hover:bg-surface1 dark:hover:bg-card rounded-md cursor-pointer transition-colors"
[style.padding-left]="indentStyle" [style.padding-left]="indentStyle"
(click)="onClick($event)" (click)="onClick($event)"
(contextmenu)="onContextMenu($event)" (contextmenu)="onContextMenu($event)"
[class.bg-gray-50]="isGroup" [class.bg-surface1]="isGroup"
[class.dark:bg-gray-800/50]="isGroup"> [class.dark:bg-card/50]="isGroup">
<!-- Icon --> <!-- Icon -->
<span class="text-base select-none" [class.cursor-pointer]="isGroup"> <span class="text-base select-none" [class.cursor-pointer]="isGroup">
@ -14,7 +14,7 @@
<!-- Text --> <!-- Text -->
<span <span
class="flex-1 text-sm text-gray-900 dark:text-gray-100 truncate" class="flex-1 text-sm text-main dark:text-gray-100 truncate"
[title]="displayText"> [title]="displayText">
{{ displayText }} {{ displayText }}
</span> </span>
@ -22,13 +22,13 @@
@if (isGroup) { @if (isGroup) {
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<button <button
class="p-1 text-gray-500 hover:text-blue-600 dark:text-gray-400 dark:hover:text-blue-400 transition-colors" class="p-1 text-muted hover:text-blue-600 dark:text-muted dark:hover:text-blue-400 transition-colors"
title="Ajouter un favori dans ce groupe" title="Ajouter un favori dans ce groupe"
(click)="onAddBookmark($event)"> (click)="onAddBookmark($event)">
</button> </button>
<button <button
class="p-1 text-gray-500 hover:text-red-600 dark:text-gray-400 dark:hover:text-red-400 transition-colors" class="p-1 text-muted hover:text-red-600 dark:text-muted dark:hover:text-red-400 transition-colors"
title="Supprimer ce groupe" title="Supprimer ce groupe"
(click)="onDelete($event)"> (click)="onDelete($event)">
🗑 🗑
@ -38,14 +38,14 @@
<!-- Badge for group count --> <!-- Badge for group count -->
@if (isGroup && hasChildren) { @if (isGroup && hasChildren) {
<span class="text-xs text-gray-500 dark:text-gray-400 bg-gray-200 dark:bg-gray-700 px-2 py-0.5 rounded-full"> <span class="text-xs text-muted dark:text-muted bg-surface2 dark:bg-surface2 px-2 py-0.5 rounded-full">
{{ children.length }} {{ children.length }}
</span> </span>
} }
<!-- Context Menu Button --> <!-- Context Menu Button -->
<button <button
class="opacity-0 hover:opacity-100 focus:opacity-100 p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 rounded transition-opacity" class="opacity-0 hover:opacity-100 focus:opacity-100 p-1 text-muted hover:text-main dark:text-muted dark:hover:text-main rounded transition-opacity"
(click)="onContextMenu($event); $event.stopPropagation()" (click)="onContextMenu($event); $event.stopPropagation()"
title="More options"> title="More options">
@ -54,20 +54,20 @@
<!-- Context Menu --> <!-- Context Menu -->
@if (showMenu()) { @if (showMenu()) {
<div <div
class="absolute right-0 top-full mt-1 z-10 w-48 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-md shadow-lg" class="absolute right-0 top-full mt-1 z-10 w-48 bg-card dark:bg-card border border-border dark:border-border rounded-md shadow-lg"
(click)="$event.stopPropagation()"> (click)="$event.stopPropagation()">
@if (isGroup) { @if (isGroup) {
<button <button
class="w-full px-4 py-2 text-left text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors" class="w-full px-4 py-2 text-left text-sm text-main dark:text-main hover:bg-surface1 dark:hover:bg-surface2 transition-colors"
(click)="onAddBookmark()"> (click)="onAddBookmark()">
Add Bookmark Here Add Bookmark Here
</button> </button>
<button <button
class="w-full px-4 py-2 text-left text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors" class="w-full px-4 py-2 text-left text-sm text-main dark:text-main hover:bg-surface1 dark:hover:bg-surface2 transition-colors"
(click)="onAddGroup()"> (click)="onAddGroup()">
Add Subgroup Add Subgroup
</button> </button>
<hr class="border-gray-200 dark:border-gray-700" /> <hr class="border-border dark:border-border" />
<button <button
class="w-full px-4 py-2 text-left text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors" class="w-full px-4 py-2 text-left text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
(click)="onDelete()"> (click)="onDelete()">
@ -75,11 +75,11 @@
</button> </button>
} @else { } @else {
<button <button
class="w-full px-4 py-2 text-left text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors" class="w-full px-4 py-2 text-left text-sm text-main dark:text-main hover:bg-surface1 dark:hover:bg-surface2 transition-colors"
(click)="onEdit()"> (click)="onEdit()">
Edit Edit
</button> </button>
<hr class="border-gray-200 dark:border-gray-700" /> <hr class="border-border dark:border-border" />
<button <button
class="w-full px-4 py-2 text-left text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors" class="w-full px-4 py-2 text-left text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
(click)="onDelete()"> (click)="onDelete()">
@ -93,7 +93,7 @@
<!-- Drop list for this group (always present for drag & drop to work) --> <!-- Drop list for this group (always present for drag & drop to work) -->
@if (isGroup) { @if (isGroup) {
<div <div
class="ml-6 border-l border-gray-200 dark:border-gray-700 pl-2 min-h-[40px] transition-colors" class="ml-6 border-l border-border dark:border-border pl-2 min-h-[40px] transition-colors"
[class.border-blue-500]="isDraggingOver()" [class.border-blue-500]="isDraggingOver()"
[class.dark:border-blue-400]="isDraggingOver()" [class.dark:border-blue-400]="isDraggingOver()"
[class.bg-blue-500/5]="isDraggingOver()" [class.bg-blue-500/5]="isDraggingOver()"
@ -128,12 +128,12 @@
<!-- Drop zone (always visible for groups) --> <!-- Drop zone (always visible for groups) -->
<div class="min-h-[20px] flex items-center justify-center"> <div class="min-h-[20px] flex items-center justify-center">
@if (isExpanded() && children.length === 0) { @if (isExpanded() && children.length === 0) {
<div class="py-2 px-3 text-xs text-gray-400 dark:text-gray-500 italic"> <div class="py-2 px-3 text-xs text-muted dark:text-muted italic">
Drop items here Drop items here
</div> </div>
} }
@if (!isExpanded()) { @if (!isExpanded()) {
<div class="py-1 px-2 text-xs text-gray-400 dark:text-gray-500 italic opacity-50"> <div class="py-1 px-2 text-xs text-muted dark:text-muted italic opacity-50">
Drop here ({{ children.length }} items) Drop here ({{ children.length }} items)
</div> </div>
} }

View File

@ -1,12 +1,12 @@
<div class="bookmarks-panel flex flex-col h-full bg-white dark:bg-gray-900"> <div class="bookmarks-panel flex flex-col h-full bg-card dark:bg-main">
<!-- Header --> <!-- Header -->
<div class="bookmarks-header flex-shrink-0 p-4 border-b border-gray-200 dark:border-gray-700"> <div class="bookmarks-header flex-shrink-0 p-4 border-b border-border dark:border-border">
<div class="flex items-center justify-between mb-3"> <div class="flex items-center justify-between mb-3">
<h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Bookmarks</h2> <h2 class="text-lg font-semibold text-main dark:text-gray-100">Bookmarks</h2>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button <button
type="button" type="button"
class="btn-standard-sm" class="btn btn-outline btn-sm"
(click)="createGroup()" (click)="createGroup()"
title="Créer un groupe"> title="Créer un groupe">
+ Group + Group
@ -24,13 +24,14 @@
[value]="searchTerm()" [value]="searchTerm()"
(input)="onSearchChange($any($event.target).value)" (input)="onSearchChange($any($event.target).value)"
placeholder="Search bookmarks..." placeholder="Search bookmarks..."
class="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500" class="w-full px-3 py-2 text-sm border border-border dark:border-border rounded-md bg-card dark:bg-card text-main dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
/> />
@if (searchTerm()) { @if (searchTerm()) {
<button <button
(click)="onSearchChange('')" (click)="onSearchChange('')"
class="btn-standard-icon absolute right-2 top-1/2 -translate-y-1/2" class="btn btn-ghost btn-sm absolute right-2 top-1/2 -translate-y-1/2"
title="Clear search"> title="Clear search"
aria-label="Clear search">
</button> </button>
} }
@ -41,7 +42,7 @@
<!-- Body --> <!-- Body -->
<div class="bookmarks-body flex-1 overflow-y-auto p-4"> <div class="bookmarks-body flex-1 overflow-y-auto p-4">
@if (loading()) { @if (loading()) {
<div class="flex items-center justify-center py-8 text-gray-500 dark:text-gray-400"> <div class="flex items-center justify-center py-8 text-muted dark:text-muted">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div> <div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div> </div>
} @else if (error()) { } @else if (error()) {
@ -51,14 +52,14 @@
<span class="text-sm text-red-700 dark:text-red-300 flex-1">{{ error() }}</span> <span class="text-sm text-red-700 dark:text-red-300 flex-1">{{ error() }}</span>
<button <button
(click)="clearError()" (click)="clearError()"
class="btn-standard-icon !text-red-600 dark:!text-red-400 hover:!text-red-800 dark:hover:!text-red-200" class="btn btn-ghost btn-sm !text-red-600 dark:!text-red-400 hover:!text-red-800 dark:hover:!text-red-200"
title="Dismiss"> title="Dismiss" aria-label="Dismiss">
</button> </button>
</div> </div>
</div> </div>
} @else if (isEmpty()) { } @else if (isEmpty()) {
<div class="text-center py-8 text-gray-500 dark:text-gray-400"> <div class="text-center py-8 text-muted dark:text-muted">
<p class="mb-4">No bookmarks yet</p> <p class="mb-4">No bookmarks yet</p>
<p class="text-sm">Use the bookmark icon in the note toolbar to add one.</p> <p class="text-sm">Use the bookmark icon in the note toolbar to add one.</p>
</div> </div>
@ -115,7 +116,7 @@
class="mb-1" /> class="mb-1" />
} }
@if (displayItems().length === 0) { @if (displayItems().length === 0) {
<p class="text-sm text-gray-400 dark:text-gray-500 py-4 text-center">No bookmarks to display.</p> <p class="text-sm text-muted dark:text-muted py-4 text-center">No bookmarks to display.</p>
} }
</div> </div>
} }
@ -124,20 +125,20 @@
<!-- Conflict Modal --> <!-- Conflict Modal -->
@if (conflictInfo()) { @if (conflictInfo()) {
<div class="fixed inset-0 bg-black/50 flex items-center justify-center z-50"> <div class="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full mx-4 p-6"> <div class="bg-card dark:bg-card rounded-lg shadow-xl max-w-md w-full mx-4 p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-3">Conflict Detected</h3> <h3 class="text-lg font-semibold text-main dark:text-gray-100 mb-3">Conflict Detected</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4"> <p class="text-sm text-muted dark:text-muted mb-4">
The bookmarks file has been modified externally. What would you like to do? The bookmarks file has been modified externally. What would you like to do?
</p> </p>
<div class="flex gap-3"> <div class="flex gap-3">
<button <button
(click)="resolveConflictReload()" (click)="resolveConflictReload()"
class="btn-standard-primary flex-1"> class="btn btn-solid btn-md flex-1">
Reload from file Reload from file
</button> </button>
<button <button
(click)="resolveConflictOverwrite()" (click)="resolveConflictOverwrite()"
class="btn-standard-danger flex-1"> class="btn btn-outline btn-md flex-1">
Overwrite file Overwrite file
</button> </button>
</div> </div>

View File

@ -15,7 +15,7 @@ import { BadgeCountComponent } from '../../app/shared/ui/badge-count.component';
<div> <div>
<div <div
(click)="onFolderClick(folder)" (click)="onFolderClick(folder)"
class="flex items-center cursor-pointer px-3 py-2 rounded-lg my-0.5 text-sm hover:bg-slate-500/10 dark:hover:bg-slate-200/10 transition" class="flex items-center cursor-pointer px-3 py-2 rounded-lg my-0.5 text-sm hover:bg-slate-500/10 dark:hover:bg-surface2/10 transition"
> >
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2 transition-transform text-obs-l-text-muted dark:text-obs-d-text-muted" [class.rotate-90]="folder.isOpen" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2 transition-transform text-obs-l-text-muted dark:text-obs-d-text-muted" [class.rotate-90]="folder.isOpen" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
@ -45,7 +45,7 @@ import { BadgeCountComponent } from '../../app/shared/ui/badge-count.component';
[class.bg-obs-l-bg-main]="selectedNoteId() === file.id" [class.bg-obs-l-bg-main]="selectedNoteId() === file.id"
[class.dark:bg-obs-d-bg-main]="selectedNoteId() === file.id" [class.dark:bg-obs-d-bg-main]="selectedNoteId() === file.id"
[class.hover:bg-slate-500/10]="selectedNoteId() !== file.id" [class.hover:bg-slate-500/10]="selectedNoteId() !== file.id"
[class.dark:hover:bg-slate-200/10]="selectedNoteId() !== file.id" [class.dark:hover:bg-surface2/10]="selectedNoteId() !== file.id"
> >
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2 flex-shrink-0 text-obs-l-text-muted dark:text-obs-d-text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2 flex-shrink-0 text-obs-l-text-muted dark:text-obs-d-text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />

View File

@ -38,12 +38,12 @@ export interface GraphOptions {
imports: [CommonModule, FormsModule, SearchInputWithAssistantComponent], imports: [CommonModule, FormsModule, SearchInputWithAssistantComponent],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
template: ` template: `
<div class="graph-options-panel bg-white dark:bg-gray-800 border-l border-gray-200 dark:border-gray-700 h-full overflow-y-auto"> <div class="graph-options-panel bg-card dark:bg-card border-l border-border dark:border-border h-full overflow-y-auto">
<div class="p-4 space-y-6"> <div class="p-4 space-y-6">
<!-- Filters Section --> <!-- Filters Section -->
<section> <section>
<h3 class="text-sm font-semibold text-gray-900 dark:text-gray-100 mb-3 flex items-center gap-2"> <h3 class="text-sm font-semibold text-main dark:text-gray-100 mb-3 flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
</svg> </svg>
@ -56,8 +56,8 @@ export interface GraphOptions {
type="checkbox" type="checkbox"
[(ngModel)]="filters().showTags" [(ngModel)]="filters().showTags"
(change)="emitOptionsChange()" (change)="emitOptionsChange()"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"> class="rounded border-border text-blue-600 focus:ring-ring dark:border-border dark:bg-surface2">
<span class="text-sm text-gray-700 dark:text-gray-300 group-hover:text-gray-900 dark:group-hover:text-gray-100">Tags</span> <span class="text-sm text-main dark:text-main group-hover:text-main dark:group-hover:text-gray-100">Tags</span>
</label> </label>
<label class="flex items-center gap-2 cursor-pointer group"> <label class="flex items-center gap-2 cursor-pointer group">
@ -65,8 +65,8 @@ export interface GraphOptions {
type="checkbox" type="checkbox"
[(ngModel)]="filters().showAttachments" [(ngModel)]="filters().showAttachments"
(change)="emitOptionsChange()" (change)="emitOptionsChange()"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"> class="rounded border-border text-blue-600 focus:ring-ring dark:border-border dark:bg-surface2">
<span class="text-sm text-gray-700 dark:text-gray-300 group-hover:text-gray-900 dark:group-hover:text-gray-100">Attachments</span> <span class="text-sm text-main dark:text-main group-hover:text-main dark:group-hover:text-gray-100">Attachments</span>
</label> </label>
<label class="flex items-center gap-2 cursor-pointer group"> <label class="flex items-center gap-2 cursor-pointer group">
@ -74,8 +74,8 @@ export interface GraphOptions {
type="checkbox" type="checkbox"
[(ngModel)]="filters().existingFilesOnly" [(ngModel)]="filters().existingFilesOnly"
(change)="emitOptionsChange()" (change)="emitOptionsChange()"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"> class="rounded border-border text-blue-600 focus:ring-ring dark:border-border dark:bg-surface2">
<span class="text-sm text-gray-700 dark:text-gray-300 group-hover:text-gray-900 dark:group-hover:text-gray-100">Existing files only</span> <span class="text-sm text-main dark:text-main group-hover:text-main dark:group-hover:text-gray-100">Existing files only</span>
</label> </label>
<label class="flex items-center gap-2 cursor-pointer group"> <label class="flex items-center gap-2 cursor-pointer group">
@ -83,8 +83,8 @@ export interface GraphOptions {
type="checkbox" type="checkbox"
[(ngModel)]="filters().showOrphans" [(ngModel)]="filters().showOrphans"
(change)="emitOptionsChange()" (change)="emitOptionsChange()"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"> class="rounded border-border text-blue-600 focus:ring-ring dark:border-border dark:bg-surface2">
<span class="text-sm text-gray-700 dark:text-gray-300 group-hover:text-gray-900 dark:group-hover:text-gray-100">Orphans</span> <span class="text-sm text-main dark:text-main group-hover:text-main dark:group-hover:text-gray-100">Orphans</span>
</label> </label>
</div> </div>
@ -96,14 +96,14 @@ export interface GraphOptions {
[placeholder]="'Search files…'" [placeholder]="'Search files…'"
[context]="'graph'" [context]="'graph'"
[showSearchIcon]="true" [showSearchIcon]="true"
[inputClass]="'w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500'" [inputClass]="'w-full px-3 py-2 text-sm border border-border dark:border-border rounded-md bg-card dark:bg-surface2 text-main dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-ring'"
/> />
</div> </div>
</section> </section>
<!-- Groups Section --> <!-- Groups Section -->
<section> <section>
<h3 class="text-sm font-semibold text-gray-900 dark:text-gray-100 mb-3 flex items-center gap-2"> <h3 class="text-sm font-semibold text-main dark:text-gray-100 mb-3 flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
</svg> </svg>
@ -112,7 +112,7 @@ export interface GraphOptions {
<div class="space-y-2 mb-3"> <div class="space-y-2 mb-3">
@for (group of colorGroups(); track $index) { @for (group of colorGroups(); track $index) {
<div class="flex items-center gap-2 p-2 bg-gray-50 dark:bg-gray-700/50 rounded-lg"> <div class="flex items-center gap-2 p-2 bg-surface1 dark:bg-surface2/50 rounded-lg">
<div <div
[style.background-color]="getGroupColorPreview(group)" [style.background-color]="getGroupColorPreview(group)"
class="w-6 h-6 rounded-full border-2 border-white dark:border-gray-800 shadow-sm flex-shrink-0"> class="w-6 h-6 rounded-full border-2 border-white dark:border-gray-800 shadow-sm flex-shrink-0">
@ -122,11 +122,11 @@ export interface GraphOptions {
[value]="group.query" [value]="group.query"
(input)="updateGroupQuery($index, $any($event.target).value)" (input)="updateGroupQuery($index, $any($event.target).value)"
placeholder="Query (e.g., tag:#code)" placeholder="Query (e.g., tag:#code)"
class="flex-1 px-2 py-1 text-xs bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded" class="flex-1 px-2 py-1 text-xs bg-card dark:bg-card border border-border dark:border-border rounded"
/> />
<button <button
(click)="removeGroup($index)" (click)="removeGroup($index)"
class="p-1 text-gray-400 hover:text-red-500 transition-colors" class="p-1 text-muted hover:text-red-500 transition-colors"
title="Remove group" title="Remove group"
> >
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
@ -147,7 +147,7 @@ export interface GraphOptions {
<!-- Display Section --> <!-- Display Section -->
<section> <section>
<h3 class="text-sm font-semibold text-gray-900 dark:text-gray-100 mb-3 flex items-center gap-2"> <h3 class="text-sm font-semibold text-main dark:text-gray-100 mb-3 flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
@ -161,12 +161,12 @@ export interface GraphOptions {
type="checkbox" type="checkbox"
[(ngModel)]="display().showArrows" [(ngModel)]="display().showArrows"
(change)="emitOptionsChange()" (change)="emitOptionsChange()"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"> class="rounded border-border text-blue-600 focus:ring-blue-500 dark:border-border dark:bg-surface2">
<span class="text-sm text-gray-700 dark:text-gray-300 group-hover:text-gray-900 dark:group-hover:text-gray-100">Arrows</span> <span class="text-sm text-main dark:text-main group-hover:text-main dark:group-hover:text-gray-100">Arrows</span>
</label> </label>
<div> <div>
<label class="flex items-center justify-between text-sm text-gray-700 dark:text-gray-300 mb-1"> <label class="flex items-center justify-between text-sm text-main dark:text-main mb-1">
<span>Text fade threshold</span> <span>Text fade threshold</span>
<span class="font-mono text-xs">{{ display().textFadeThreshold }}</span> <span class="font-mono text-xs">{{ display().textFadeThreshold }}</span>
</label> </label>
@ -176,11 +176,11 @@ export interface GraphOptions {
max="100" max="100"
[(ngModel)]="display().textFadeThreshold" [(ngModel)]="display().textFadeThreshold"
(input)="emitOptionsChange()" (input)="emitOptionsChange()"
class="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer accent-blue-600"> class="w-full h-2 bg-surface2 dark:bg-surface2 rounded-lg appearance-none cursor-pointer accent-blue-600">
</div> </div>
<div> <div>
<label class="flex items-center justify-between text-sm text-gray-700 dark:text-gray-300 mb-1"> <label class="flex items-center justify-between text-sm text-main dark:text-main mb-1">
<span>Node size</span> <span>Node size</span>
<span class="font-mono text-xs">{{ display().nodeSize }}</span> <span class="font-mono text-xs">{{ display().nodeSize }}</span>
</label> </label>
@ -190,11 +190,11 @@ export interface GraphOptions {
max="20" max="20"
[(ngModel)]="display().nodeSize" [(ngModel)]="display().nodeSize"
(input)="emitOptionsChange()" (input)="emitOptionsChange()"
class="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer accent-blue-600"> class="w-full h-2 bg-surface2 dark:bg-surface2 rounded-lg appearance-none cursor-pointer accent-blue-600">
</div> </div>
<div> <div>
<label class="flex items-center justify-between text-sm text-gray-700 dark:text-gray-300 mb-1"> <label class="flex items-center justify-between text-sm text-main dark:text-main mb-1">
<span>Link thickness</span> <span>Link thickness</span>
<span class="font-mono text-xs">{{ display().linkThickness }}</span> <span class="font-mono text-xs">{{ display().linkThickness }}</span>
</label> </label>
@ -204,7 +204,7 @@ export interface GraphOptions {
max="10" max="10"
[(ngModel)]="display().linkThickness" [(ngModel)]="display().linkThickness"
(input)="emitOptionsChange()" (input)="emitOptionsChange()"
class="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer accent-blue-600"> class="w-full h-2 bg-surface2 dark:bg-surface2 rounded-lg appearance-none cursor-pointer accent-blue-600">
</div> </div>
</div> </div>
@ -221,7 +221,7 @@ export interface GraphOptions {
<button <button
type="button" type="button"
(click)="forcesExpanded.set(!forcesExpanded())" (click)="forcesExpanded.set(!forcesExpanded())"
class="w-full flex items-center justify-between text-sm font-semibold text-gray-900 dark:text-gray-100 mb-3"> class="w-full flex items-center justify-between text-sm font-semibold text-main dark:text-gray-100 mb-3">
<span class="flex items-center gap-2"> <span class="flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
@ -242,7 +242,7 @@ export interface GraphOptions {
@if (forcesExpanded()) { @if (forcesExpanded()) {
<div class="space-y-4"> <div class="space-y-4">
<div> <div>
<label class="flex items-center justify-between text-sm text-gray-700 dark:text-gray-300 mb-1"> <label class="flex items-center justify-between text-sm text-main dark:text-main mb-1">
<span>Charge</span> <span>Charge</span>
<span class="font-mono text-xs">{{ forces().chargeStrength }}</span> <span class="font-mono text-xs">{{ forces().chargeStrength }}</span>
</label> </label>
@ -252,11 +252,11 @@ export interface GraphOptions {
max="0" max="0"
[(ngModel)]="forces().chargeStrength" [(ngModel)]="forces().chargeStrength"
(input)="emitOptionsChange()" (input)="emitOptionsChange()"
class="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer accent-blue-600"> class="w-full h-2 bg-surface2 dark:bg-surface2 rounded-lg appearance-none cursor-pointer accent-blue-600">
</div> </div>
<div> <div>
<label class="flex items-center justify-between text-sm text-gray-700 dark:text-gray-300 mb-1"> <label class="flex items-center justify-between text-sm text-main dark:text-main mb-1">
<span>Link distance</span> <span>Link distance</span>
<span class="font-mono text-xs">{{ forces().linkDistance }}</span> <span class="font-mono text-xs">{{ forces().linkDistance }}</span>
</label> </label>
@ -266,11 +266,11 @@ export interface GraphOptions {
max="500" max="500"
[(ngModel)]="forces().linkDistance" [(ngModel)]="forces().linkDistance"
(input)="emitOptionsChange()" (input)="emitOptionsChange()"
class="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer accent-blue-600"> class="w-full h-2 bg-surface2 dark:bg-surface2 rounded-lg appearance-none cursor-pointer accent-blue-600">
</div> </div>
<div> <div>
<label class="flex items-center justify-between text-sm text-gray-700 dark:text-gray-300 mb-1"> <label class="flex items-center justify-between text-sm text-main dark:text-main mb-1">
<span>Center strength</span> <span>Center strength</span>
<span class="font-mono text-xs">{{ forces().centerStrength.toFixed(2) }}</span> <span class="font-mono text-xs">{{ forces().centerStrength.toFixed(2) }}</span>
</label> </label>
@ -281,7 +281,7 @@ export interface GraphOptions {
step="0.01" step="0.01"
[(ngModel)]="forces().centerStrength" [(ngModel)]="forces().centerStrength"
(input)="emitOptionsChange()" (input)="emitOptionsChange()"
class="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer accent-blue-600"> class="w-full h-2 bg-surface2 dark:bg-surface2 rounded-lg appearance-none cursor-pointer accent-blue-600">
</div> </div>
</div> </div>
} }

View File

@ -37,7 +37,7 @@ import { createFilteredGraphData, createGroupLegend, createFocusedGraphData } fr
], ],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
template: ` template: `
<div class="flex flex-col h-full w-full bg-gray-50 dark:bg-gray-900"> <div class="flex flex-col h-full w-full bg-surface1 dark:bg-main">
<!-- Main graph area --> <!-- Main graph area -->
<div class="flex-1 relative"> <div class="flex-1 relative">
<app-graph-canvas <app-graph-canvas
@ -53,10 +53,10 @@ import { createFilteredGraphData, createGroupLegend, createFocusedGraphData } fr
<button <button
type="button" type="button"
(click)="toggleSettings()" (click)="toggleSettings()"
class="absolute top-5 right-5 flex items-center justify-center w-11 h-11 rounded-full bg-white dark:bg-gray-800 shadow-lg hover:shadow-xl transition-all hover:rotate-90 z-50 border border-gray-200 dark:border-gray-700" class="absolute top-5 right-5 flex items-center justify-center w-11 h-11 rounded-full bg-card dark:bg-card shadow-lg hover:shadow-xl transition-all hover:rotate-90 z-50 border border-border dark:border-border"
aria-label="Graph settings" aria-label="Graph settings"
title="Graph settings"> title="Graph settings">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-gray-700 dark:text-gray-300" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-main dark:text-main" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /> d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
@ -66,7 +66,7 @@ import { createFilteredGraphData, createGroupLegend, createFocusedGraphData } fr
<!-- Legend at bottom --> <!-- Legend at bottom -->
@if (legendItems().length > 0) { @if (legendItems().length > 0) {
<div class="p-4 border-t border-gray-200 dark:border-gray-700"> <div class="p-4 border-t border-border dark:border-border">
<app-graph-legend <app-graph-legend
[items]="legendItems()" [items]="legendItems()"
(itemClicked)="onLegendItemClicked($event)"> (itemClicked)="onLegendItemClicked($event)">

View File

@ -37,7 +37,7 @@ export interface GraphDisplayOptions {
imports: [CommonModule], imports: [CommonModule],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
template: ` template: `
<div class="relative h-full w-full bg-gray-50 dark:bg-gray-900"> <div class="relative h-full w-full bg-surface1 dark:bg-main">
<svg #graphSvg class="w-full h-full"> <svg #graphSvg class="w-full h-full">
<defs> <defs>
<marker <marker
@ -99,7 +99,7 @@ export interface GraphDisplayOptions {
</svg> </svg>
<!-- Info overlay --> <!-- Info overlay -->
<div class="absolute top-4 left-4 bg-white dark:bg-gray-800 px-3 py-2 rounded-lg shadow-md text-xs text-gray-700 dark:text-gray-300"> <div class="absolute top-4 left-4 bg-card dark:bg-card px-3 py-2 rounded-lg shadow-md text-xs text-main dark:text-main">
<div><strong>Nodes:</strong> {{ simulatedNodes().length }}</div> <div><strong>Nodes:</strong> {{ simulatedNodes().length }}</div>
<div><strong>Links:</strong> {{ edges().length }}</div> <div><strong>Links:</strong> {{ edges().length }}</div>
</div> </div>

View File

@ -1,10 +1,11 @@
import { Component, Input, OnChanges, SimpleChanges, signal, computed, inject, effect, CUSTOM_ELEMENTS_SCHEMA, Output, EventEmitter } from '@angular/core'; import { Component, Input, OnChanges, SimpleChanges, signal, computed, inject, effect, CUSTOM_ELEMENTS_SCHEMA, Output, EventEmitter, ElementRef, ViewChild, AfterViewInit } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { MarkdownService } from '../../services/markdown.service'; import { MarkdownService } from '../../services/markdown.service';
import { Note } from '../../types'; import { Note } from '../../types';
import { DrawingsEditorComponent } from '../../app/features/drawings/drawings-editor.component'; import { DrawingsEditorComponent } from '../../app/features/drawings/drawings-editor.component';
import { EditorStateService } from '../../services/editor-state.service'; import { EditorStateService } from '../../services/editor-state.service';
import { TocService } from '../../services/toc.service';
/** /**
* Composant réutilisable pour afficher du contenu Markdown * Composant réutilisable pour afficher du contenu Markdown
@ -83,8 +84,9 @@ import { EditorStateService } from '../../services/editor-state.service';
<!-- Markdown Content --> <!-- Markdown Content -->
<div <div
*ngIf="!isExcalidrawFile()" *ngIf="!isExcalidrawFile()"
class="markdown-viewer__content prose prose-slate dark:prose-invert max-w-none" class="markdown-viewer__content"
[innerHTML]="renderedHtml()"> #contentRef>
<div class="md-view prose themed-prose max-w-none" [innerHTML]="renderedHtml()"></div>
</div> </div>
<!-- Error State --> <!-- Error State -->
@ -187,10 +189,12 @@ import { EditorStateService } from '../../services/editor-state.service';
} }
`] `]
}) })
export class MarkdownViewerComponent implements OnChanges { export class MarkdownViewerComponent implements OnChanges, AfterViewInit {
private markdownService = inject(MarkdownService); private markdownService = inject(MarkdownService);
private sanitizer = inject(DomSanitizer); private sanitizer = inject(DomSanitizer);
private editorState = inject(EditorStateService); private editorState = inject(EditorStateService);
private toc = inject(TocService);
@ViewChild('contentRef') private contentRef?: ElementRef<HTMLElement>;
/** Contenu markdown brut à afficher */ /** Contenu markdown brut à afficher */
@Input() content: string = ''; @Input() content: string = '';
@ -252,6 +256,21 @@ export class MarkdownViewerComponent implements OnChanges {
this.setupLazyLoading(); this.setupLazyLoading();
} }
}); });
// After each HTML render, apply anchors and scrollspy
effect(() => {
// depend on renderedHtml() signal
void this.renderedHtml();
if (this.isExcalidrawFile()) return;
// next tick to ensure DOM updated
setTimeout(() => {
const root = this.contentRef?.nativeElement as HTMLElement | undefined;
if (!root) return;
this.applyAnchors(root);
this.toc.extract(root);
this.toc.setupScrollSpy(root, 84);
}, 0);
});
} }
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
@ -302,4 +321,37 @@ export class MarkdownViewerComponent implements OnChanges {
} }
}, 0); }, 0);
} }
ngAfterViewInit(): void {
setTimeout(() => {
if (this.isExcalidrawFile()) return;
const root = this.contentRef?.nativeElement as HTMLElement | undefined;
if (!root) return;
this.applyAnchors(root);
this.toc.extract(root);
this.toc.setupScrollSpy(root, 84);
}, 0);
}
private applyAnchors(root: HTMLElement): void {
const headings = Array.from(root.querySelectorAll('h1, h2, h3, h4, h5, h6')) as HTMLHeadingElement[];
for (const h of headings) {
if (!h.id) {
// ensure id if missing
h.id = (h.textContent || '').trim().toLowerCase()
.normalize('NFD').replace(/[\u0300-\u036f]/g, '')
.replace(/[^\w\s-]/g, '')
.trim().replace(/\s+/g, '-').replace(/-+/g, '-') || 'section';
}
const exists = h.querySelector(':scope > a.heading-anchor');
if (!exists) {
const a = document.createElement('a');
a.className = 'heading-anchor';
a.href = `#${h.id}`;
a.textContent = '#';
a.setAttribute('aria-label', 'Anchor link');
h.appendChild(a);
}
}
}
} }

View File

@ -15,23 +15,23 @@ export interface PreviewData {
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
template: ` template: `
<div <div
class="preview-card bg-white dark:bg-gray-800 rounded-2xl shadow-xl border border-gray-200 dark:border-gray-700 p-4 max-w-md overflow-hidden" class="preview-card bg-card dark:bg-card rounded-2xl shadow-xl border border-border dark:border-border p-4 max-w-md overflow-hidden"
(mouseenter)="mouseEnter.emit()" (mouseenter)="mouseEnter.emit()"
(mouseleave)="mouseLeave.emit()"> (mouseleave)="mouseLeave.emit()">
<h2 class="font-bold text-lg mb-2 text-gray-900 dark:text-gray-100 truncate"> <h2 class="font-bold text-lg mb-2 text-main dark:text-gray-100 truncate">
{{ previewData().title }} {{ previewData().title }}
</h2> </h2>
<div <div
class="prose prose-sm dark:prose-invert max-w-none line-clamp-5 text-gray-700 dark:text-gray-300" class="prose prose-sm dark:prose-invert max-w-none line-clamp-5 text-main dark:text-main"
[innerHTML]="sanitizedExcerpt()"> [innerHTML]="sanitizedExcerpt()">
</div> </div>
<div class="mt-3 flex justify-end"> <div class="mt-3 flex justify-end">
<button <button
(click)="openNote.emit()" (click)="openNote.emit()"
class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 transition-colors p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-700" class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 transition-colors p-1 rounded hover:bg-surface1 dark:hover:bg-surface2"
aria-label="Ouvrir la note"> aria-label="Ouvrir la note">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />

View File

@ -32,7 +32,7 @@ import { SearchOptions } from '../../core/search/search-parser.types';
<!-- Search icon --> <!-- Search icon -->
<svg <svg
*ngIf="showSearchIcon" *ngIf="showSearchIcon"
class="pointer-events-none absolute left-3 top-1/2 h-5 w-5 -translate-y-1/2 text-text-muted dark:text-gray-400 z-10" class="pointer-events-none absolute left-3 top-1/2 h-5 w-5 -translate-y-1/2 text-text-muted dark:text-muted z-10"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@ -144,7 +144,7 @@ export class SearchBarComponent implements AfterViewInit, OnInit {
@Input() context: string = 'vault'; @Input() context: string = 'vault';
@Input() showExamples: boolean = true; @Input() showExamples: boolean = true;
@Input() showSearchIcon: boolean = true; @Input() showSearchIcon: boolean = true;
@Input() inputClass: string = 'w-full rounded-full border border-border bg-bg-muted/70 py-2.5 pr-4 text-sm text-text-main placeholder:text-text-muted shadow-subtle focus:outline-none focus:ring-2 focus:ring-ring transition-all dark:border-gray-600 dark:bg-gray-800/80 dark:text-gray-100 dark:placeholder-gray-400 dark:focus:ring-blue-500'; @Input() inputClass: string = 'w-full rounded-full border border-border bg-bg-muted/70 py-2.5 pr-4 text-sm text-text-main placeholder:text-text-muted shadow-subtle focus:outline-none focus:ring-2 focus:ring-ring transition-all dark:border-border dark:bg-card/80 dark:text-gray-100 dark:placeholder-gray-400 dark:focus:ring-ring';
@Input() initialQuery: string = ''; @Input() initialQuery: string = '';
@Input() caseSensitiveDefault: boolean = false; @Input() caseSensitiveDefault: boolean = false;
@Input() regexDefault: boolean = false; @Input() regexDefault: boolean = false;

View File

@ -31,7 +31,7 @@ import { SearchPreferencesService } from '../../core/search/search-preferences.s
<div class="relative"> <div class="relative">
<svg <svg
*ngIf="showSearchIcon" *ngIf="showSearchIcon"
class="pointer-events-none absolute left-3 top-1/2 h-5 w-5 -translate-y-1/2 text-text-muted dark:text-gray-400" class="pointer-events-none absolute left-3 top-1/2 h-5 w-5 -translate-y-1/2 text-text-muted dark:text-muted"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@ -97,7 +97,7 @@ export class SearchInputWithAssistantComponent implements AfterViewInit, OnInit
@Input() value: string = ''; @Input() value: string = '';
@Input() context: string = 'default'; @Input() context: string = 'default';
@Input() showSearchIcon: boolean = true; @Input() showSearchIcon: boolean = true;
@Input() inputClass: string = 'w-full rounded-full border border-border bg-bg-muted/70 py-2.5 pr-4 text-sm text-text-main placeholder:text-text-muted shadow-subtle focus:outline-none focus:ring-2 focus:ring-ring transition-all dark:border-gray-600 dark:bg-gray-800/80 dark:text-gray-100 dark:placeholder-gray-400 dark:focus:ring-blue-500'; @Input() inputClass: string = 'w-full rounded-full border border-border bg-bg-muted/70 py-2.5 pr-4 text-sm text-text-main placeholder:text-text-muted shadow-subtle focus:outline-none focus:ring-2 focus:ring-ring transition-all dark:border-border dark:bg-card/80 dark:text-gray-100 dark:placeholder-gray-400 dark:focus:ring-ring';
@Input() showExamples: boolean = true; @Input() showExamples: boolean = true;
@Output() valueChange = new EventEmitter<string>(); @Output() valueChange = new EventEmitter<string>();

View File

@ -39,9 +39,9 @@ import { parseSearchQuery } from '../../core/search/search-parser';
imports: [CommonModule, FormsModule, SearchBarComponent, SearchResultsComponent], imports: [CommonModule, FormsModule, SearchBarComponent, SearchResultsComponent],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
template: ` template: `
<div class="flex flex-col h-full bg-bg-primary dark:bg-gray-900"> <div class="flex flex-col h-full bg-bg-primary dark:bg-main">
<!-- Search bar --> <!-- Search bar -->
<div class="p-4 border-b border-border dark:border-gray-700"> <div class="p-4 border-b border-border dark:border-border">
<app-search-bar <app-search-bar
[placeholder]="placeholder" [placeholder]="placeholder"
[context]="context" [context]="context"
@ -58,10 +58,10 @@ import { parseSearchQuery } from '../../core/search/search-parser';
<!-- Search options toggles --> <!-- Search options toggles -->
@if (hasSearched() && results().length > 0) { @if (hasSearched() && results().length > 0) {
<div class="flex flex-col gap-3 p-4 border-b border-border dark:border-gray-700 bg-bg-muted dark:bg-gray-800"> <div class="flex flex-col gap-3 p-4 border-b border-border dark:border-border bg-bg-muted dark:bg-card">
<!-- Collapse results toggle --> <!-- Collapse results toggle -->
<label class="flex items-center justify-between cursor-pointer group"> <label class="flex items-center justify-between cursor-pointer group">
<span class="text-sm text-text-main dark:text-gray-200">Collapse results</span> <span class="text-sm text-text-main dark:text-main">Collapse results</span>
<div class="relative"> <div class="relative">
<input <input
type="checkbox" type="checkbox"
@ -69,14 +69,14 @@ import { parseSearchQuery } from '../../core/search/search-parser';
(change)="onToggleCollapse()" (change)="onToggleCollapse()"
class="sr-only peer" class="sr-only peer"
/> />
<div class="w-11 h-6 bg-gray-300 dark:bg-gray-600 rounded-full peer peer-checked:bg-blue-500 dark:peer-checked:bg-blue-600 transition-colors"></div> <div class="w-11 h-6 bg-muted dark:bg-gray-600 rounded-full peer peer-checked:bg-blue-500 dark:peer-checked:bg-blue-600 transition-colors"></div>
<div class="absolute left-1 top-1 w-4 h-4 bg-white rounded-full transition-transform peer-checked:translate-x-5"></div> <div class="absolute left-1 top-1 w-4 h-4 bg-card rounded-full transition-transform peer-checked:translate-x-5"></div>
</div> </div>
</label> </label>
<!-- Show more context toggle --> <!-- Show more context toggle -->
<label class="flex items-center justify-between cursor-pointer group"> <label class="flex items-center justify-between cursor-pointer group">
<span class="text-sm text-text-main dark:text-gray-200">Show more context</span> <span class="text-sm text-text-main dark:text-main">Show more context</span>
<div class="relative"> <div class="relative">
<input <input
type="checkbox" type="checkbox"
@ -84,14 +84,14 @@ import { parseSearchQuery } from '../../core/search/search-parser';
(change)="onToggleContext()" (change)="onToggleContext()"
class="sr-only peer" class="sr-only peer"
/> />
<div class="w-11 h-6 bg-gray-300 dark:bg-gray-600 rounded-full peer peer-checked:bg-blue-500 dark:peer-checked:bg-blue-600 transition-colors"></div> <div class="w-11 h-6 bg-muted dark:bg-gray-600 rounded-full peer peer-checked:bg-blue-500 dark:peer-checked:bg-blue-600 transition-colors"></div>
<div class="absolute left-1 top-1 w-4 h-4 bg-white rounded-full transition-transform peer-checked:translate-x-5"></div> <div class="absolute left-1 top-1 w-4 h-4 bg-card rounded-full transition-transform peer-checked:translate-x-5"></div>
</div> </div>
</label> </label>
<!-- Explain search terms toggle --> <!-- Explain search terms toggle -->
<label class="flex items-center justify-between cursor-pointer group"> <label class="flex items-center justify-between cursor-pointer group">
<span class="text-sm text-text-main dark:text-gray-200">Explain search terms</span> <span class="text-sm text-text-main dark:text-main">Explain search terms</span>
<div class="relative"> <div class="relative">
<input <input
type="checkbox" type="checkbox"
@ -99,19 +99,19 @@ import { parseSearchQuery } from '../../core/search/search-parser';
(change)="onToggleExplain()" (change)="onToggleExplain()"
class="sr-only peer" class="sr-only peer"
/> />
<div class="w-11 h-6 bg-gray-300 dark:bg-gray-600 rounded-full peer peer-checked:bg-blue-500 dark:peer-checked:bg-blue-600 transition-colors"></div> <div class="w-11 h-6 bg-muted dark:bg-gray-600 rounded-full peer peer-checked:bg-blue-500 dark:peer-checked:bg-blue-600 transition-colors"></div>
<div class="absolute left-1 top-1 w-4 h-4 bg-white rounded-full transition-transform peer-checked:translate-x-5"></div> <div class="absolute left-1 top-1 w-4 h-4 bg-card rounded-full transition-transform peer-checked:translate-x-5"></div>
</div> </div>
</label> </label>
</div> </div>
} }
<!-- Explain search terms panel --> <!-- Explain search terms panel -->
@if (explainSearchTerms && currentQuery().trim()) { @if (explainSearchTerms && currentQuery().trim()) {
<div class="p-4 border-b border-border dark:border-gray-700 bg-bg-primary/60 dark:bg-gray-900/60"> <div class="p-4 border-b border-border dark:border-border bg-bg-primary/60 dark:bg-main/60">
<h4 class="text-xs font-semibold uppercase tracking-wide text-text-muted mb-2">Match all of:</h4> <h4 class="text-xs font-semibold uppercase tracking-wide text-text-muted mb-2">Match all of:</h4>
<ul class="ml-1 space-y-1 text-xs"> <ul class="ml-1 space-y-1 text-xs">
@for (line of explanationLines(); track $index) { @for (line of explanationLines(); track $index) {
<li class="text-text-muted dark:text-gray-400">{{ line }}</li> <li class="text-text-muted dark:text-muted">{{ line }}</li>
} }
</ul> </ul>
</div> </div>
@ -123,11 +123,11 @@ import { parseSearchQuery } from '../../core/search/search-parser';
<div class="flex items-center justify-center h-full"> <div class="flex items-center justify-center h-full">
<div class="flex flex-col items-center gap-3"> <div class="flex flex-col items-center gap-3">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-accent dark:border-blue-400"></div> <div class="animate-spin rounded-full h-8 w-8 border-b-2 border-accent dark:border-blue-400"></div>
<p class="text-sm text-text-muted dark:text-gray-400">Searching...</p> <p class="text-sm text-text-muted dark:text-muted">Searching...</p>
</div> </div>
</div> </div>
} @else if (hasSearched() && results().length === 0) { } @else if (hasSearched() && results().length === 0) {
<div class="flex flex-col items-center justify-center h-full text-text-muted dark:text-gray-400 p-8"> <div class="flex flex-col items-center justify-center h-full text-text-muted dark:text-muted p-8">
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 mb-4 opacity-50" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 mb-4 opacity-50" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg> </svg>
@ -144,7 +144,7 @@ import { parseSearchQuery } from '../../core/search/search-parser';
(noteOpen)="onNoteOpen($event)" (noteOpen)="onNoteOpen($event)"
/> />
} @else { } @else {
<div class="flex flex-col items-center justify-center h-full text-text-muted dark:text-gray-400 p-8"> <div class="flex flex-col items-center justify-center h-full text-text-muted dark:text-muted p-8">
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 mb-4 opacity-50" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 mb-4 opacity-50" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg> </svg>

View File

@ -35,17 +35,17 @@ type NavigationItem =
@if (isOpen()) { @if (isOpen()) {
<div <div
#popover #popover
class="absolute top-full left-0 right-0 mt-1 bg-bg-primary dark:bg-gray-800 border border-border dark:border-gray-700 rounded-xl shadow-2xl z-50 overflow-hidden max-h-[360px]" class="absolute top-full left-0 right-0 mt-1 bg-bg-primary dark:bg-card border border-border dark:border-border rounded-xl shadow-2xl z-50 overflow-hidden max-h-[360px]"
style="z-index: 9999;" style="z-index: 9999;"
(click)="$event.stopPropagation()" (click)="$event.stopPropagation()"
> >
<!-- Search Options --> <!-- Search Options -->
<div class="p-3 border-b border-border dark:border-gray-700"> <div class="p-3 border-b border-border dark:border-border">
<div class="flex items-center justify-between mb-2"> <div class="flex items-center justify-between mb-2">
<h4 class="text-xs font-semibold uppercase tracking-wide text-text-main dark:text-gray-100">Search options</h4> <h4 class="text-xs font-semibold uppercase tracking-wide text-text-main dark:text-gray-100">Search options</h4>
<button <button
(click)="showHelp = !showHelp" (click)="showHelp = !showHelp"
class="p-1 rounded-full hover:bg-bg-muted dark:hover:bg-gray-700 transition-colors" class="p-1 rounded-full hover:bg-bg-muted dark:hover:bg-surface2 transition-colors"
title="Show help" title="Show help"
> >
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5 text-text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5 text-text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor">
@ -55,15 +55,15 @@ type NavigationItem =
</div> </div>
@if (showHelp) { @if (showHelp) {
<div class="mb-2 p-2 bg-bg-muted dark:bg-gray-900 rounded-lg text-[11px] space-y-1.5"> <div class="mb-2 p-2 bg-bg-muted dark:bg-main rounded-lg text-[11px] space-y-1.5">
<div><strong>Operators:</strong></div> <div><strong>Operators:</strong></div>
<div class="grid grid-cols-2 gap-1.5 text-text-muted dark:text-gray-400"> <div class="grid grid-cols-2 gap-1.5 text-text-muted dark:text-muted">
<div><code class="px-1 py-0.5 bg-bg-primary dark:bg-gray-800 rounded">OR</code> Either term</div> <div><code class="px-1 py-0.5 bg-bg-primary dark:bg-card rounded">OR</code> Either term</div>
<div><code class="px-1 py-0.5 bg-bg-primary dark:bg-gray-800 rounded">-term</code> Exclude</div> <div><code class="px-1 py-0.5 bg-bg-primary dark:bg-card rounded">-term</code> Exclude</div>
<div><code class="px-1 py-0.5 bg-bg-primary dark:bg-gray-800 rounded">"phrase"</code> Exact match</div> <div><code class="px-1 py-0.5 bg-bg-primary dark:bg-card rounded">"phrase"</code> Exact match</div>
<div><code class="px-1 py-0.5 bg-bg-primary dark:bg-gray-800 rounded">term*</code> Wildcard</div> <div><code class="px-1 py-0.5 bg-bg-primary dark:bg-card rounded">term*</code> Wildcard</div>
</div> </div>
<div><strong>Combine:</strong> <code class="px-1 py-0.5 bg-bg-primary dark:bg-gray-800 rounded">tag:#todo OR tag:#urgent</code></div> <div><strong>Combine:</strong> <code class="px-1 py-0.5 bg-bg-primary dark:bg-card rounded">tag:#todo OR tag:#urgent</code></div>
</div> </div>
} }
@ -71,20 +71,20 @@ type NavigationItem =
<button <button
*ngFor="let option of searchOptions(); let i = index" *ngFor="let option of searchOptions(); let i = index"
(click)="insertOption(option.prefix)" (click)="insertOption(option.prefix)"
class="w-full text-left px-2.5 py-1.5 rounded-lg hover:bg-bg-muted dark:hover:bg-gray-700 transition-colors group text-sm" class="w-full text-left px-2.5 py-1.5 rounded-lg hover:bg-bg-muted dark:hover:bg-surface2 transition-colors group text-sm"
[ngClass]="{ [ngClass]="{
'bg-bg-muted': isSelected('option', i), 'bg-bg-muted': isSelected('option', i),
'dark:bg-gray-700': isSelected('option', i) 'dark:bg-surface2': isSelected('option', i)
}" }"
> >
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<code class="text-xs font-mono text-accent dark:text-blue-400 whitespace-nowrap">{{ option.prefix }}</code> <code class="text-xs font-mono text-accent dark:text-blue-400 whitespace-nowrap">{{ option.prefix }}</code>
<div class="flex-1 min-w-0 text-[13px] text-text-muted dark:text-gray-400 group-hover:text-text-main dark:group-hover:text-gray-100"> <div class="flex-1 min-w-0 text-[13px] text-text-muted dark:text-muted group-hover:text-text-main dark:group-hover:text-gray-100">
{{ option.description }} {{ option.description }}
</div> </div>
<span <span
*ngIf="showExamples && option.example" *ngIf="showExamples && option.example"
class="text-[11px] text-text-muted dark:text-gray-500 font-mono ml-2 truncate" class="text-[11px] text-text-muted dark:text-muted font-mono ml-2 truncate"
> >
{{ option.example }} {{ option.example }}
</span> </span>
@ -95,12 +95,12 @@ type NavigationItem =
<!-- History --> <!-- History -->
@if (history().length > 0) { @if (history().length > 0) {
<div class="p-3 border-b border-border dark:border-gray-700"> <div class="p-3 border-b border-border dark:border-border">
<div class="flex items-center justify-between mb-2"> <div class="flex items-center justify-between mb-2">
<h4 class="text-xs font-semibold uppercase tracking-wide text-text-main dark:text-gray-100">History</h4> <h4 class="text-xs font-semibold uppercase tracking-wide text-text-main dark:text-gray-100">History</h4>
<button <button
(click)="clearHistory()" (click)="clearHistory()"
class="text-[11px] text-text-muted dark:text-gray-400 hover:text-accent dark:hover:text-blue-400 transition-colors" class="text-[11px] text-text-muted dark:text-muted hover:text-accent dark:hover:text-blue-400 transition-colors"
> >
Clear Clear
</button> </button>
@ -109,13 +109,13 @@ type NavigationItem =
<button <button
*ngFor="let item of history(); let i = index" *ngFor="let item of history(); let i = index"
(click)="selectHistoryItem(item)" (click)="selectHistoryItem(item)"
class="w-full text-left px-2.5 py-1.5 rounded-lg hover:bg-bg-muted dark:hover:bg-gray-700 transition-colors group text-sm" class="w-full text-left px-2.5 py-1.5 rounded-lg hover:bg-bg-muted dark:hover:bg-surface2 transition-colors group text-sm"
[ngClass]="{ [ngClass]="{
'bg-bg-muted': isSelected('history', i), 'bg-bg-muted': isSelected('history', i),
'dark:bg-gray-700': isSelected('history', i) 'dark:bg-surface2': isSelected('history', i)
}" }"
> >
<span class="block truncate text-[13px] text-text-muted dark:text-gray-300 group-hover:text-text-main dark:group-hover:text-gray-100 font-mono"> <span class="block truncate text-[13px] text-text-muted dark:text-main group-hover:text-text-main dark:group-hover:text-gray-100 font-mono">
{{ item }} {{ item }}
</span> </span>
</button> </button>
@ -133,13 +133,13 @@ type NavigationItem =
<button <button
*ngFor="let suggestion of suggestions(); let i = index" *ngFor="let suggestion of suggestions(); let i = index"
(click)="insertSuggestion(suggestion)" (click)="insertSuggestion(suggestion)"
class="w-full text-left px-2.5 py-1.5 rounded-lg hover:bg-bg-muted dark:hover:bg-gray-700 transition-colors text-sm" class="w-full text-left px-2.5 py-1.5 rounded-lg hover:bg-bg-muted dark:hover:bg-surface2 transition-colors text-sm"
[ngClass]="{ [ngClass]="{
'bg-bg-muted': isSelected('suggestion', i), 'bg-bg-muted': isSelected('suggestion', i),
'dark:bg-gray-700': isSelected('suggestion', i) 'dark:bg-surface2': isSelected('suggestion', i)
}" }"
> >
<span class="block truncate text-[13px] text-text-main dark:text-gray-200">{{ suggestion }}</span> <span class="block truncate text-[13px] text-text-main dark:text-main">{{ suggestion }}</span>
</button> </button>
</div> </div>
</div> </div>

View File

@ -45,14 +45,14 @@ type SortOption = 'relevance' | 'name' | 'modified';
imports: [CommonModule, FormsModule], imports: [CommonModule, FormsModule],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
template: ` template: `
<div class="flex flex-col h-full bg-bg-primary dark:bg-gray-900"> <div class="flex flex-col h-full bg-bg-primary dark:bg-main">
<!-- Results header --> <!-- Results header -->
<div class="flex items-center justify-between p-4 border-b border-border dark:border-gray-700"> <div class="flex items-center justify-between p-4 border-b border-border dark:border-border">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<h3 class="text-sm font-semibold text-text-main dark:text-gray-100"> <h3 class="text-sm font-semibold text-text-main dark:text-gray-100">
{{ totalResults() }} {{ totalResults() === 1 ? 'result' : 'results' }} {{ totalResults() }} {{ totalResults() === 1 ? 'result' : 'results' }}
@if (totalMatches() > 0) { @if (totalMatches() > 0) {
<span class="text-text-muted dark:text-gray-400"> <span class="text-text-muted dark:text-muted">
({{ totalMatches() }} {{ totalMatches() === 1 ? 'match' : 'matches' }}) ({{ totalMatches() }} {{ totalMatches() === 1 ? 'match' : 'matches' }})
</span> </span>
} }
@ -61,11 +61,11 @@ type SortOption = 'relevance' | 'name' | 'modified';
<!-- Sort options --> <!-- Sort options -->
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<label class="text-xs text-text-muted dark:text-gray-400">Sort:</label> <label class="text-xs text-text-muted dark:text-muted">Sort:</label>
<select <select
[(ngModel)]="sortBy" [(ngModel)]="sortBy"
(change)="onSortChange()" (change)="onSortChange()"
class="text-xs px-2 py-1 rounded border border-border dark:border-gray-600 bg-bg-primary dark:bg-gray-800 text-text-main dark:text-gray-100" class="text-xs px-2 py-1 rounded border border-border dark:border-border bg-bg-primary dark:bg-card text-text-main dark:text-gray-100"
> >
<option value="relevance">Relevance</option> <option value="relevance">Relevance</option>
<option value="name">Name</option> <option value="name">Name</option>
@ -77,7 +77,7 @@ type SortOption = 'relevance' | 'name' | 'modified';
<!-- Results list --> <!-- Results list -->
<div class="flex-1 overflow-y-auto"> <div class="flex-1 overflow-y-auto">
@if (sortedGroups().length === 0) { @if (sortedGroups().length === 0) {
<div class="flex flex-col items-center justify-center h-full text-text-muted dark:text-gray-400 p-8"> <div class="flex flex-col items-center justify-center h-full text-text-muted dark:text-muted p-8">
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 mb-4 opacity-50" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 mb-4 opacity-50" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg> </svg>
@ -86,7 +86,7 @@ type SortOption = 'relevance' | 'name' | 'modified';
} @else { } @else {
<div class="divide-y divide-border dark:divide-gray-700"> <div class="divide-y divide-border dark:divide-gray-700">
@for (group of sortedGroups(); track group.noteId) { @for (group of sortedGroups(); track group.noteId) {
<div class="hover:bg-bg-muted dark:hover:bg-gray-800 transition-colors"> <div class="hover:bg-bg-muted dark:hover:bg-card transition-colors">
<!-- File header --> <!-- File header -->
<div <div
class="flex items-center justify-between p-3 cursor-pointer" class="flex items-center justify-between p-3 cursor-pointer"
@ -96,7 +96,7 @@ type SortOption = 'relevance' | 'name' | 'modified';
<!-- Expand/collapse icon --> <!-- Expand/collapse icon -->
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4 text-text-muted dark:text-gray-400 transition-transform" class="h-4 w-4 text-text-muted dark:text-muted transition-transform"
[class.rotate-90]="group.isExpanded" [class.rotate-90]="group.isExpanded"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@ -115,7 +115,7 @@ type SortOption = 'relevance' | 'name' | 'modified';
<div class="text-sm font-medium text-text-main dark:text-gray-100 truncate"> <div class="text-sm font-medium text-text-main dark:text-gray-100 truncate">
{{ group.fileName }} {{ group.fileName }}
</div> </div>
<div class="text-xs text-text-muted dark:text-gray-400 truncate"> <div class="text-xs text-text-muted dark:text-muted truncate">
{{ group.filePath }} {{ group.filePath }}
</div> </div>
</div> </div>
@ -143,23 +143,23 @@ type SortOption = 'relevance' | 'name' | 'modified';
<div class="px-3 pb-3 pl-11 space-y-2"> <div class="px-3 pb-3 pl-11 space-y-2">
@for (match of group.matches; track $index) { @for (match of group.matches; track $index) {
<div <div
class="p-2 rounded bg-bg-muted dark:bg-gray-800 hover:bg-bg-secondary dark:hover:bg-gray-700 cursor-pointer transition-colors" class="p-2 rounded bg-bg-muted dark:bg-card hover:bg-bg-secondary dark:hover:bg-surface2 cursor-pointer transition-colors"
(click)="openNote(group.noteId, $event, match.line)" (click)="openNote(group.noteId, $event, match.line)"
> >
<!-- Match type badge --> <!-- Match type badge -->
<div class="flex items-center gap-2 mb-1"> <div class="flex items-center gap-2 mb-1">
<span class="text-xs px-1.5 py-0.5 rounded bg-bg-primary dark:bg-gray-900 text-text-muted dark:text-gray-400 font-mono"> <span class="text-xs px-1.5 py-0.5 rounded bg-bg-primary dark:bg-main text-text-muted dark:text-muted font-mono">
{{ match.type }} {{ match.type }}
</span> </span>
@if (match.line) { @if (match.line) {
<span class="text-xs text-text-muted dark:text-gray-500"> <span class="text-xs text-text-muted dark:text-muted">
Line {{ match.line }} Line {{ match.line }}
</span> </span>
} }
</div> </div>
<!-- Match context with highlighting --> <!-- Match context with highlighting -->
<div class="text-sm text-text-main dark:text-gray-200 font-mono leading-relaxed whitespace-pre-wrap"> <div class="text-sm text-text-main dark:text-main font-mono leading-relaxed whitespace-pre-wrap">
<span [innerHTML]="highlightMatch(match)"></span> <span [innerHTML]="highlightMatch(match)"></span>
</div> </div>
</div> </div>

View File

@ -113,7 +113,7 @@ interface MetadataEntry {
[attr.title]="tocOpen() ? 'Cacher sommaire' : 'Afficher sommaire'" [attr.title]="tocOpen() ? 'Cacher sommaire' : 'Afficher sommaire'"
[attr.aria-label]="tocOpen() ? 'Cacher sommaire' : 'Afficher sommaire'" [attr.aria-label]="tocOpen() ? 'Cacher sommaire' : 'Afficher sommaire'"
> >
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" [ngClass]="tocOpen() ? 'text-accent' : 'text-text-muted'"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/><path d="M13 8h5"/><path d="M13 12h5"/><path d="M13 16h5"/></svg> <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" [ngClass]="tocOpen() ? 'toc-toggle--active' : 'toc-toggle--idle'"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/><path d="M13 8h5"/><path d="M13 12h5"/><path d="M13 16h5"/></svg>
</button> </button>
<button <button
@ -139,6 +139,13 @@ interface MetadataEntry {
</button> </button>
@if (menuOpen()) { @if (menuOpen()) {
<div class="absolute right-0 mt-2 w-56 rounded-md border border-border bg-card shadow-subtle not-prose z-10"> <div class="absolute right-0 mt-2 w-56 rounded-md border border-border bg-card shadow-subtle not-prose z-10">
<button type="button" class="block w-full text-left px-3 py-2 hover:bg-muted" (click)="parametersRequested.emit(); closeMenu()">
<svg xmlns="http://www.w3.org/2000/svg" class="inline h-4 w-4 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="3"/>
<path d="M12 1v6m0 6v6m-6-6h6m6 0h-6M4.93 4.93l4.24 4.24m5.66 5.66l4.24 4.24m-4.24-14.14l4.24-4.24M4.93 19.07l4.24-4.24"/>
</svg>
Parameters
</button>
<button type="button" class="block w-full text-left px-3 py-2 hover:bg-muted" (click)="legacyRequested.emit(); closeMenu()">🔧 Legacy</button> <button type="button" class="block w-full text-left px-3 py-2 hover:bg-muted" (click)="legacyRequested.emit(); closeMenu()">🔧 Legacy</button>
</div> </div>
} }
@ -224,7 +231,7 @@ interface MetadataEntry {
{{ getAuthorFromFrontmatter() ?? note().author ?? 'Auteur inconnu' }} {{ getAuthorFromFrontmatter() ?? note().author ?? 'Auteur inconnu' }}
</span> </span>
@if (hasState('favoris')) { @if (hasState('favoris')) {
<span class="inline-flex items-center gap-1 transition-colors" [ngClass]="state('favoris') ? 'text-rose-500' : 'text-gray-400'" title="{{ state('favoris') ? 'Ajouté aux favoris' : 'Non favori' }}" role="img" aria-label="{{ state('favoris') ? 'Ajouté aux favoris' : 'Non favori' }}"> <span class="inline-flex items-center gap-1 transition-colors" [ngClass]="state('favoris') ? 'text-rose-500' : 'text-muted'" title="{{ state('favoris') ? 'Ajouté aux favoris' : 'Non favori' }}" role="img" aria-label="{{ state('favoris') ? 'Ajouté aux favoris' : 'Non favori' }}">
@if (state('favoris')) { @if (state('favoris')) {
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="currentColor" stroke="none"> <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="currentColor" stroke="none">
<path d="M19 14.5c1.5-1.5 2-4 0-6s-5-2-7 1c-2-3-5-3.5-7-1s-1.5 4.5 0 6l7 6z" /> <path d="M19 14.5c1.5-1.5 2-4 0-6s-5-2-7 1c-2-3-5-3.5-7-1s-1.5 4.5 0 6l7 6z" />
@ -237,7 +244,7 @@ interface MetadataEntry {
</span> </span>
} }
@if (hasState('publish')) { @if (hasState('publish')) {
<span class="inline-flex items-center gap-1 transition-colors" [ngClass]="state('publish') ? 'text-green-500' : 'text-gray-400'" title="{{ state('publish') ? 'Publié sur le web' : 'Non publié' }}" role="img" aria-label="{{ state('publish') ? 'Publié sur le web' : 'Non publié' }}"> <span class="inline-flex items-center gap-1 transition-colors" [ngClass]="state('publish') ? 'text-green-500' : 'text-muted'" title="{{ state('publish') ? 'Publié sur le web' : 'Non publié' }}" role="img" aria-label="{{ state('publish') ? 'Publié sur le web' : 'Non publié' }}">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10" /> <circle cx="12" cy="12" r="10" />
<path d="M2 12h20" /> <path d="M2 12h20" />
@ -246,7 +253,7 @@ interface MetadataEntry {
</span> </span>
} }
@if (hasState('draft')) { @if (hasState('draft')) {
<span class="inline-flex items-center gap-1 transition-colors" [ngClass]="state('draft') ? 'text-yellow-500' : 'text-gray-400'" title="{{ state('draft') ? 'Brouillon actif' : 'Pas un brouillon' }}" role="img" aria-label="{{ state('draft') ? 'Brouillon actif' : 'Pas un brouillon' }}"> <span class="inline-flex items-center gap-1 transition-colors" [ngClass]="state('draft') ? 'text-yellow-500' : 'text-muted'" title="{{ state('draft') ? 'Brouillon actif' : 'Pas un brouillon' }}" role="img" aria-label="{{ state('draft') ? 'Brouillon actif' : 'Pas un brouillon' }}">
@if (state('draft')) { @if (state('draft')) {
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 3h18v4H3z" /> <path d="M3 3h18v4H3z" />
@ -261,7 +268,7 @@ interface MetadataEntry {
</span> </span>
} }
@if (hasState('template')) { @if (hasState('template')) {
<span class="inline-flex items-center gap-1 transition-colors" [ngClass]="state('template') ? 'text-amber-500' : 'text-gray-400'" title="{{ state('template') ? 'Modèle' : 'Non modèle' }}" role="img" aria-label="{{ state('template') ? 'Modèle' : 'Non modèle' }}"> <span class="inline-flex items-center gap-1 transition-colors" [ngClass]="state('template') ? 'text-amber-500' : 'text-muted'" title="{{ state('template') ? 'Modèle' : 'Non modèle' }}" role="img" aria-label="{{ state('template') ? 'Modèle' : 'Non modèle' }}">
@if (state('template')) { @if (state('template')) {
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="4" width="18" height="14" rx="2" /> <rect x="3" y="4" width="18" height="14" rx="2" />
@ -276,7 +283,7 @@ interface MetadataEntry {
</span> </span>
} }
@if (hasState('task')) { @if (hasState('task')) {
<span class="inline-flex items-center gap-1 transition-colors" [ngClass]="state('task') ? 'text-indigo-500' : 'text-gray-400'" title="{{ state('task') ? 'Tâche' : 'Pas une tâche' }}" role="img" aria-label="{{ state('task') ? 'Tâche' : 'Pas une tâche' }}"> <span class="inline-flex items-center gap-1 transition-colors" [ngClass]="state('task') ? 'text-indigo-500' : 'text-muted'" title="{{ state('task') ? 'Tâche' : 'Pas une tâche' }}" role="img" aria-label="{{ state('task') ? 'Tâche' : 'Pas une tâche' }}">
@if (state('task')) { @if (state('task')) {
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="4" width="18" height="16" rx="2" /> <rect x="3" y="4" width="18" height="16" rx="2" />
@ -290,7 +297,7 @@ interface MetadataEntry {
</span> </span>
} }
@if (hasState('private')) { @if (hasState('private')) {
<span class="inline-flex items-center gap-1 transition-colors" [ngClass]="state('private') ? 'text-purple-500' : 'text-gray-400'" title="{{ state('private') ? 'Privé' : 'Public' }}" role="img" aria-label="{{ state('private') ? 'Privé' : 'Public' }}"> <span class="inline-flex items-center gap-1 transition-colors" [ngClass]="state('private') ? 'text-purple-500' : 'text-muted'" title="{{ state('private') ? 'Privé' : 'Public' }}" role="img" aria-label="{{ state('private') ? 'Privé' : 'Public' }}">
@if (state('private')) { @if (state('private')) {
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="11" width="18" height="11" rx="2" /> <rect x="3" y="11" width="18" height="11" rx="2" />
@ -305,7 +312,7 @@ interface MetadataEntry {
</span> </span>
} }
@if (hasState('archive')) { @if (hasState('archive')) {
<span class="inline-flex items-center gap-1 transition-colors" [ngClass]="state('archive') ? 'text-amber-600' : 'text-gray-400'" title="{{ state('archive') ? 'Document archivé' : 'Document non archivé' }}" role="img" aria-label="{{ state('archive') ? 'Document archivé' : 'Document non archivé' }}"> <span class="inline-flex items-center gap-1 transition-colors" [ngClass]="state('archive') ? 'text-amber-600' : 'text-muted'" title="{{ state('archive') ? 'Document archivé' : 'Document non archivé' }}" role="img" aria-label="{{ state('archive') ? 'Document archivé' : 'Document non archivé' }}">
@if (state('archive')) { @if (state('archive')) {
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="22,12 18,12 18,8"/> <polyline points="22,12 18,12 18,8"/>
@ -358,6 +365,7 @@ export class NoteViewerComponent implements OnDestroy {
addTagRequested = output<void>(); addTagRequested = output<void>();
fullScreenRequested = output<void>(); fullScreenRequested = output<void>();
legacyRequested = output<void>(); legacyRequested = output<void>();
parametersRequested = output<void>();
fullScreenActive = input<boolean>(false); fullScreenActive = input<boolean>(false);
tocOpen = input<boolean>(false); tocOpen = input<boolean>(false);

View File

@ -12,7 +12,9 @@ export type LogEvent =
| 'GRAPH_VIEW_CLOSE' | 'GRAPH_VIEW_CLOSE'
| 'GRAPH_VIEW_SETTINGS_CHANGE' | 'GRAPH_VIEW_SETTINGS_CHANGE'
| 'CALENDAR_SEARCH_EXECUTED' | 'CALENDAR_SEARCH_EXECUTED'
| 'THEME_CHANGE'; | 'THEME_CHANGE'
| 'THEME_MODE_CHANGE'
| 'LANGUAGE_CHANGE';
export interface LogContext { export interface LogContext {
route?: string; route?: string;

View File

@ -32,7 +32,7 @@ export class SearchHighlighterService {
// Add highlighted match // Add highlighted match
const matchText = text.substring(range.start, range.end); const matchText = text.substring(range.start, range.end);
result += `<mark class="bg-yellow-200 dark:bg-yellow-600 text-gray-900 dark:text-gray-900 px-0.5 rounded font-medium">${this.escapeHtml(matchText)}</mark>`; result += `<mark class="bg-yellow-200 dark:bg-yellow-600 text-main dark:text-main px-0.5 rounded font-medium">${this.escapeHtml(matchText)}</mark>`;
lastIndex = range.end; lastIndex = range.end;
} }

View File

@ -250,29 +250,19 @@ export class MarkdownService {
const list = tokens as unknown as MarkdownItToken[]; const list = tokens as unknown as MarkdownItToken[];
const token = list[idx]; const token = list[idx];
const level = Number.parseInt(token.tag.replace('h', ''), 10); const level = Number.parseInt(token.tag.replace('h', ''), 10);
const headingClass = `md-heading md-heading-${level} text-text-main font-bold mt-6 mb-3 pb-1 border-b border-border`; token.attrJoin('class', `md-heading md-heading-${level}`);
token.attrJoin('class', headingClass);
return self.renderToken(tokens, idx, options); return self.renderToken(tokens, idx, options);
}; };
md.renderer.rules.blockquote_open = (tokens, idx, options, renderEnv, self) => { md.renderer.rules.blockquote_open = (tokens, idx, options, renderEnv, self) => {
const list = tokens as unknown as MarkdownItToken[];
const token = list[idx];
token.attrJoin('class', 'border-l-4 border-gray-300 dark:border-gray-500 pl-4 py-2 my-4 italic text-gray-700 dark:text-gray-300');
return self.renderToken(tokens, idx, options); return self.renderToken(tokens, idx, options);
}; };
md.renderer.rules.bullet_list_open = (tokens, idx, options, renderEnv, self) => { md.renderer.rules.bullet_list_open = (tokens, idx, options, renderEnv, self) => {
const list = tokens as unknown as MarkdownItToken[];
const token = list[idx];
token.attrJoin('class', 'mb-4 list-disc ml-6');
return self.renderToken(tokens, idx, options); return self.renderToken(tokens, idx, options);
}; };
md.renderer.rules.ordered_list_open = (tokens, idx, options, renderEnv, self) => { md.renderer.rules.ordered_list_open = (tokens, idx, options, renderEnv, self) => {
const list = tokens as unknown as MarkdownItToken[];
const token = list[idx];
token.attrJoin('class', 'mb-4 list-decimal ml-6');
return self.renderToken(tokens, idx, options); return self.renderToken(tokens, idx, options);
}; };
@ -314,12 +304,11 @@ export class MarkdownService {
</figure>`; </figure>`;
}; };
md.renderer.rules.footnote_block_open = () => '<section class="md-footnotes text-sm text-text-muted mt-12"><header class="font-semibold uppercase tracking-wide mb-2">Notes</header><ol class="space-y-2">'; md.renderer.rules.footnote_block_open = () => '<section class="footnotes"><ol>';
md.renderer.rules.footnote_block_close = () => '</ol></section>'; md.renderer.rules.footnote_block_close = () => '</ol></section>';
const defaultFootnoteOpen = md.renderer.rules.footnote_open ?? ((tokens, idx, options, renderEnv, self) => self.renderToken(tokens, idx, options)); const defaultFootnoteOpen = md.renderer.rules.footnote_open ?? ((tokens, idx, options, renderEnv, self) => self.renderToken(tokens, idx, options));
md.renderer.rules.footnote_open = (tokens, idx, options, renderEnv, self) => { md.renderer.rules.footnote_open = (tokens, idx, options, renderEnv, self) => {
tokens[idx].attrJoin('class', 'md-footnote-item');
return defaultFootnoteOpen(tokens, idx, options, renderEnv, self); return defaultFootnoteOpen(tokens, idx, options, renderEnv, self);
}; };
@ -327,10 +316,10 @@ export class MarkdownService {
const list = tokens as unknown as MarkdownItToken[]; const list = tokens as unknown as MarkdownItToken[];
const token = list[idx]; const token = list[idx];
const id = token.attrGet('id') ?? ''; const id = token.attrGet('id') ?? '';
return `<sup class="md-footnote-ref"><a href="#${this.escapeHtml(id)}" class="md-external-link" rel="footnote">${token.content}</a></sup>`; return `<sup class="footnote-ref"><a href="#${this.escapeHtml(id)}" rel="footnote">${token.content}</a></sup>`;
}; };
md.renderer.rules.hr = () => '<hr class="my-6 border-border">'; md.renderer.rules.hr = () => '<hr>';
return md; return md;
} }
@ -359,10 +348,27 @@ export class MarkdownService {
bodyHtml = `<pre class="code-block__body"><code class="hljs" data-raw-code="${encodedRawCode}">${highlightedCode}</code></pre>`; bodyHtml = `<pre class="code-block__body"><code class="hljs" data-raw-code="${encodedRawCode}">${highlightedCode}</code></pre>`;
} }
// Determine kind for icon and header accent
const isShell = ['bash','sh','shell','zsh','powershell','ps','ps1','cmd'].includes(normalizedLanguage);
const kind = isMermaid ? 'mermaid' : (isShell ? 'shell' : 'code');
// Inline SVG icons
const iconSvg = kind === 'mermaid' ? `
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M3 7h7l2 4h9"/><circle cx="7" cy="17" r="2"/><circle cx="17" cy="17" r="2"/>
</svg>` : (kind === 'shell' ? `
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M4 7l6 5-6 5"/><path d="M13 17h7"/>
</svg>` : `
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M8 17l-5-5 5-5"/><path d="M16 7l5 5-5 5"/><path d="M14 4l-4 16"/>
</svg>`);
const headerHtml = ` const headerHtml = `
<div class="code-block__header"> <div class="code-block__header">
<div class="code-block__controls" aria-hidden="true"> <div class="code-block__kind">
<span></span><span></span><span></span> <span class="code-block__kind-icon" aria-hidden="true">${iconSvg}</span>
<span class="code-block__kind-label">${kind}</span>
</div> </div>
<div class="code-block__actions"> <div class="code-block__actions">
<button type="button" class="code-block__language-badge" data-language="${languageLabel.toLowerCase()}" data-code-id="${codeId}">${safeLanguageDisplay}</button> <button type="button" class="code-block__language-badge" data-language="${languageLabel.toLowerCase()}" data-code-id="${codeId}">${safeLanguageDisplay}</button>
@ -371,8 +377,8 @@ export class MarkdownService {
</div> </div>
`; `;
const wrapperClass = isMermaid ? 'code-block code-block--mermaid text-left' : 'code-block text-left'; const wrapperClass = `code-block ${isMermaid ? 'code-block--mermaid' : ''} code-block--kind-${kind} text-left`;
return `<div class="${wrapperClass}" style="max-width: 800px; width: 100%; margin: 0;" data-language="${languageLabel.toLowerCase()}" data-code-id="${codeId}">${headerHtml}${bodyHtml}</div>`; return `<div class="${wrapperClass}" style="max-width: 800px; width: 100%; margin: 0;" data-language="${languageLabel.toLowerCase()}" data-kind="${kind}" data-code-id="${codeId}">${headerHtml}${bodyHtml}</div>`;
} }
private highlightCode(code: string, normalizedLanguage: string, rawLanguage: string): string { private highlightCode(code: string, normalizedLanguage: string, rawLanguage: string): string {

View File

@ -0,0 +1,85 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
export interface TocItem {
id: string;
text: string;
level: number; // 1..6
}
@Injectable({ providedIn: 'root' })
export class TocService {
private itemsSubject = new BehaviorSubject<TocItem[]>([]);
private activeIdSubject = new BehaviorSubject<string | null>(null);
private observer?: IntersectionObserver;
get items$(): Observable<TocItem[]> { return this.itemsSubject.asObservable(); }
get activeId$(): Observable<string | null> { return this.activeIdSubject.asObservable(); }
extract(container: HTMLElement): TocItem[] {
const headings = Array.from(container.querySelectorAll('h1, h2, h3, h4')) as HTMLHeadingElement[];
const items: TocItem[] = headings.map(h => ({
id: h.id || this.ensureId(h),
text: (h.textContent || '').trim(),
level: Number(h.tagName.substring(1))
}));
this.itemsSubject.next(items);
return items;
}
setupScrollSpy(container: HTMLElement, rootMarginTopPx = 84): void {
this.dispose();
const headings = Array.from(container.querySelectorAll('h1, h2, h3, h4')) as HTMLHeadingElement[];
if (!('IntersectionObserver' in window) || headings.length === 0) return;
this.observer = new IntersectionObserver((entries) => {
const visible = entries
.filter(e => e.isIntersecting)
.sort((a, b) => (a.target as HTMLElement).offsetTop - (b.target as HTMLElement).offsetTop);
if (visible[0]) {
const id = (visible[0].target as HTMLElement).id;
if (id) this.activeIdSubject.next(id);
}
}, {
root: container,
rootMargin: `-${rootMarginTopPx}px 0px -60% 0px`,
threshold: [0, 1.0]
});
headings.forEach(h => this.observer!.observe(h));
}
smoothScrollTo(id: string, offsetPx = 84, container?: HTMLElement): void {
const el = document.getElementById(id);
if (!el) return;
if (container) {
const rect = el.getBoundingClientRect();
const cRect = container.getBoundingClientRect();
const top = container.scrollTop + (rect.top - cRect.top) - offsetPx;
container.scrollTo({ top, behavior: 'smooth' });
} else {
const rect = el.getBoundingClientRect();
const top = window.scrollY + rect.top - offsetPx;
window.scrollTo({ top, behavior: 'smooth' });
}
}
dispose(): void {
if (this.observer) {
this.observer.disconnect();
this.observer = undefined;
}
}
private ensureId(h: HTMLHeadingElement): string {
const base = (h.textContent || '').trim().toLowerCase()
.normalize('NFD').replace(/[\u0300-\u036f]/g, '')
.replace(/[^\w\s-]/g, '')
.trim().replace(/\s+/g, '-').replace(/-+/g, '-');
let id = base || 'section';
let i = 1;
while (document.getElementById(id)) id = `${base}-${i++}`;
h.id = id;
return id;
}
}

View File

@ -1,4 +1,9 @@
@import './styles-test.css'; @import './styles-test.css';
@import './styles/themes.css';
@import './styles/markdown.css';
@import './styles/syntax.css';
@import './styles/toc.css';
@import './styles/tokens.css';
@import './styles/_overlay-scrollbar.css'; @import './styles/_overlay-scrollbar.css';
@import './styles/codemirror.css'; @import './styles/codemirror.css';
@ -260,8 +265,29 @@ excalidraw-editor .excalidraw .layer-ui__wrapper {
--btn-muted-text: var(--text-muted); --btn-muted-text: var(--text-muted);
--scrollbar-thumb-color: rgba(148, 163, 184, 0.45); --scrollbar-thumb-color: rgba(148, 163, 184, 0.45);
--scrollbar-thumb-color-active: rgba(148, 163, 184, 0.75); --scrollbar-thumb-color-active: rgba(148, 163, 184, 0.75);
/* Theme accent + CodeMirror highlight tokens */
--color-accent: var(--primary, #3b82f6);
--cm-hl-bg: color-mix(in srgb, var(--color-accent) 28%, transparent);
--cm-hl-br: 3px;
/* Button design tokens */
--btn-radius: 0.75rem;
--btn-padding-y: .5rem;
--btn-padding-x: .75rem;
--btn-font: 500;
--btn-shadow: 0 2px 10px rgba(0,0,0,.06);
--btn-ring: 2px;
--btn-speed: 180ms;
--btn-bg: var(--color-accent);
--btn-fg: white;
--btn-bg-hover: color-mix(in srgb, var(--color-accent) 85%, white 15%);
--btn-bg-active: color-mix(in srgb, var(--color-accent) 75%, black 25%);
--btn-outline: color-mix(in srgb, var(--color-accent) 30%, transparent);
} }
[data-theme="dark"],
.dark { .dark {
--external-link: #22d3ee; --external-link: #22d3ee;
--external-link-hover: #0ea5e9; --external-link-hover: #0ea5e9;
@ -276,6 +302,11 @@ excalidraw-editor .excalidraw .layer-ui__wrapper {
--btn-muted-text: var(--text-muted); --btn-muted-text: var(--text-muted);
--scrollbar-thumb-color: rgba(148, 163, 184, 0.35); --scrollbar-thumb-color: rgba(148, 163, 184, 0.35);
--scrollbar-thumb-color-active: rgba(226, 232, 240, 0.72); --scrollbar-thumb-color-active: rgba(226, 232, 240, 0.72);
/* Dark theme adjustment */
--cm-hl-bg: color-mix(in srgb, var(--color-accent) 22%, transparent);
--btn-bg-hover: color-mix(in srgb, var(--color-accent) 80%, white 20%);
--btn-bg-active: color-mix(in srgb, var(--color-accent) 70%, black 30%);
} }
@media (min-width: 1024px) { @media (min-width: 1024px) {
@ -287,6 +318,44 @@ excalidraw-editor .excalidraw .layer-ui__wrapper {
} }
@layer components { @layer components {
/* CodeMirror markdown highlight class - theme driven */
.cm-md-highlight {
background: var(--cm-hl-bg);
border-radius: var(--cm-hl-br, 3px);
transition: background var(--btn-speed) ease;
}
/* Theme specific accent overrides */
[data-theme="nimbus"] { --color-accent: #7c3aed; }
[data-theme="emerald"] { --color-accent: #10b981; }
/* Unified Button Utilities */
.btn {
@apply inline-flex items-center justify-center gap-2 select-none;
border-radius: var(--btn-radius);
padding: var(--btn-padding-y) var(--btn-padding-x);
font-weight: var(--btn-font);
box-shadow: var(--btn-shadow);
transition: transform var(--btn-speed) ease, background var(--btn-speed) ease, color var(--btn-speed) ease, border-color var(--btn-speed) ease, box-shadow var(--btn-speed) ease;
outline: none;
}
.btn:focus-visible { box-shadow: 0 0 0 var(--btn-ring) var(--btn-outline); }
.btn:active { transform: translateY(1px) scale(0.99); }
.btn-solid { background: var(--btn-bg); color: var(--btn-fg); }
.btn-solid:hover { background: var(--btn-bg-hover); }
.btn-solid:active { background: var(--btn-bg-active); }
.btn-outline { background: transparent; color: var(--btn-bg); border: 1px solid var(--btn-outline); }
.btn-outline:hover { background: color-mix(in srgb, var(--btn-bg) 8%, transparent); }
.btn-ghost { background: transparent; color: var(--btn-bg); }
.btn-ghost:hover { background: color-mix(in srgb, var(--btn-bg) 6%, transparent); }
.btn-sm { @apply text-sm; padding: .375rem .6rem; }
.btn-md { @apply text-base; }
.btn-lg { @apply text-lg; padding: .65rem 1rem; }
/* Adaptive scrollbar states */ /* Adaptive scrollbar states */
.adaptive-scrollbar { .adaptive-scrollbar {
position: relative; position: relative;

View File

@ -226,8 +226,9 @@
Focus Styles (Accessibility) Focus Styles (Accessibility)
============================================ */ ============================================ */
.cm-editor.cm-focused { .cm-editor.cm-focused {
outline: 2px solid var(--cm-cursor); outline: 2px solid var(--ring);
outline-offset: -2px; outline-offset: -2px;
box-shadow: 0 0 0 2px color-mix(in oklab, var(--ring) 25%, transparent);
} }
/* ============================================ /* ============================================

View File

@ -167,6 +167,15 @@
outline-offset: 2px; outline-offset: 2px;
} }
.toc-toggle--idle {
color: var(--toc-muted);
}
.toc-toggle--active {
color: var(--toc-active);
filter: drop-shadow(0 0 4px color-mix(in oklab, var(--toc-active) 45%, transparent));
}
.badge { .badge {
@apply inline-flex items-center rounded-full px-2.5 py-1 text-xs font-semibold uppercase tracking-wide; @apply inline-flex items-center rounded-full px-2.5 py-1 text-xs font-semibold uppercase tracking-wide;
background-color: color-mix(in srgb, var(--bg-muted) 90%, transparent); background-color: color-mix(in srgb, var(--bg-muted) 90%, transparent);

109
src/styles/markdown.css Normal file
View File

@ -0,0 +1,109 @@
/* Scoped Markdown display styles */
.md-view {
color: var(--md-text);
line-height: 1.65;
}
/* Headings */
.md-view h1, .md-view h2, .md-view h3, .md-view h4, .md-view h5, .md-view h6 {
scroll-margin-top: 84px;
font-weight: 700;
}
.md-view h1 { color: var(--md-h1, var(--md-text)); font-size: 2rem; letter-spacing:-0.01em; }
.md-view h2 { color: var(--md-h2, var(--md-text)); font-size: 1.6rem; border-bottom: 1px solid var(--md-hr); padding-bottom: .25rem; }
.md-view h3 { color: var(--md-h3, var(--md-heading-accent)); font-size: 1.25rem; }
.md-view h4 { color: var(--md-heading-accent); font-size: 1.1rem; }
.md-view h5 { color: var(--md-muted); font-size: 1rem; font-weight: 600; }
.md-view h6 { color: var(--md-muted); font-size: .9rem; font-weight: 600; opacity:.85; }
/* Text, emphasis, inline code */
.md-view p { margin: .6rem 0; }
.md-view strong { font-weight: 700; }
.md-view em { font-style: italic; }
.md-view del { opacity:.8; text-decoration-thickness: .1rem; }
.md-view code {
background: var(--md-code-bg);
color: var(--md-code-fg);
border: 1px solid var(--md-pre-border);
border-radius: .45rem;
padding: .12rem .35rem;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
font-size: .94em;
}
/* Links & images */
.md-view a { color: var(--md-link); text-decoration: none; border-bottom: 1px dashed color-mix(in oklab, var(--md-link) 55%, transparent); }
.md-view a:hover { color: var(--md-link-hover); border-bottom-style: solid; }
.md-view img { max-width: 100%; border-radius: .5rem; display:block; margin: .5rem auto; }
/* Lists */
.md-view ul, .md-view ol { margin: .6rem 0 .6rem 1.25rem; }
.md-view li { margin: .25rem 0; }
.md-view ul ul, .md-view ol ol { margin-top:.25rem; }
/* Tasks */
.md-view input[type="checkbox"] {
appearance:none; width:1.05rem; height:1.05rem; margin-right:.5rem; vertical-align: middle;
border:1.5px solid var(--md-task-border); border-radius:.35rem; background:var(--md-task-bg);
}
.md-view input[type="checkbox"]:checked {
background: conic-gradient(var(--md-task-check) 0 100%, transparent 0);
border-color: var(--md-task-check);
}
/* Blockquote */
.md-view blockquote {
margin: .8rem 0; padding:.6rem .9rem;
background: var(--md-quote-bg);
border-left: .25rem solid var(--md-quote-bar);
color: var(--md-quote-fg);
border-radius: .5rem;
}
/* Code fences */
.md-view pre {
background: var(--md-pre-bg);
color: var(--md-pre-fg);
border:1px solid var(--md-pre-border);
border-radius:.75rem;
padding: .9rem 1rem;
overflow:auto;
}
.md-view pre code { background: transparent; border:0; padding:0; }
/* Tables */
.md-view table { width:100%; border-collapse: collapse; margin: .8rem 0; font-size:.95rem; }
.md-view thead th {
text-align:left; background: var(--md-table-head-bg);
border-bottom:1px solid var(--md-table-border); padding:.6rem .5rem; font-weight:600;
}
.md-view tbody td {
border-bottom:1px solid var(--md-table-border); padding:.6rem .5rem;
}
.md-view tbody tr:nth-child(even) { background: var(--md-table-row-alt); }
/* Separators */
.md-view hr { border:none; height:1px; background: var(--md-hr); margin:1.2rem 0; }
/* Footnotes */
.md-view sup.footnote-ref a { color: var(--md-fn-sup); font-weight:600; }
.md-view .footnotes { border-top:1px solid var(--md-fn-rule); margin-top:1rem; padding-top:.6rem; font-size:.9rem; color: var(--md-muted); }
/* Embedded HTML */
.md-view iframe, .md-view video { max-width:100%; border-radius:.5rem; }
/* Anchor visible on hover */
.md-view .heading-anchor {
opacity:0; margin-left:.25rem; font-size:.85em; color: var(--md-muted); text-decoration:none;
}
.md-view h1:hover .heading-anchor,
.md-view h2:hover .heading-anchor,
.md-view h3:hover .heading-anchor,
.md-view h4:hover .heading-anchor,
.md-view h5:hover .heading-anchor,
.md-view h6:hover .heading-anchor { opacity:1; }
/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
.md-view * { transition: none !important; animation: none !important; }
}

28
src/styles/syntax.css Normal file
View File

@ -0,0 +1,28 @@
/* Syntax highlighting mapped to Markdown tokens (Highlight.js selectors) */
.md-view pre code.hljs .hljs-keyword { color: var(--md-syntax-1); }
.md-view pre code.hljs .hljs-built_in { color: var(--md-syntax-2); }
.md-view pre code.hljs .hljs-type { color: var(--md-syntax-2); }
.md-view pre code.hljs .hljs-title { color: var(--md-syntax-2); }
.md-view pre code.hljs .hljs-string { color: var(--md-syntax-3); }
.md-view pre code.hljs .hljs-number { color: var(--md-syntax-4); }
.md-view pre code.hljs .hljs-literal { color: var(--md-syntax-4); }
.md-view pre code.hljs .hljs-attr { color: var(--md-syntax-2); }
.md-view pre code.hljs .hljs-attribute { color: var(--md-syntax-2); }
.md-view pre code.hljs .hljs-params { color: var(--md-syntax-2); }
.md-view pre code.hljs .hljs-selector-tag { color: var(--md-syntax-1); }
.md-view pre code.hljs .hljs-selector-id { color: var(--md-syntax-2); }
.md-view pre code.hljs .hljs-selector-class { color: var(--md-syntax-2); }
.md-view pre code.hljs .hljs-variable { color: var(--md-syntax-5); }
.md-view pre code.hljs .hljs-template-variable { color: var(--md-syntax-5); }
.md-view pre code.hljs .hljs-symbol { color: var(--md-syntax-5); }
.md-view pre code.hljs .hljs-bullet { color: var(--md-syntax-5); }
.md-view pre code.hljs .hljs-meta { color: var(--md-syntax-5); }
.md-view pre code.hljs .hljs-comment { opacity: .75; }
/* Prism fallback (if used) */
.md-view pre[class*="language-"] .token.keyword { color: var(--md-syntax-1); }
.md-view pre[class*="language-"] .token.function { color: var(--md-syntax-2); }
.md-view pre[class*="language-"] .token.string { color: var(--md-syntax-3); }
.md-view pre[class*="language-"] .token.number { color: var(--md-syntax-4); }
.md-view pre[class*="language-"] .token.punctuation,
.md-view pre[class*="language-"] .token.operator { color: var(--md-syntax-5); }

1047
src/styles/themes.css Normal file

File diff suppressed because it is too large Load Diff

30
src/styles/toc.css Normal file
View File

@ -0,0 +1,30 @@
/* TOC styles for pane and page */
.toc-pane, .toc-page {
background: var(--toc-bg);
color: var(--toc-fg);
border-left: 1px solid var(--toc-border);
}
.toc-page { border:1px solid var(--toc-border); border-radius:1rem; padding:1rem; }
.toc-list { list-style:none; margin:0; padding:.25rem .5rem .75rem .5rem; }
.toc-item { margin:.15rem 0; }
.toc-link {
display:flex; gap:.45rem; align-items:center;
padding:.35rem .5rem; border-radius:.5rem; color: var(--toc-fg);
text-decoration:none; border:1px solid transparent;
}
.toc-link:hover { color: var(--toc-hover); background: color-mix(in oklab, var(--surface-2) 88%, transparent); }
.toc-link.active {
color: var(--toc-active);
background: color-mix(in oklab, var(--surface-2) 80%, transparent);
border-color: color-mix(in oklab, var(--toc-active) 40%, var(--toc-border));
}
.toc-depth-2 { padding-left: .75rem; }
.toc-depth-3 { padding-left: 1.25rem; opacity:.95; }
.toc-depth-4 { padding-left: 1.75rem; opacity:.85; }
.toc-muted { color: var(--toc-muted); font-size:.9em; }
.toc-link:focus-visible { outline: 2px solid var(--toc-active); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
.toc-link { transition: none !important; }
}

View File

@ -6,7 +6,7 @@ module.exports = {
'./index.html', './index.html',
'./src/**/*.{html,ts,css}' './src/**/*.{html,ts,css}'
], ],
darkMode: ['class', '[data-theme="dark"]'], darkMode: 'class',
theme: { theme: {
extend: { extend: {
screens: { screens: {
@ -18,22 +18,44 @@ module.exports = {
'2xl': '1536px', '2xl': '1536px',
}, },
colors: { colors: {
// Core semantic tokens
bg: 'var(--bg)',
'bg-main': 'var(--bg-main)', 'bg-main': 'var(--bg-main)',
'bg-muted': 'var(--bg-muted)', 'bg-muted': 'var(--bg-muted)',
fg: 'var(--fg)',
card: 'var(--card)', card: 'var(--card)',
elevated: 'var(--elevated)', elevated: 'var(--elevated)',
'text-main': 'var(--text-main)', 'text-main': 'var(--text-main)',
'text-muted': 'var(--text-muted)', 'text-muted': 'var(--text-muted)',
muted: 'var(--muted)',
border: 'var(--border)', border: 'var(--border)',
// Surfaces
surface1: 'var(--surface-1)',
surface2: 'var(--surface-2)',
sidebar: 'var(--sidebar-bg)',
// Brand & accents
primary: 'var(--primary)',
brand: 'var(--brand)', brand: 'var(--brand)',
'brand-700': 'var(--brand-700)', 'brand-700': 'var(--brand-700)',
'brand-800': 'var(--brand-800)', 'brand-800': 'var(--brand-800)',
secondary: 'var(--secondary)',
accent: 'var(--accent)', accent: 'var(--accent)',
// Status
success: 'var(--success)', success: 'var(--success)',
warning: 'var(--warning)', warning: 'var(--warning)',
danger: 'var(--danger)', danger: 'var(--danger)',
info: 'var(--info)', info: 'var(--info)',
// UI elements
chip: 'var(--chip-bg)',
link: 'var(--link)',
'link-hover': 'var(--link-hover)',
ring: 'var(--ring)', ring: 'var(--ring)',
// Legacy nimbus (keep for compatibility)
nimbus: { nimbus: {
50: '#f0f9ff', 50: '#f0f9ff',
500: '#0ea5e9', 500: '#0ea5e9',

View File

@ -8,6 +8,8 @@ tags:
- accueil - accueil
- markdown - markdown
- bruno - bruno
- tag1
- tag3
aliases: [] aliases: []
status: en-cours status: en-cours
publish: false publish: false

View File

@ -18,5 +18,7 @@ tags:
- accueil - accueil
- markdown - markdown
- bruno - bruno
- tag1
- tag3
--- ---
Ceci est la page 1 Ceci est la page 1

View File

@ -12,17 +12,13 @@ tags:
- home - home
aliases: [] aliases: []
status: en-cours status: en-cours
publish: false publish: true
favoris: false favoris: true
template: false template: true
task: false task: true
archive: false archive: true
draft: false draft: true
private: false private: true
title: Page de test Markdown
created: 2025-09-25T21:20:45-04:00
modified: 2025-09-25T21:20:45-04:00
category: test
first_name: Bruno first_name: Bruno
birth_date: 2025-06-18 birth_date: 2025-06-18
email: bruno.charest@gmail.com email: bruno.charest@gmail.com

View File

@ -4,19 +4,21 @@ auteur: Bruno Charest
creation_date: 2025-09-25T07:45:20-04:00 creation_date: 2025-09-25T07:45:20-04:00
modification_date: 2025-10-19T12:09:47-04:00 modification_date: 2025-10-19T12:09:47-04:00
catégorie: "" catégorie: ""
tags:
- tag1
- tag2
- test
- test2
- home
aliases: [] aliases: []
status: en-cours status: en-cours
publish: false publish: true
favoris: false favoris: true
template: false template: true
task: false task: true
archive: false archive: true
draft: false draft: true
private: false private: true
title: Page de test Markdown
created: 2025-09-25T21:20:45-04:00
modified: 2025-09-25T21:20:45-04:00
category: test
first_name: Bruno first_name: Bruno
birth_date: 2025-06-18 birth_date: 2025-06-18
email: bruno.charest@gmail.com email: bruno.charest@gmail.com
@ -24,12 +26,6 @@ number: 12345
todo: false todo: false
url: https://google.com url: https://google.com
image: https://images.unsplash.com/photo-1675789652575-0a5d2425b6c2?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2070&q=80 image: https://images.unsplash.com/photo-1675789652575-0a5d2425b6c2?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2070&q=80
tags:
- tag1
- tag2
- test
- test2
- home
--- ---
#tag1 #tag2 #test #test2 #tag1 #tag2 #test #test2