2026-06-24-svg-loupe-design.md

docs/superpowers/specs/2026-06-24-svg-loupe-design.md

SVG-Outline-Lupe — Design

Datum: 2026-06-24 Status: Spec (zur Review) Scope: Minimal — die eine ausgelieferte Schrift (Ysabeau) perfekt + kugelsicher. Kein generischer Any-Font-Algorithmus, kein visuelles Font-Tool (beides bewusst out of scope).

Problem

Die Such-/Menü-Lupe wird aus den Schrift-Glyphen „0" (Linse) und „1" (Stiel) gebaut. Als Webfont rendern ist fragil: auf no-JS-Clients bei kaltem Cache rastert der rotierte Glyph auf einem Compositing-Layer einmal im Fallback-Font und wird ohne JS-Repaint nie neu gemalt → falsche Schrift. Diverse Fixes (translateZ, font-display, Re-Raster-Animationen, eager-load, inline-Subset-@font-face) wirkten im Headless-Test, aber nicht in echten Browsern (Chrome/Firefox, frisches Inkognito). Headless-Chromium ist toleranter als echte Browser → wiederholte Fehlalarme, weil Webfont-Rendering umgebungsabhängig ist.

Lösung

Die Lupe nicht aus einem geladenen Webfont rendern, sondern aus den Glyph-Outlines als Inline-SVG-Pfaden. Die „0"- und „1"-Konturen werden einmalig (build-time, fonttools) aus der Ysabeau-Datei extrahiert und als feste <path d="…"> ins CSS/HTML gebacken.

Warum das alle Probleme löst:

Geometrie / Alignment-Methode

Aus der wght-400-Instanz von Ysabeau (fontTools.varLib.instancer wght=400), unitsPerEm 1000:

Die finalen Transform-Werte/viewBox werden in der Implementierung per Screenshot-Iteration sauber getrimmt (die obigen Werte sind der berechnete Startpunkt). Acceptance ist visuell + deterministisch, nicht ein fixer Zahlenwert.

Komponenten

  1. Glyph-Extraktion (einmaliger Build-Schritt). fonttools: Ysabeau → wght=400 instanzieren → „0" und „1" via SVGPathPen als Pfad-d exportieren. Output: zwei Pfad-Strings + bboxes. (venv unter /tmp/ft-venv existiert; Vorgehen dokumentiert, kein Runtime-Dependency.)

  2. Loupe-SVG-Markup (inline, ~0,8 KB):

    <svg class="meta-loupe" viewBox="…" aria-hidden="true" focusable="false">
      <g class="meta-loupe-lens"><path d="…0…"/></g>
      <g class="meta-loupe-handle"><path d="…1…"/></g>
    </svg>

    fill: var(--text-color). Größe/Position via CSS (ersetzt das bisherige font-size-basierte Lupen-Styling).

  3. Menü-Integration. Die zwei <span class="menu-loupe">0</span><span class="menu-loupe-handle">1</span> im Body-Include werden durch das SVG ersetzt — in tools/generate-menu.py (das die Nav-HTML inkl. .menu-search bäckt) und im regenerierten includes/something-in-the-body.html. CSS in meta.css positioniert das SVG links im Suchfeld (gleiche Optik/Größe wie bisher).

  4. ⊘-Stoppschild (no-JS). Bei Klick auf das deaktivierte Suchfeld: .menu-search:has(input:disabled:active) .meta-loupe-handle { transform: … } — der Stiel „1" rotiert/verschiebt sich, sodass er als Diagonale mittig durch die Linse „0" geht = ⊘. Reine CSS-Transform auf der SVG-Gruppe, kein JS. Kein Font-Freeze, da kein Font zu swappen ist (SVG-Pfade rastern immer korrekt).

  5. where-is-01.html. Die Cursor-Lupe (.cursor-loupe::before/::after, content '0'/'1') wird durch dasselbe Inline-SVG ersetzt; das bestehende JS (Zoom2D, folgt dem Pointer) bleibt und skaliert/positioniert das SVG statt der Pseudo-Elemente.

Cleanup (Teil dieser Arbeit)

Da der Webfont-Weg ersetzt wird, entfernen:

Out of scope

Acceptance Criteria (messbar / deterministisch)

  1. Keine Font-Abhängigkeit der Lupe: grep zeigt keine @font-face-YsabeauLoupe und keine font-family-Lupe mehr in meta.css; Lupe ist Inline-SVG. CSP-Header wieder font-src 'self' (curl).
  2. Identisch mit/ohne JS: Playwright-Screenshot der Menü-Lupe JS-on und JS-off sind visuell gleich (beide = Ysabeau-Lupe).
  3. Font-unabhängig bewiesen: Screenshot mit allen Font-Requests geblockt (route abort *.ttf/*.woff*) → Lupe rendert unverändert korrekt.
  4. ⊘ formt sich: Screenshot bei :active auf dem disabled Suchfeld → erkennbares Stoppschild (Diagonale mittig durch die Linse).
  5. where-is-01: Cursor-Lupe ist das SVG und folgt dem Pointer (JS-on).
  6. No-regression: tools/check-no-js-page.py PASS auf testseite + einer konvertierten Seite; Menü-Links unverändert; node --check/CSS-Braces ok.
  7. Echter-Browser-Check: Headless täuschte bei Webfonts — hier irrelevant, weil SVG deterministisch rendert. Tim verifiziert final in frischem Inkognito (Chrome + Firefox): Lupe + ⊘ korrekt ohne JS.

Risiken / offene Punkte