managarten/docs/plans/articles-homepage.md
Till JS 72a5995fa5 feat(articles): M9 workbench homepage — 4-tab shell + QuickAdd + StatsView
Articles ist jetzt als Workbench-App in apps.ts registriert
(icon BookOpen, collection 'articles', paramKey 'articleId') und
landet damit im Scene-App-Picker. HomeView/ListView/HighlightsView/
StatsView teilen sich eine neue ArticlesTabShell, die sowohl als
SvelteKit-Route als auch als Workbench-Karte rendert.

Shell (ArticlesTabShell.svelte):
 - Top-Bar mit QuickAddInput (URL einfügen + Enter = Save + goto
   Reader; kein Preview-Schritt) und Settings-Gear.
 - Tab-Leiste darunter: Leseliste | Highlights | Favoriten | Stats.
   Leseliste ist Default (initialTab='list').
 - Tab-Wechsel läuft intern via $state + Svelte-Context — kritisch
   für die Workbench-Karte, wo goto() den User aus der Karte kicken
   würde. getArticlesTabContext() aus tab-context.ts gibt tief
   verschachtelten Sektionen eine switchTo(tab)-API.
 - Padding 1rem 1.25rem auf der Shell selbst — PageShell.page-body
   hat null padding, sonst klebt QuickAdd am Card-Rand. Im Route-
   Kontext addiert's sich zum (app)-Layout-Padding ohne zu viel.

Tabs:
 - Leseliste (list): bestehende ListView mit optionalem
   initialFilter-Prop. Continue-Reading-Strip (HomeSectionWeiterlesen
   horizontal carousel) erscheint über den Filter-Chips wenn
   status='reading'-Artikel existieren und filter ∈ {all, reading}.
   Filter-Chips sind einzeilig + horizontal scrollbar mit
   scroll-snap-Einrast; inaktive Chips haben jetzt sichtbare
   Background-Füllung + Border via color-mix(currentColor) — adaptiv
   fürs Theme.
 - Highlights (highlights): HighlightsView unverändert (nur der
   eigene Header + Zurück-Button raus, liegt jetzt in der Shell).
 - Favoriten (favorites): ListView mit initialFilter='favorites' —
   Shell-Shortcut auf den Filter.
 - Stats (stats): neue StatsView mit Stats-Strip (savedThisWeek,
   finishedThisWeek, avg reading time), Highlight-Counter, Top-
   Sources und Archiv-Link.

Routes (unter (tabs)-Gruppe):
 - /articles                → initialTab="list"   (Default)
 - /articles/list           → initialTab="list"   (alias)
 - /articles/highlights     → initialTab="highlights"
 - /articles/favorites      → initialTab="favorites"
 - /articles/stats          → initialTab="stats"
 Detail/Add/Settings bleiben bewusst ausserhalb — die haben ihren
 eigenen Reader/Form-Chrome und sollen die Tab-Leiste nicht zeigen.

Neue Files:
 - ArticlesTabShell.svelte   (Tab-Host)
 - tab-context.ts            (Cross-Tab-Switch-Context)
 - components/ArticleCard.svelte (shared Card aus ListView extrahiert,
                                  row + compact Varianten)
 - components/QuickAddInput.svelte (URL-Input aus HomeView extrahiert)
 - components/HomeSectionSources.svelte
 - components/HomeSectionStats.svelte
 - components/HomeSectionWeiterlesen.svelte
 - views/StatsView.svelte
 - routes/(app)/articles/(tabs)/{+page,list,highlights,favorites,stats}

Gelöscht:
 - HomeView.svelte (Overview-Tab wurde rausgenommen auf User-Feedback)
 - HomeSectionFrisch/Highlights/Favorites (durch eigene Tabs ersetzt)

docs/plans/articles-homepage.md dokumentiert den Architektur-Plan,
inklusive der Entscheidung für "eine Card pro Domain, interne Tabs"
statt zwei separater App-Registrierungen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 17:50:38 +02:00

14 KiB
Raw Permalink Blame History

Articles — Workbench Homepage

Status (2026-04-22)

Proposed. Follow-up auf die M1M8-Umsetzung (siehe articles-module.md).

Ziel

Die /articles-Landing ist heute eine flache Filter-Chips + Karten-Liste. Für einen Pocket-Klon ist das funktional aber „langweilig" — der User sieht keinen kuratierten Einstieg, keinen Fortschritt, keine Wiederaufnahme-Empfehlung. Bei Readwise Reader / Matter / Omnivore ist die Landing eine reiche Übersicht mit mehreren Sektionen; die flache Liste steht als Fallback daneben.

