pediatric-ai-scribe-v3/shared
Daniel 12eaf57ddb feat(client): Learning Hub — sanitized HTML body + Marp slide viewer
Closes the last two Learning Hub follow-ups flagged in the earlier
commit body: rich-HTML body rendering (instead of pre-wrap plain text)
and in-React Marp slide playback (instead of the legacy-viewer link).

client/src/lib/sanitize.ts (new)
  Inline HTML sanitizer. Parses via DOMParser (sandboxed — no scripts
  run), walks the tree and:
    • Removes script / style / iframe / object / embed / link / meta /
      base / form / input / button / select / textarea.
    • Drops every on* event-handler attribute.
    • Drops href / src / xlink:href values starting with javascript:
      or data:text/html.
    • Drops any attribute whose value contains "javascript:".
    • Falls back to entity-escaping the raw string if parsing throws.
  Admin-authored Learning Hub content is the trust model here —
  essentially CMS content. A full DOMPurify dep would be strictly
  better but adding a package requires a network install; the inline
  sanitizer covers the realistic XSS vectors without the dep bump.

client/src/pages/Learning.tsx
  • Body rendering (non-presentation content_type) now runs through
    sanitizeHtml + dangerouslySetInnerHTML with a `prose prose-sm`
    Tailwind-typography class. Markdown/HTML formatting from admin
    content now appears correctly (headings, lists, code, bold,
    italics, links) instead of raw text.
  • SlideViewer component (new) replaces the "Open in legacy viewer"
    button for content_type === 'presentation'. Fetches
    /api/learning/content/:slug/slides (returns { css, slides[] } —
    server-side Marp output), sanitizes each slide's HTML + the CSS
    block, renders one slide at a time with:
      - prev/next buttons
      - keyboard ←/→ and PageUp/PageDown navigation
      - slide counter (N/total)
      - fullscreen toggle (Escape exits)
    Marp's own CSS is injected scoped-ish via sanitizer so slide
    theming survives.

shared/types.ts + client/src/shared/types.ts — additive:
  LearningSlidesOk { css: string; slides: string[] }

Client tsc + vite build clean. Initial bundle unchanged
(342.86 kB / 106.03 kB gz) since Learning.tsx was already lazy-loaded;
the sanitizer + SlideViewer roll into its chunk.
2026-04-24 02:30:44 +02:00
..
clinical feat(client): port BP Percentile — all 10 Calculator pills now run in React 2026-04-24 02:15:17 +02:00
schemas.test.ts build(ts): day 5 — Vitest + Zod + Knip tooling 2026-04-23 19:54:30 +02:00
schemas.ts build(ts): day 5 — Vitest + Zod + Knip tooling 2026-04-23 19:54:30 +02:00
types.ts feat(client): Learning Hub — sanitized HTML body + Marp slide viewer 2026-04-24 02:30:44 +02:00