* Initial plan * Wire package metadata into About app Co-authored-by: mroxso <24775431+mroxso@users.noreply.github.com> * Complete About version test wiring Co-authored-by: mroxso <24775431+mroxso@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: mroxso <24775431+mroxso@users.noreply.github.com>
25 KiB
PLAN.md — Nostr Layer Systems "Web OS"
1. Vision
Die App ist kein klassisches Website-Layout mit Navigation und Unterseiten, sondern ein Desktop-Betriebssystem im Browser. Jede frühere „Page" wird zu einer App, die als Fenster geöffnet, verschoben, skaliert, minimiert, maximiert und geschlossen werden kann.
Optisch: macOS-Grundstruktur, PostHog-Ästhetik.
- macOS liefert die Mechanik: Menüleiste oben, Fenster mit Titelleiste, Traffic-Light-Buttons, Fokus/Z-Order.
- posthog.com liefert die Optik: hell, ruhig, viel Weißraum, flache Flächen statt Glaseffekt-Orgie, kräftige aber sparsame Akzentfarbe, klare Typo-Hierarchie, dezente 1px-Rahmen, leicht verspielte Details ohne Skeuomorphismus.
Nicht-Ziel: kein Fake-macOS-Klon mit Apple-Icons, keine Glassmorphism-Überladung, keine Emulator-Spielerei. Das OS ist eine Metapher für Multitasking, nicht Selbstzweck.
2. Ausgangslage (Stand heute)
Das Repo ist der Startpunkt für LAYER.systems:
src/AppRouter.tsx— drei Routen:/,/:nip19,*src/pages/—Index.tsx(Platzhalter),NIP19Page.tsx,NotFound.tsxsrc/components/ui/— vollständiges shadcn/ui-Set (48+ Komponenten)- Nostr-Infrastruktur vorhanden:
NostrProvider,useNostr,useAuthor,useCurrentUser,useNostrPublish,useUploadFile,LoginArea - Theming über CSS-Variablen in
src/index.css(:root/.dark),useTheme
Es gibt also noch keine Feature-Seiten, die migriert werden müssten — der Window-Manager kann von Anfang an als Fundament gebaut werden, statt nachträglich übergestülpt zu werden. Das ist der günstigste Zeitpunkt für diese Architektur.
Dependencies sparsam. Drag/Resize wird mit Pointer-Events selbst implementiert
(~150 Zeilen), weil dnd-Bibliotheken für Fenster-Dragging überdimensioniert sind und wir
volle Kontrolle über Snapping, Grenzen und Touch-Verhalten brauchen. Ebenso kein neues
State-Lib — useReducer + Context genügen. Einzige Neuzugänge sind die drei
Markdown-Pakete für die Artikel-App (§6.1), die ausschließlich in deren Lazy-Chunk landen.
3. Architektur
3.1 Schichtenmodell
┌─────────────────────────────────────────────┐
│ MenuBar (fix, oben, 28px) │ z-index 100
├─────────────────────────────────────────────┤
│ │
│ Desktop (Wallpaper + App-Icons) │ z-index 0
│ │
│ ┌──────────────┐ │
│ │ WindowFrame │ ┌──────────────┐ │ z-index 10..99
│ │ │ │ WindowFrame │ │ (Stapel nach Fokus)
│ └──────────────┘ └──────────────┘ │
│ │
├─────────────────────────────────────────────┤
└─────────────────────────────────────────────┘
(kein Dock — volle Desktopfläche)
3.2 Neue Verzeichnisstruktur
src/
os/
registry.ts # App-Registry: Single Source of Truth
types.ts # AppDefinition, WindowState, ...
WindowManagerContext.ts
WindowManagerProvider.tsx
windowReducer.ts # reine Reducer-Logik (gut testbar)
useWindowManager.ts
useDrag.ts # Pointer-basiertes Drag
useResize.ts # Pointer-basiertes Resize (8 Handles)
layout.ts # Kaskade, Snapping, Clamping, Zentrierung
persistence.ts # localStorage-Serialisierung
components/os/
Desktop.tsx # Wallpaper + Icon-Grid + Marquee-Auswahl
DesktopIcon.tsx
MenuBar.tsx # macOS-Leiste oben
MenuBarClock.tsx
WindowLayer.tsx # rendert alle offenen Fenster
WindowFrame.tsx # Chrome: Titlebar, Traffic Lights, Resize-Handles
TrafficLights.tsx
MobileAppShell.tsx # Fullscreen-Fallback für Mobile
AppChrome.tsx # Toolbar/Sidebar-Bausteine für App-Inhalte
apps/
<app-id>/
index.tsx # der Fenster-Inhalt
definition.ts # Metadaten für die Registry
src/pages/ bleibt bestehen, schrumpft aber auf: Index.tsx (rendert nur noch
den <Desktop />-Shell), NIP19Page.tsx, NotFound.tsx.
3.3 App-Registry
Die Registry ist der zentrale Katalog. Eine neue App hinzufügen = ein Eintrag, keine Router-Änderung, kein Menü-Update, kein Icon-Grid-Update.
// src/os/types.ts
export interface AppDefinition {
id: string; // 'feed', 'settings', 'profile'
title: string; // "Feed"
icon: LucideIcon;
category: 'social' | 'tools' | 'system';
component: React.LazyExoticComponent<React.ComponentType<AppProps>>;
defaultSize: { width: number; height: number };
minSize?: { width: number; height: number };
resizable?: boolean; // default true
singleton?: boolean; // nur eine Instanz (z.B. Settings) — default true
showOnDesktop?: boolean; // default true
requiresAuth?: boolean; // zeigt Login-Prompt statt Inhalt
}
export interface AppProps {
windowId: string;
params?: Record<string, string>; // z.B. { npub: '...' } bei Deep-Link
setTitle: (title: string) => void; // App kann Fenstertitel setzen
}
Alle App-Komponenten werden per React.lazy() geladen → das initiale Bundle enthält
nur den Shell. Ein Fenster ist ein Code-Split-Punkt. <Suspense> in WindowFrame
zeigt einen Skeleton-Inhalt.
3.4 Window-State
export interface WindowState {
id: string; // nanoid-artig, `${appId}-${counter}`
appId: string;
title: string;
x: number; y: number; // Position relativ zum Desktop-Bereich
width: number; height: number;
z: number; // Stapelreihenfolge
minimized: boolean;
maximized: boolean;
prevRect?: Rect; // Rect vor dem Maximieren (für Restore)
params?: Record<string, string>;
}
Verwaltung über useReducer + Context (WindowManagerProvider), kein neues
State-Lib. Actions:
OPEN_APP · CLOSE_WINDOW · FOCUS_WINDOW · MOVE_WINDOW · RESIZE_WINDOW ·
MINIMIZE · RESTORE · TOGGLE_MAXIMIZE · SET_TITLE · CLOSE_ALL · HYDRATE
Wichtige Reducer-Regeln:
OPEN_APPbeisingleton: trueund bereits offenem Fenster → fokussieren + ggf. entminimieren statt neue Instanz.zwird beim Fokussieren aufmaxZ + 1gesetzt. Beiz > 9000einmalig normalisieren (alle Fenster neu durchnummerieren), um Overflow-Drift zu vermeiden.- Neue Fenster erscheinen kaskadiert (je +28px x/y, Reset nach 6 Fenstern), auf sichtbaren Bereich geclamped, initial mittig-versetzt.
Performance: Während Drag/Resize wird die Position nicht in den Reducer
geschrieben (das würde bei jedem Pointer-Move alle Fenster neu rendern). Stattdessen
schreibt das Drag-Hook direkt per transform auf das DOM-Element und dispatcht
einmal auf pointerup. Zusätzlich bekommt jedes WindowFrame ein React.memo.
3.5 Routing & Deep-Links
Der bestehende Router bleibt intakt (Vorgabe aus AGENTS.md: /:nip19 darf nicht
verschachtelt werden). Der OS-Zustand lebt im Query-String:
/ → Desktop, keine Fenster (oder wiederhergestellte Session)
/?app=feed → Desktop mit geöffnetem Feed-Fenster
/?app=profile&npub=npub1... → Profil-App mit Parameter
/npub1abc... → NIP19Page: öffnet Desktop + passende App im Fenster
Index.tsxliest beim Mount?app=und öffnet die entsprechenden Fenster.NIP19Page.tsxrendert ebenfalls den Desktop und öffnet je nach Identifier-Typ (npub/nprofile→ Profil-App,note/nevent→ Thread-App,naddr→ Artikel-App) ein Fenster mit den dekodierten Parametern.- Beim Fokuswechsel wird die URL per
replaceStateauf die aktive App aktualisiert, damit „Link kopieren" und Browser-Reload das Erwartete tun. Kein History-Spam: Fensterbewegungen schreiben nie in die URL. - Jedes Fenster hat im Kontextmenü „Link kopieren".
3.6 Persistenz
localStorage-Key nostr:os-session (v1-versioniert):
offene Fenster, Positionen, Größen, Z-Order, Minimiert-Status.
- Beim Start: Hydration → jedes Fenster gegen aktuelle Viewport-Größe clampen (Fenster außerhalb des Bildschirms zurückholen).
- Unbekannte
appId(App wurde entfernt) wird beim Hydrieren still verworfen. - Alles in
try/catch; kaputter/fehlender State ⇒ leerer Desktop, nie ein Crash. - Schreiben debounced (300 ms).
- Im Menü: „Fenster › Alle schließen" und „Sitzung zurücksetzen".
4. Design-System (PostHog-Kalibrierung)
4.1 Farben
src/index.css wird angepasst — die vorhandenen Token-Namen bleiben (shadcn hängt
daran), nur die Werte ändern sich, plus neue OS-Token.
| Token | Light | Zweck |
|---|---|---|
--os-desktop |
hsl(40 20% 96%) |
warmes Off-White als Wallpaper-Basis |
--background |
hsl(0 0% 100%) |
Fenster-Inhalt |
--foreground |
hsl(20 14% 12%) |
fast-schwarz, leicht warm |
--border |
hsl(30 10% 88%) |
1px-Hairlines |
--primary |
hsl(265 85% 60%) |
Nostr-Violett als Akzent |
--os-titlebar |
hsl(40 15% 98%) |
Titelleiste aktiv |
--os-titlebar-inactive |
hsl(40 10% 96%) |
Titelleiste inaktiv |
Dark Mode ist gleichwertig, nicht Nachgedanke: --os-desktop: hsl(24 10% 8%),
Fenster hsl(24 8% 12%), Rahmen hsl(24 6% 22%).
4.2 Materialien
- Fenster:
bg-background,border border-border,rounded-xl, Schatten in zwei Stufen — fokussiertshadow-2xl, unfokussiertshadow-md+ leicht reduzierte Deckkraft der Titelleiste. Der Fokus muss auf einen Blick erkennbar sein. - Menüleiste:
backdrop-blur-md+ halbtransparentes Weiß. Der einzige Ort mit Blur — das hält es besonders statt beliebig. - Radius:
--radius: 0.75rem(bereits gesetzt) passt; Fensterxl, Buttonsmd. - Bewegung: kurz und funktional. Öffnen 160 ms
scale(.96)→1+ Fade, Minimieren 200 ms Richtung Menüleiste, Schließen 120 ms Fade. Alles respektiertprefers-reduced-motion(dann: nur Opazität, keine Transforms).
4.3 Typografie
- UI-Text: System-Stack (
-apple-system, ui-sans-serif, …), 13px Basis in Chrome-Flächen, 14–15px in App-Inhalten. - Überschriften in Apps: klar größer,
font-semibold,tracking-tight. - Monospace (
ui-monospace) für technische Werte: Pubkeys, Event-IDs, Relay-URLs.
4.4 „App statt Website"
Damit sich Inhalte nicht wie Landingpages anfühlen, gilt für jeden App-Inhalt:
- Keine eigene Page-Kopfzeile mit riesigem H1 — der Fenstertitel ist die Überschrift.
- Kein zentrierter Content mit
max-w-4xl mx-autound viel Luft daneben; Layout füllt das Fenster (h-full, Flex/Grid). - Scrollen passiert innerhalb des Fensters, nie auf
body. - Optionale App-Toolbar direkt unter der Titelleiste (Suche, Filter, Aktionen),
36px hoch,
border-b. - Optionale App-Sidebar links (200–240px), wenn die App Navigation braucht — wie Mail.app oder Finder.
- Leerzustände sind knapp und handlungsorientiert („Noch keine Notizen · Neue anlegen"), keine Marketing-Illustrationen.
- Dichte statt Weite: Listenzeilen ~44px, keine Karten-mit-Riesenpadding.
5. Komponenten im Detail
5.1 MenuBar (oben, fix, 28px)
[◆ Logo] [App-Name ▾] [Datei ▾] [Ansicht ▾] [Fenster ▾] ······ [Relays] [Theme] [Uhr] [Avatar ▾]
- Links: Logo-Menü (Über, Einstellungen…, Sitzung zurücksetzen).
- App-Menü: Name des fokussierten Fensters in
font-semibold— der macOS-Kern-Trick, der die OS-Illusion trägt. Ohne Fokus: „Finder"-Äquivalent, hier „Desktop". - Fenster-Menü: Liste aller offenen Fenster mit Häkchen beim aktiven, plus „Alle minimieren" / „Alle schließen".
- Rechts: Relay-Status (Punkt grün/gelb/rot + Anzahl verbundener Relays),
Theme-Toggle, Uhrzeit (Locale-formatiert, minütlich aktualisiert), Login/Avatar
(bestehende
LoginArea, in Menü-Optik umgestylt). - Umsetzung mit dem vorhandenen shadcn
menubarbzw.dropdown-menu— Keyboard-Navigation und Fokus-Handling sind damit geschenkt.
5.2 Desktop
- Wallpaper: feines Punktraster (~22px Abstand, sehr geringer Kontrast) auf
--os-desktop. Kein Foto, kein Verlauf — das Raster gibt Textur und Tiefe, ohne den Fensterinhalten Aufmerksamkeit zu stehlen. Umsetzung als CSSradial-gradient+background-size, ein einziger Token für die Punktfarbe (Light/Dark). - Icon-Grid: oben links beginnend, spaltenweise nach unten (macOS-Konvention), ~80px Zellen, Icon + Label.
- Interaktion: einfacher Klick = Auswahl, Doppelklick = öffnen (Touch: einfacher Tap), Enter öffnet Auswahl, Pfeiltasten navigieren.
- Rechtsklick auf leere Fläche → Kontextmenü (Hintergrund wechseln, Symbole aufräumen, Alle Fenster schließen).
5.3 WindowFrame
- Titelleiste 36px: links Traffic Lights, mittig Titel (
truncate, 13px,font-medium), rechts optionaler App-Aktionsslot. - Traffic Lights: rot/gelb/grün, 12px, Symbole (×, −, ⤢) erscheinen erst beim
Hover über die Gruppe. Unfokussiert alle grau.
Jeweils echte
<button>mitaria-label— nicht nur farbigedivs. - Doppelklick auf Titelleiste → maximieren/wiederherstellen.
- Drag nur an der Titelleiste; Fenster darf nicht unter die Menüleiste geschoben werden; mind. 60px müssen im Viewport bleiben.
- Resize: 8 Handles (4 Kanten je 6px, 4 Ecken je 12px), respektiert
minSize. - Maximieren = Vollfläche unterhalb der Menüleiste (kein Fullscreen-API).
- Snapping (Phase 5): Ziehen an den linken/rechten Rand → Halbseite, nach oben → maximiert; mit Vorschau-Overlay.
5.4 Tastatur
| Kürzel | Aktion |
|---|---|
⌘/Ctrl + W |
Fokussiertes Fenster schließen |
⌘/Ctrl + M |
Minimieren |
| `⌘/Ctrl + `` | Nächstes Fenster |
⌘/Ctrl + K |
Spotlight / Command-Palette (nutzt vorhandenes cmdk) |
Esc |
Menü/Palette schließen |
Fokusfalle pro Fenster ist nicht gewünscht (Fenster sind nicht-modal), aber Tab-Reihenfolge folgt der Z-Order, und ein neu geöffnetes Fenster bekommt den Fokus.
5.5 Mobile (< 768px)
Fenster-Management ergibt auf dem Handy keinen Sinn. Über useIsMobile:
- Menüleiste bleibt (kompakt: Logo, aktive App, Avatar).
- Desktop-Icons werden zum App-Grid wie ein Homescreen.
- Eine App öffnet fullscreen mit Zurück-Pfeil statt Traffic Lights.
- Offene Apps über einen „App-Switcher" (Liste) erreichbar.
- Kein Drag, kein Resize, keine Positionsspeicherung.
Damit gilt die Vorgabe „responsiv bis ~360px" ohne verkrüppelte Fenster-Metapher.
6. Die Apps
Sieben Apps für v1 — bewusst begrenzt, dafür jede wirklich fertig statt vieler halber:
| App | ID | Icon | Inhalt |
|---|---|---|---|
| Feed | feed |
Rss |
Globale/Follow-Timeline (kind 1), Composer, Antworten |
| Profil | profile |
User |
kind-0-Metadaten, Notizen des Autors, Follow-Button |
| Notizen | notes |
FileText |
Einzelne Notiz / Thread (aus note1/nevent1) |
| Einstellungen | settings |
Settings |
Theme, Relay-Verwaltung, Blossom-Server, Account |
| Über | about |
Info |
Was ist das, welche NIPs, Links |
| Relay-Monitor | relays |
Activity |
System-App: verbundene Relays, Latenz, Event-Durchsatz, Fehler |
| Artikel | articles |
BookOpen |
Long-form-Reader (NIP-23, kind 30023) aus naddr1, Markdown-Rendering |
Später: Nachrichten (NIP-17/44), Wallet (NIP-60).
Hinweise zu den beiden Zusatz-Apps:
- Relay-Monitor trägt das OS-Thema am stärksten und ist billig: er liest den
Verbindungsstatus aus dem vorhandenen
NostrProvider/Pool, misst Round-Trip-Zeiten und zeigt eine dichte Tabelle. Er speist zugleich die Relay-Anzeige in der Menüleiste (§5.1) — eine Datenquelle, zwei Oberflächen. - Artikel rendert Markdown mit
react-markdown(Wahl und Begründung siehe §6.1).
6.1 Markdown-Rendering (Artikel-App)
Wahl: react-markdown@10 + remark-gfm@4 + rehype-sanitize@6.
Der entscheidende Unterschied zu den Alternativen ist nicht Größe oder Geschwindigkeit, sondern das Sicherheitsmodell:
| Kandidat | Ausgabe | Bewertung |
|---|---|---|
react-markdown |
React-Elementbaum | Ruft nie innerHTML auf. Roh-HTML im Markdown wird ohne das optionale rehype-raw gar nicht erst interpretiert. Sicher by construction. |
marked |
HTML-String | Schnell und winzig, erzwingt aber dangerouslySetInnerHTML + DOMPurify. Sicher nur, solange niemand den Sanitizer-Schritt vergisst. |
markdown-it |
HTML-String | Sehr erweiterbar, gleiche Schwäche wie marked; zusätzlich das größte Paket der drei. |
Bei Inhalten, die ungeprüft von fremden Relays kommen, ist „sicher, weil es gar nicht
anders geht" mehr wert als „sicher, wenn man daran gedacht hat". Deshalb react-markdown.
Zwei weitere Gründe, die konkret zu diesem Projekt passen:
components-Prop = Styling ohne CSS-Kampf. Jedes Markdown-Element wird auf unsere eigenen Komponenten gemappt (h1→ Fenster-Typo,pre→ Code-Block mit Monospace-Token,table→ shadcn-Table). Ein String-Renderer würde uns stattdessen inprose-Overrides zwingen.- Abgefangene Links tragen die OS-Metapher. Über
components.alassen sichnostr:-URIs undnpub/naddr-Erwähnungen abfangen und in Fenster-Öffner verwandeln (openApp('profile', { npub })) statt in Seitennavigation. Genau das ist der Punkt, an dem sich die App wie ein OS und nicht wie eine Website anfühlt — mit einem HTML-String wäre das nur über nachträgliches DOM-Patching machbar.
Konfiguration:
remark-gfmfür Tabellen, Strikethrough, Task-Listen und Autolinks — NIP-23-Artikel nutzen GFM in der Praxis regelmäßig.rehype-sanitizeals zweite Verteidigungslinie. Streng genommen redundant, solangerehype-rawnicht aktiv ist, aber es kostet ~3 kB und schützt davor, dass eine spätere Änderung die Sicherheitsannahme still kippt.urlTransformzusätzlich eigenhändig setzen: nurhttps:,http:,mailto:undnostr:durchlassen, alles andere verwerfen (javascript:,data:).rehype-rawwird nicht installiert. Wer es später hinzufügen will, muss diese Entscheidung bewusst umstoßen.- Der gesamte Block hängt am
React.lazy-Chunk der Artikel-App und belastet das Start-Bundle nicht.
Bilder aus Artikeln laufen über die projektübliche sanitizeUrl-Prüfung (siehe
nostr-security), bevor sie ins img-Mapping gehen.
Jede App wird nach den Regeln aus §4.4 gebaut. Nostr-Datenzugriff ausschließlich über
die vorhandenen Hooks (useNostr + TanStack Query, useAuthor, useNostrPublish),
inkl. der projektüblichen Validierungsfunktion pro Event-Kind vor dem Rendern.
7. Umsetzung in Phasen
Jede Phase endet mit einem lauffähigen Stand und einem grünen npm run test.
Phase 1 — Fundament (Window-Manager ohne Optik)
src/os/types.ts,registry.ts,windowReducer.ts,WindowManagerProvider,useWindowManagerWindowManagerProviderinApp.tsxeinhängen (innerhalbTooltipProvider)Desktop+WindowLayer+ rudimentäresWindowFrame(öffnen/schließen/fokussieren)- Eine Dummy-App zum Testen
- ✅ Ergebnis: Fenster lassen sich öffnen, fokussieren, schließen
Phase 2 — Fenster-Interaktion
useDrag,useResize(Pointer-Events, Direkt-DOM während der Geste)- Maximieren/Wiederherstellen, Minimieren, Kaskadierung, Clamping
layout.tsmit Viewport-Grenzen; Reaktion aufresizedes Fensters- ✅ Ergebnis: Fenster verhalten sich wie echte Fenster
Phase 3 — macOS-Shell
MenuBarmit App-Menü, Fenster-Menü, Uhr, Relay-Status, Theme-Toggle, LoginDesktopmit Icon-Grid, Auswahl, Doppelklick-Öffnen, Kontextmenü- Tastenkürzel
- ✅ Ergebnis: fühlt sich wie ein Desktop an
Phase 4 — Visuelles Design
index.css: neue Farbtoken (Light + Dark), OS-Token- Fenster-Chrome finalisieren: Traffic Lights, Fokus-/Unfokus-Zustände, Schatten
- Animationen inkl.
prefers-reduced-motion - ✅ Ergebnis: PostHog-Look sitzt
Phase 5 — Politur
- Minimier-/Restore-Animation Richtung Menüleiste
- Snapping an Ränder mit Vorschau
- Command-Palette (
⌘K) übercmdk - Persistenz + Session-Wiederherstellung
- ✅ Ergebnis: Reload stellt den Arbeitsplatz wieder her
Phase 6 — Apps mit echtem Inhalt
- Feed, Profil, Notizen, Einstellungen, Über, Relay-Monitor, Artikel
- Deep-Link-Integration in
NIP19Page - Mobile-Shell
- ✅ Ergebnis: funktionierender Nostr-Client im OS-Gewand
8. Risiken und wie wir sie adressieren
| Risiko | Gegenmaßnahme |
|---|---|
| Ruckeln beim Ziehen vieler Fenster | Direktes DOM-transform während der Geste, Dispatch erst auf pointerup; React.memo pro Fenster |
| OS-Metapher nervt auf Mobile | Ab < 768px Fullscreen-Shell statt Fenster (§5.5) |
| Schlechte Zugänglichkeit ("hübsche divs") | Traffic Lights als echte Buttons mit aria-label, Fenster als role="dialog" aria-label={title}, vollständige Tastaturbedienung, sichtbare Fokusringe |
| SEO / Crawler sehen nur einen leeren Desktop | Pro geöffneter App useSeoMeta mit passendem Titel/Beschreibung setzen; /:nip19-Routen liefern echte Metadaten |
| Bundle wächst mit jeder App | Jede App per React.lazy, Suspense-Skeleton im Fenster |
| Gespeicherte Fenster liegen außerhalb des Viewports | Beim Hydrieren gegen aktuelle Größe clampen |
| Z-Index-Drift | Normalisierung ab z > 9000 |
| Session-State inkompatibel nach Refactor | Versionsschlüssel im localStorage; unbekannte Version ⇒ verwerfen |
9. Getroffene Entscheidungen
Diese Punkte sind geklärt und in den Plan eingearbeitet:
| Frage | Entscheidung | Konsequenz |
|---|---|---|
| Dock | Kein Dock | Offene Fenster ausschließlich über das Fenster-Menü in der Menüleiste. Minimieren animiert dorthin. Volle Desktopfläche, weniger Code. |
| Akzentfarbe | Nostr-Violett hsl(265 85% 60%) |
Eigene Identität statt PostHog-Klon, gleiche Helligkeit/Sättigung wie das Referenz-Orange. Grün bleibt für Statussignale reserviert. |
| Wallpaper | Punktraster, fix | Kein Preset-Umschalter, keine Persistenz dafür nötig. Ein Token für die Punktfarbe je Theme. |
| Markdown | react-markdown + remark-gfm + rehype-sanitize |
Kein innerHTML, also XSS-sicher by construction; components-Mapping erlaubt Fenster-öffnende nostr:-Links (§6.1). |
| App-Umfang v1 | Sieben Apps: Feed, Profil, Notizen, Einstellungen, Über, Relay-Monitor, Artikel | Relay-Monitor und Artikel sind aus „später" nach v1 gezogen (§6). |
10. Definition of Done für v1
- Fenster: öffnen, schließen, ziehen, skalieren, minimieren, maximieren, fokussieren — alles flüssig
- Menüleiste zeigt korrekt die fokussierte App und alle offenen Fenster
- Desktop-Icons öffnen Apps per Doppelklick und Tastatur
- Light- und Dark-Mode beide vollständig durchgestaltet
- Mobile-Shell funktioniert bis 360px Breite
- Deep-Links (
/npub1…,/note1…,?app=) öffnen das richtige Fenster - Sitzung überlebt einen Reload
- Vollständige Tastaturbedienung, sichtbare Fokusringe,
prefers-reduced-motionbeachtet npm run testgrün (tsc + eslint + vitest + build)
11. Was während der Umsetzung dazukam
Diese Punkte standen nicht im ursprünglichen Plan, ergaben sich aber aus der Arbeit am laufenden System:
setParamspro Fenster. Apps können ihre eigenen Parameter ändern (§3.5). Damit bleibt die URL auch bei Navigation innerhalb einer App korrekt, und die Auswahl überlebt den Wechsel zwischen Desktop- und Mobile-Shell.- NIP-19-Relay-Hints werden ausgewertet. Ohne sie scheiterten Deep-Links regelmäßig,
weil das Event auf einem Relay liegt, das der Leser nicht abonniert hat.
nprofile,neventundnaddrreichen ihre Hints jetzt bis in die Query durch — und kopierte Links bekommen umgekehrt die eigenen Read-Relays als Hint mit. - Reader auf schmalen Fenstern. Die Sidebar verschwand unter 640px und machte die Artikelliste unerreichbar. Jetzt wechseln Liste und Artikel sich ab.
- Fenstertitel werden auf 48 Zeichen gekürzt. Artikel-Überschriften von Relays sind beliebig lang und sprengten Titelleiste, Fenster-Menü und Browser-Tab.
- Minimierte Fenster. Das
hidden-Attribut verliert gegen dieflex-Utility-Klasse; das Ausblenden läuft deshalb über einen Inline-Style. - Relay-Anzeige ist neutral statt rot, wenn nichts verbunden ist. Relays werden erst bei Bedarf geöffnet und danach wieder geschlossen — „0 verbunden" ist der Normalzustand, kein Fehler.
- Inter als selbstgehostete Schrift (
@fontsource-variable/inter), weil die CSP des Projektsfont-src 'self'vorgibt und Google Fonts damit ausscheidet. - Kompakte Variante von
LoginArea/AccountSwitcher, damit der Login in eine 28px hohe Menüleiste passt statt sie zu sprengen.