Dieses Plan-Dokument beschreibt eine neue HomeView als Default-Einstieg — multi-sektional, reaktiv, leanes Data-Layer (alle Queries existieren schon). Die bestehende ListView bleibt 1:1 erhalten und ist via Tab oder direkter Route (/articles/list) weiter erreichbar.

Abgrenzung

  • Kein Workbench-Cards-Rewrite. Articles registriert sich als eine App mit einer list-View. Die HomeView ist der neue Body dieser View — intern mehrere Sektionen, extern ein einziger Einstieg. Siehe workbench-cards-migration.md — das Prinzip „eine Karte pro Domain, interne Tabs wenn nötig" passt hier.
  • Kein Scene-Template. Wir schaffen keinen neuen Template-Handler („Lese-Scene"); Articles landet als ganz normale Workbench-App.
  • Kein Ersatz für das Dashboard-Widget. ArticlesUnreadWidget bleibt auf dem globalen /dashboard — das ist ein anderer Surface (Dashboard-Tile) mit anderem Zweck (3-Zeilen-Schnappschuss im Widget-Grid).
  • Keine neuen Queries/Stores. Alles was die Sections brauchen ist bereits in queries.ts vorhanden: useAllArticles, useStats, useAllHighlights. Falls wir was fehlt, wird's dort ergänzt — nicht im HomeView.

Warum jetzt

  1. M1M8 sind durch, das Modul funktioniert end-to-end — Polish-Phase.
  2. Nutzer kommt jetzt öfter vorbei (Bookmarklet + Share-Target → mehr Saves pro Woche). Eine Landing die sagt „hier ist was Neues, hier warst du stehengeblieben" lohnt sich plötzlich.
  3. Articles-as-Workbench-App-Registrierung fehlt noch komplett — das Modul lebt heute nur als Route, nicht als draggable Scene-App. Diese Homepage-Iteration ist der natürliche Moment das mitzunehmen.

Entscheidungen vorab

  • Eine HomeView, keine Tabs. Keine „Home vs. List"-Umschaltung in der UI. Die Liste-Sektionen DER Homepage ersetzen die alte ListView-Funktionalität. Wenn User wirklich nur die Chip-Filter-Liste will, geht er auf /articles/list (separate Route, rendert weiterhin ListView.svelte).
  • Scroll-Layout, kein Bento-Grid. Sections stacken vertikal (wie myday/ListView.svelte). Kein CSS-Grid-Layout mit Bento-Kacheln — das skaliert auf Mobile schlecht und ist optisch lauter. Scroll-Landing bleibt ruhig und funktional.
  • Gleiche Theme-Tokens wie das restliche Mana-UI (bg-surface, text-muted-foreground etc.). Kein Reader-Theme hier — die Homepage ist UI, nicht Content. Das unterscheidet sie von DetailView (Reader).
  • Keine Persistenz der Sektions-Reihenfolge. User kann Sektionen nicht umordnen/ausblenden. Wenn das Bedürfnis entsteht → separates Ticket. Startpunkt ist ein gut kuratiertes fixes Layout.

View-Struktur

┌──────────────────────────────────────────────────┐
│  HEADER                                          │
│  "Artikel"     [+ Neu] [✎ Highlights] [⚙ Settings]
│                                                   │
│  Weiterlesen  (wenn status='reading' existiert) │
│  ┌─────┐ ┌─────┐ ┌─────┐                        │
│  │     │ │     │ │     │  horizontal scroll      │
│  │card │ │card │ │card │  mit Progress-Bar       │
│  └─────┘ └─────┘ └─────┘                        │
│                                                   │
│  Frisch in der Leseliste  (status='unread')      │
│  ┌─────────────────────┐                         │
│  │ Artikel-Karte       │  vertikal, max 8        │
│  └─────────────────────┘                         │
│  ┌─────────────────────┐                         │
│  │ Artikel-Karte       │                         │
│  └─────────────────────┘                         │
│  [ Alle ungelesenen →]                           │
│                                                   │
│  Letzte Highlights  (wenn vorhanden)              │
│  ┌─────────────────────┐                         │
│  │ "…quote text…"      │  max 5                  │
│  │ Artikel-Titel       │                         │
│  └─────────────────────┘                         │
│  [ Alle Highlights → ]                           │
│                                                   │
│  Diese Woche                                      │
│  12 gespeichert · 4 gelesen · 7 min pro Artikel  │
│                                                   │
│  Deine Quellen  (top 5 siteName counts)           │
│  • spiegel.de         14 Artikel                  │
│  • theverge.com        8                          │
│  • …                                              │
│                                                   │
│  Favoriten  (wenn welche markiert)                │
│  ┌─────────────────────┐                         │
│  │ ★ Artikel-Karte     │                         │
│  └─────────────────────┘                         │
│                                                   │
│  Archiv-Link                                      │
│  [ 37 archivierte Artikel → ]                    │
└──────────────────────────────────────────────────┘

Sektionen im Detail

  1. Weiterlesen (Continue Reading) — nur wenn mindestens ein Artikel mit status='reading' existiert. Horizontaler Karussell-Scroll, 35 sichtbar auf Desktop. Jede Karte zeigt Titel + Site + readingProgress als dünnen Progress-Bar unten. Click → Reader.

  2. Frisch in der Leseliste — unread Artikel, sortiert savedAt DESC, max 8. Wenn mehr → Link „Alle ungelesenen →" der nach /articles/list?filter=unread geht.

  3. Letzte Highlights — max 5 Highlights sortiert createdAt DESC, gruppiert per Artikel (ein Eintrag pro Artikel, mit erstem Highlight als Preview und Count falls mehrere). Click → Artikel im Reader. Link „Alle Highlights →" nach /articles/highlights.

  4. Diese Woche (Stats-Strip) — eine Zeile, drei Zahlen: savedThisWeek, finishedThisWeek, durchschnittliche Lesezeit (ø readingTimeMinutes aller unread/reading Artikel). Keine Grafik — eine Text-Zeile.

  5. Deine Quellen — top 5 siteName nach Count, mit Total-Count pro Site. Click auf eine Site → /articles/list?filter=all&site=<siteName> (Site-Filter in der ListView neu, siehe „Offene Fragen").

  6. Favoriten — nur wenn favorites > 0. Max 4 Karten. Gleicher Card-Style wie „Frisch".

  7. Archiv-Link — schlichte Zeile am Ende: „37 archivierte Artikel →". Kein Embedded-Inhalt, reiner Link in die gefilterte ListView.

Empty-State: wenn der User 0 Artikel hat → identisch zur aktuellen ListView-Empty-State („Noch nichts gespeichert" + CTA + Hinweis auf Bookmarklet).

Daten-Queries (alle existieren bereits)

Sektion Query Datei
Weiterlesen useAllArticles() filtered status==='reading' queries.ts
Frisch useAllArticles() filtered status==='unread', slice(0,8) queries.ts
Highlights useAllHighlights(), slice(0,5) queries.ts
Stats-Strip useStats() — direkt savedThisWeek, finishedThisWeek, topSites queries.ts
Quellen useStats().topSites queries.ts
Favoriten useAllArticles() filtered isFavorite, slice(0,4) queries.ts
Archiv-Count useStats().archived queries.ts

Keine neue Query notwendig, nur Views die $derived darüberlegen.

Struktur

apps/mana/apps/web/src/lib/modules/articles/
├── HomeView.svelte              ← NEU — Default-Einstieg
├── ListView.svelte              ← bleibt, Fallback für Power-User
├── views/
│   ├── DetailView.svelte        (unverändert)
│   └── HighlightsView.svelte    (unverändert)
└── components/
    ├── HomeSectionWeiterlesen.svelte   ← NEU
    ├── HomeSectionFrisch.svelte        ← NEU
    ├── HomeSectionHighlights.svelte    ← NEU
    ├── HomeSectionStats.svelte         ← NEU
    ├── HomeSectionSources.svelte       ← NEU
    ├── HomeSectionFavorites.svelte     ← NEU
    └── ArticleCard.svelte              ← NEU — geteilte Karte, aus ListView extrahiert

Die einzelnen Section-Components sind bewusst klein und isoliert. HomeView ist nur Layout + Section-Orchestration.

Shared ArticleCard.svelte wird aus der ListView extrahiert (aktuell inline), damit beide Views identisch aussehen.

Routing

  • /articles → mountet HomeView (war bisher ListView)
  • /articles/list (neu) → mountet ListView (Fallback-Route für User die die flache Liste wollen)
  • /articles/[id] (unverändert)
  • /articles/add (unverändert)
  • /articles/highlights (unverändert)
  • /articles/settings (unverändert)

/articles/list verlinkt man aus der HomeView via „Alle ungelesenen →" Buttons; direkte URL-Navigation ist der Backup.

App-Registry-Integration

Neu in apps/mana/apps/web/src/lib/app-registry/apps.ts — Articles als draggable Scene-App registrieren:

registerApp({
  id: 'articles',
  name: 'Artikel',
  color: '#f97316',
  icon: BookOpen,  // oder BookmarkSimple aus @mana/shared-icons
  views: {
    list: { load: () => import('$lib/modules/articles/HomeView.svelte') },
    detail: { load: () => import('$lib/modules/articles/views/DetailView.svelte') },
  },
  contextMenuActions: [
    {
      id: 'new-article',
      label: 'URL speichern',
      icon: Plus,
      action: () => goto('/articles/add'),
    },
  ],
  collection: 'articles',
  paramKey: 'articleId',
  dragType: 'article',
  getDisplayData: (item) => ({
    title: (item.title as string) || 'Artikel',
    subtitle: item.siteName as string | undefined,
  }),
});

Danach ist Articles im Scene-App-Picker verfügbar und User kann „Articles" in jede Workbench-Scene ziehen — dort rendert dann die HomeView statt des Dashboard-Widgets. Das Dashboard-Widget bleibt für den separaten /dashboard-Surface.

UI-Tokens

Alle Sektions-Chrome (Überschriften, Buttons, Cards) nutzt die globalen Theme-Tokens aus @mana/shared-tailwind:

  • Überschriften: text-foreground font-semibold text-sm uppercase tracking-wide
  • Cards: bg-card border-border hover:bg-surface-hover
  • Muted text: text-muted-foreground
  • Primary-Akzente (Favoriten-Stern, Weiterlesen-Progress): weiterhin Articles-Orange #f97316 — konsistent mit dem restlichen Modul-Branding

Keine Tailwind-Raw-Farben (bg-gray-*, bg-white/*). Der pre-existing validate-theme-tokens.mjs-Audit darf nicht neu anschlagen.

Reihenfolge (Milestones)

  1. M9.1 — ArticleCard extrahieren — bestehende ListView-Card-Logik in components/ArticleCard.svelte auslagern, ListView angepasst, keine Verhaltensänderung. Ziel: keine Regression, nur Refactor.
  2. M9.2 — HomeView-Skelett + Sektionen — alle 6 Section-Components neu, HomeView orchestriert. Leere Sektionen werden ausgeblendet ({#if ...}). Route /articles mountet neu die HomeView.
  3. M9.3 — ListView-Route/articles/list angelegt, rendert weiterhin die bestehende ListView.svelte. „Alle ungelesenen →"-Links aus HomeView linken dorthin.
  4. M9.4 — App-Registry-Eintrag — Articles in apps.ts registriert, Icon ausgewählt, drag-type + getDisplayData. Articles erscheint im Scene-App-Picker.
  5. M9.5 — Site-Filter in ListView (klein) — ListView bekommt ?site=<siteName>-Query-Param-Support damit „Deine Quellen"-Links funktionieren. Tag-Filter analog via ?tag=<tagId> falls nicht schon da.

Offene Fragen

  • Continue-Reading-Karten Horizontal vs. Vertical: horizontal-scroll wirkt iOS-Reader-haft, vertical konsistent mit Rest. Ich neige zu horizontal für die Continue-Reading (weil's eine „Carousel of things you started"-Affordanz hat) und vertikal für alles andere. Wenn's zu bunt wird, alles vertikal — wenig Aufwand umzubauen.
  • Sektions-Reihenfolge — obige Liste ist mein Vorschlag. „Weiterlesen" zuerst fühlt sich richtig an (wie bei Netflix „Continue Watching"). „Frisch" danach. Highlights-Preview als drittes. Alternativ wäre „Frisch" primär wenn kein Weiterlesen da ist — das passiert aber schon durch {#if}-Gating.
  • Max-Counts pro Sektion (8 / 5 / 4) — Bauchgefühl. Bei User-Feedback nachjustieren. Dokumentiert als CONSTANTS-File falls's mal konfigurierbar werden soll.
  • Icon für die App-Registry — ich würde BookOpen oder BookmarkSimple aus @mana/shared-icons nehmen. Passt zu „Lesen / Später lesen". Abstimmung mit dem aktuellen APP_ICONS.articles-SVG (Lesezeichen-Dokument) — konsistent halten.
  • „Site-Filter" URL-Schema: ListView akzeptiert aktuell nur ?filter=<status>. Neu brauche ich ?site=<name> + ?tag=<id>. Pure Query-Param-Erweiterung in der ListView. Kein neues Routing.
  • Mobile-Layout: max-width 700px wie DetailView-Meta-Bar? Oder volle Breite mit ggf. 2-Spalten-Grid auf Desktop? Erste Iteration: single-column, 800px max, vertikal. 2-Spalten nur wenn später Bedarf.