# 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](https://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.tsx` - `src/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/ / 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 ``-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. ```ts // src/os/types.ts export interface AppDefinition { id: string; // 'feed', 'settings', 'profile' title: string; // "Feed" icon: LucideIcon; category: 'social' | 'tools' | 'system'; component: React.LazyExoticComponent>; 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; // 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. `` in `WindowFrame` zeigt einen Skeleton-Inhalt. ### 3.4 Window-State ```ts 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; } ``` 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_APP` bei `singleton: true` und bereits offenem Fenster → fokussieren + ggf. entminimieren statt neue Instanz. - `z` wird beim Fokussieren auf `maxZ + 1` gesetzt. Bei `z > 9000` einmalig 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.tsx` liest beim Mount `?app=` und öffnet die entsprechenden Fenster. - `NIP19Page.tsx` rendert 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 `replaceState` auf 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 — fokussiert `shadow-2xl`, unfokussiert `shadow-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; Fenster `xl`, Buttons `md`. - **Bewegung:** kurz und funktional. Öffnen 160 ms `scale(.96)→1` + Fade, Minimieren 200 ms Richtung Menüleiste, Schließen 120 ms Fade. Alles respektiert `prefers-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-auto` und 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 `menubar` bzw. `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 CSS `radial-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 `