feat: enhance code viewer with modern Angular inputs and improved styling

- Updated CodeRendererComponent to use Angular's new input() signals instead of @Input decorators
- Added fallback content escaping for code that can't be syntax highlighted
- Improved code viewer styling with larger fonts, better padding and gradient header background
- Enhanced file content fetching with robust cache-busting and fallback mechanisms
- Added debug logging to track content loading and syntax highlighting
- Fixe
This commit is contained in:
Bruno Charest 2025-11-02 10:51:56 -05:00
parent 0dc346d6b7
commit 79e80fd798
4 changed files with 156 additions and 22 deletions

View File

@ -31,7 +31,7 @@ Tu es un pair-programmer Angular 20. Objectif: retirer tout import global d'Exca
5) Ne change pas lUI/UX. Respecte Angular 20 et Tailwind 3.4.
```
### 0.2 Virtualiser la liste centrale Nimbus avec `PaginatedNotesList`
### 0.2 Virtualiser la liste centrale Nimbus avec `PaginatedNotesList`
- Objectif: maintenir ≥55 FPS et mémoire <200MB avec 5k+ notes.
- Critères dacceptation:

View File

@ -1,4 +1,4 @@
import { Component, Input, computed } from '@angular/core';
import { Component, computed, input, effect } from '@angular/core';
import { CommonModule } from '@angular/common';
import hljs from 'highlight.js/lib/core';
import javascript from 'highlight.js/lib/languages/javascript';
@ -32,16 +32,16 @@ hljs.registerLanguage('sql', sql);
styles: [`
:host { display:block; }
.root { border: 1px solid var(--border); background: var(--card); border-radius: 1rem; overflow: hidden; }
.header { display:flex; align-items:center; gap:.5rem; padding:.5rem .75rem; font-size:.75rem; color: var(--text-muted); border-bottom: 1px solid var(--border); background: color-mix(in oklab, var(--card) 92%, black 8%); }
.header { display:flex; align-items:center; gap:.5rem; padding:.6rem .9rem; font-size:.8rem; color: var(--text-muted); border-bottom: 1px solid var(--border); background: linear-gradient(90deg, color-mix(in oklab, var(--card) 92%, black 8%), color-mix(in oklab, var(--card) 86%, black 14%)); }
.title { display:inline-flex; align-items:center; gap:.5rem; font-weight:600; color: var(--text-main); }
.title svg { width: 16px; height: 16px; color: var(--accent, #9b87f5); }
.lang { margin-left:auto; padding:.125rem .5rem; border:1px solid var(--border); border-radius:.5rem; font-weight:600; color: var(--text-main); }
pre { margin:0; padding: .75rem 1rem; font-family: var(--font-mono, ui-monospace, Menlo, Monaco, Consolas, 'Courier New', monospace); font-size:.875rem; line-height:1.5; overflow:auto; }
.lang { margin-left:auto; padding:.15rem .6rem; border:1px solid var(--border); border-radius:.5rem; font-weight:700; letter-spacing:.04em; color: var(--text-main); }
pre { margin:0; padding: .9rem 1.1rem; font-family: var(--font-mono, ui-monospace, Menlo, Monaco, Consolas, 'Courier New', monospace); font-size:.9rem; line-height:1.5; overflow:auto; }
code { display:block; }
.hljs { color: var(--text-main); }
`],
template: `
<div class="root animate-fadeIn">
<div class="root md-view animate-fadeIn">
<div class="header">
<span class="title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
@ -57,16 +57,16 @@ hljs.registerLanguage('sql', sql);
`
})
export class CodeRendererComponent {
@Input() path: string = '';
@Input() content: string = '';
path = input<string>('');
content = input<string>('');
fileName = computed(() => {
const p = this.path || '';
const p = this.path() || '';
return p.split('/').pop() || p.split('\\').pop() || 'code';
});
languageLabel = computed(() => {
const src = this.content || '';
const src = this.content() || '';
if (!src.trim()) return 'TEXT';
try {
const res = hljs.highlightAuto(src);
@ -77,13 +77,29 @@ export class CodeRendererComponent {
});
highlighted = computed(() => {
const src = this.content || '';
const src = this.content() || '';
try {
if (!src.trim()) { return hljs.highlightAuto('').value; }
if (!src.trim()) { return ''; }
const res = hljs.highlightAuto(src);
return res.value;
const v = res.value;
return v && v.length > 0 ? v : this.escapeHtml(src);
} catch {
return hljs.highlightAuto(src).value;
return this.escapeHtml(src);
}
});
constructor() {
effect(() => {
const len = (this.content() || '').length;
// eslint-disable-next-line no-console
console.log('[CodeRenderer] content length:', len);
});
}
private escapeHtml(src: string): string {
return src
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
}

View File

@ -72,7 +72,7 @@ import { VideoPlayerComponent } from '../../app/features/note-view/components/vi
</div>
<!-- Code Viewer -->
<div *ngIf="viewerType() === 'code'" class="smart-file-viewer__text animate-fadeIn w-full h-full">
<div *ngIf="viewerType() === 'code'" class="smart-file-viewer__code animate-fadeIn w-full h-full">
<app-code-renderer [path]="filePath" [content]="resolvedContent()" />
</div>
@ -127,6 +127,18 @@ import { VideoPlayerComponent } from '../../app/features/note-view/components/vi
overflow: auto;
}
/* Code viewer should align to top-left so header is always visible */
.smart-file-viewer__code {
flex: 1;
display: flex;
align-items: flex-start;
justify-content: flex-start;
padding: 1rem 1.25rem;
overflow: auto;
min-height: 0; /* allow proper flexbox scrolling */
min-width: 0;
}
/* Excalidraw should stretch exactly like PDF */
.smart-file-viewer__excalidraw {
flex: 1;
@ -240,6 +252,12 @@ export class SmartFileViewerComponent implements OnChanges {
// Internal fetched content for non-markdown text/code files
private fetchedContent = signal<string>('');
resolvedContent = computed(() => {
const vt = this.viewerType();
// For code/text files, always prioritize fetched content
if (vt === 'code' || vt === 'text') {
return this.fetchedContent();
}
// For other types, use input content if available
const c = this.contentSig();
return (c && c.length > 0) ? c : this.fetchedContent();
});
@ -278,11 +296,52 @@ export class SmartFileViewerComponent implements OnChanges {
if (path.startsWith('.obsidian/')) {
this.fetchedContent.set('');
} else {
const url = `/vault/${encodeURI(path)}`;
fetch(url)
.then(r => r.text().catch(() => ''))
.then(txt => this.fetchedContent.set(txt))
.catch(() => this.fetchedContent.set(''));
const ts = Date.now();
const url = `/vault/${encodeURI(path)}?v=${ts}`;
console.log(`[SmartFileViewer] Fetching ${vt} file content from: ${url}`);
// Bypass HTTP cache to avoid 304 (no body) and always get fresh content
fetch(url, {
cache: 'no-store',
headers: {
'Cache-Control': 'no-cache',
'Pragma': 'no-cache',
'If-Modified-Since': 'Mon, 01 Jan 1990 00:00:00 GMT'
}
})
.then(async (r) => {
try {
const txt = await r.text();
return txt ?? '';
} catch (e) {
console.warn(`[SmartFileViewer] Response without body for ${path} (status ${r.status}).`, e);
return '';
}
})
.then(async (txt) => {
if (txt && txt.length > 0) {
console.log(`[SmartFileViewer] Successfully fetched ${txt.length} chars for ${path}`);
this.fetchedContent.set(txt);
return;
}
// Fallback: force a distinct URL to defeat proxy caches that ignore cache headers
const url2 = `/vault/${encodeURI(path)}?vv=${Date.now()}`;
console.log(`[SmartFileViewer] Fallback fetching content from: ${url2}`);
const r2 = await fetch(url2, {
cache: 'reload',
headers: {
'Cache-Control': 'no-cache',
'Pragma': 'no-cache',
'If-Modified-Since': 'Mon, 01 Jan 1990 00:00:00 GMT'
}
});
const txt2 = await r2.text().catch(() => '');
console.log(`[SmartFileViewer] Fallback fetched ${txt2.length} chars for ${path}`);
this.fetchedContent.set(txt2);
})
.catch(err => {
console.error(`[SmartFileViewer] Failed to fetch ${path}:`, err);
this.fetchedContent.set('');
});
}
} else {
this.fetchedContent.set('');

View File

@ -77,6 +77,65 @@ console.log('hello world');
> citation
> avec plusieurs lignes
```javascript
console.log('hello world');
```python
"""Exemple de script Python illustrant l'usage des dataclasses."""
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from typing import Iterable
@dataclass
class Task:
"""Représente une tâche planifiée avec une durée estimée."""
name: str
estimated_minutes: int
created_at: datetime = field(default_factory=datetime.now)
completed: bool = False
def remaining_minutes(self) -> int:
"""Calcule le temps restant estimé."""
return 0 if self.completed else self.estimated_minutes
def mark_done(self) -> None:
"""Marque la tâche comme terminée."""
self.completed = True
def summarize(tasks: Iterable[Task]) -> str:
"""Retourne un résumé textuel de la charge de travail."""
task_list = list(tasks)
total = sum(task.estimated_minutes for task in task_list)
remaining = sum(task.remaining_minutes() for task in task_list)
done = total - remaining
return (
f"Tâches: {len(task_list)}\n"
f"Terminé: {done} min\n"
f"Restant: {remaining} min\n"
f"Charge totale: {total} min"
)
def main() -> None:
"""Point d'entrée du script avec quelques exemples."""
tasks = [
Task("Préparer la réunion", estimated_minutes=45),
Task("Répondre aux emails", estimated_minutes=20),
Task("Prototyper l'interface", estimated_minutes=90),
]
# Marquer une tâche comme terminée pour illustrer la logique.
tasks[1].mark_done()
print("Résumé du jour:")
print(summarize(tasks))
# Afficher l'échéance approximative si toutes les tâches sont démarrées maintenant.
deadline = datetime.now() + timedelta(minutes=sum(task.remaining_minutes() for task in tasks))
print(f"Heure de fin estimée: {deadline:%H:%M}")
if __name__ == "__main__":
main()
```