[UI] Views modularisieren - TSX-Dateien in Komponenten aufteilen #290

Closed
opened 2026-01-25 14:52:29 +00:00 by jack · 1 comment
Owner

Problem

Die View-Dateien im UI-Package sind zu groß und monolithisch. Dies erschwert:

  • Wartbarkeit und Lesbarkeit
  • Code-Wiederverwendung
  • Testing einzelner Komponenten
  • Paralleles Arbeiten mehrerer Entwickler

Aktuelle Dateigrößen

Datei Zeilen Priorität
Settings.tsx 1536 🔴 Hoch
Sessions.tsx 945 🔴 Hoch
Documents.tsx 925 🔴 Hoch
Projects.tsx 554 🟡 Mittel
Tasks.tsx 547 🟡 Mittel
Live.tsx 442 🟡 Mittel
Insights.tsx 436 🟡 Mittel
Memories.tsx 377 🟢 Niedrig
Analytics.tsx 364 🟢 Niedrig
Dashboard.tsx 303 🟢 Niedrig
Search.tsx 250 🟢 Niedrig

Gesamt: ~7.000 Zeilen in 11 Dateien

Zielstruktur

Jede View sollte in ein eigenes Verzeichnis mit Unterkomponenten aufgeteilt werden:

packages/ui/src/views/
├── Settings/
│   ├── index.tsx              # Re-export
│   ├── Settings.tsx           # Hauptkomponente (Tabs, Layout)
│   ├── GeneralSettings.tsx
│   ├── AIProviderSettings.tsx
│   ├── WorkerSettings.tsx
│   └── components/
│       ├── SettingGroup.tsx
│       └── ApiKeyInput.tsx
│
├── Sessions/
│   ├── index.tsx
│   ├── Sessions.tsx           # Hauptkomponente (Liste, Filter)
│   ├── SessionCard.tsx
│   ├── SessionDetails.tsx
│   ├── SessionTimeline.tsx
│   └── components/
│       ├── SessionFilter.tsx
│       └── SessionStats.tsx
│
├── Documents/
│   ├── index.tsx
│   ├── Documents.tsx
│   ├── DocumentCard.tsx
│   ├── DocumentPreview.tsx
│   └── components/
│       ├── DocumentFilter.tsx
│       └── SourceBadge.tsx
│
└── ... (analog für andere Views)

Richtlinien für die Aufteilung

1. Komponenten-Größe

  • Max 200-300 Zeilen pro Komponente
  • Wenn größer → weiter aufteilen

2. Single Responsibility

  • Jede Komponente hat eine Aufgabe
  • Filter-Logik → eigene Komponente
  • Karten/Items → eigene Komponente
  • Modals/Dialoge → eigene Komponente

3. Wiederverwendbare Komponenten

Gemeinsame Patterns in components/ extrahieren:

packages/ui/src/components/
├── DataTable/           # Wiederverwendbare Tabelle
├── FilterBar/           # Such- und Filter-Leiste
├── EmptyState/          # "Keine Daten" Anzeige
├── LoadingState/        # Lade-Skeleton
├── ConfirmDialog/       # Bestätigungs-Modal
├── Pagination/          # Seiten-Navigation
└── StatCard/            # Statistik-Karte (Dashboard, Insights)

4. Hooks extrahieren

Komplexe State-Logik in Custom Hooks auslagern:

// Vorher: In Sessions.tsx
const [sessions, setSessions] = useState([]);
const [filter, setFilter] = useState({});
const [loading, setLoading] = useState(true);
// ... 50 Zeilen Logik

// Nachher: useSessions.ts
export function useSessions(projectId?: string) {
  // Alle Session-Logik gekapselt
  return { sessions, filter, setFilter, loading, refresh };
}

Schrittweise Umsetzung

Phase 1: Kritische Views (>900 Zeilen)

  1. Settings.tsx → siehe Issue #289
  2. Sessions.tsx
  3. Documents.tsx

Phase 2: Mittlere Views (400-600 Zeilen)

  1. Projects.tsx
  2. Tasks.tsx
  3. Live.tsx
  4. Insights.tsx

Phase 3: Gemeinsame Komponenten

  1. Wiederverwendbare Komponenten identifizieren
  2. In components/ extrahieren
  3. Views refactoren um gemeinsame Komponenten zu nutzen

Akzeptanzkriterien

  • Keine View-Datei über 300 Zeilen
  • Jede View in eigenem Verzeichnis mit index.tsx
  • Wiederverwendbare Komponenten in components/
  • Custom Hooks für komplexe State-Logik
  • Keine Funktionalitäts-Regression
  • TypeScript-Typen für alle Props

Priorität

Mittel - Technische Schulden, verbessert langfristige Wartbarkeit

Verwandte Issues

  • #289 - Settings.tsx Refactoring (Detail-Issue)
## Problem Die View-Dateien im UI-Package sind zu groß und monolithisch. Dies erschwert: - Wartbarkeit und Lesbarkeit - Code-Wiederverwendung - Testing einzelner Komponenten - Paralleles Arbeiten mehrerer Entwickler ## Aktuelle Dateigrößen | Datei | Zeilen | Priorität | |-------|--------|-----------| | `Settings.tsx` | 1536 | 🔴 Hoch | | `Sessions.tsx` | 945 | 🔴 Hoch | | `Documents.tsx` | 925 | 🔴 Hoch | | `Projects.tsx` | 554 | 🟡 Mittel | | `Tasks.tsx` | 547 | 🟡 Mittel | | `Live.tsx` | 442 | 🟡 Mittel | | `Insights.tsx` | 436 | 🟡 Mittel | | `Memories.tsx` | 377 | 🟢 Niedrig | | `Analytics.tsx` | 364 | 🟢 Niedrig | | `Dashboard.tsx` | 303 | 🟢 Niedrig | | `Search.tsx` | 250 | 🟢 Niedrig | **Gesamt: ~7.000 Zeilen in 11 Dateien** ## Zielstruktur Jede View sollte in ein eigenes Verzeichnis mit Unterkomponenten aufgeteilt werden: ``` packages/ui/src/views/ ├── Settings/ │ ├── index.tsx # Re-export │ ├── Settings.tsx # Hauptkomponente (Tabs, Layout) │ ├── GeneralSettings.tsx │ ├── AIProviderSettings.tsx │ ├── WorkerSettings.tsx │ └── components/ │ ├── SettingGroup.tsx │ └── ApiKeyInput.tsx │ ├── Sessions/ │ ├── index.tsx │ ├── Sessions.tsx # Hauptkomponente (Liste, Filter) │ ├── SessionCard.tsx │ ├── SessionDetails.tsx │ ├── SessionTimeline.tsx │ └── components/ │ ├── SessionFilter.tsx │ └── SessionStats.tsx │ ├── Documents/ │ ├── index.tsx │ ├── Documents.tsx │ ├── DocumentCard.tsx │ ├── DocumentPreview.tsx │ └── components/ │ ├── DocumentFilter.tsx │ └── SourceBadge.tsx │ └── ... (analog für andere Views) ``` ## Richtlinien für die Aufteilung ### 1. Komponenten-Größe - **Max 200-300 Zeilen** pro Komponente - Wenn größer → weiter aufteilen ### 2. Single Responsibility - Jede Komponente hat **eine Aufgabe** - Filter-Logik → eigene Komponente - Karten/Items → eigene Komponente - Modals/Dialoge → eigene Komponente ### 3. Wiederverwendbare Komponenten Gemeinsame Patterns in `components/` extrahieren: ``` packages/ui/src/components/ ├── DataTable/ # Wiederverwendbare Tabelle ├── FilterBar/ # Such- und Filter-Leiste ├── EmptyState/ # "Keine Daten" Anzeige ├── LoadingState/ # Lade-Skeleton ├── ConfirmDialog/ # Bestätigungs-Modal ├── Pagination/ # Seiten-Navigation └── StatCard/ # Statistik-Karte (Dashboard, Insights) ``` ### 4. Hooks extrahieren Komplexe State-Logik in Custom Hooks auslagern: ```typescript // Vorher: In Sessions.tsx const [sessions, setSessions] = useState([]); const [filter, setFilter] = useState({}); const [loading, setLoading] = useState(true); // ... 50 Zeilen Logik // Nachher: useSessions.ts export function useSessions(projectId?: string) { // Alle Session-Logik gekapselt return { sessions, filter, setFilter, loading, refresh }; } ``` ## Schrittweise Umsetzung ### Phase 1: Kritische Views (>900 Zeilen) 1. [ ] `Settings.tsx` → siehe Issue #289 2. [ ] `Sessions.tsx` 3. [ ] `Documents.tsx` ### Phase 2: Mittlere Views (400-600 Zeilen) 4. [ ] `Projects.tsx` 5. [ ] `Tasks.tsx` 6. [ ] `Live.tsx` 7. [ ] `Insights.tsx` ### Phase 3: Gemeinsame Komponenten 8. [ ] Wiederverwendbare Komponenten identifizieren 9. [ ] In `components/` extrahieren 10. [ ] Views refactoren um gemeinsame Komponenten zu nutzen ## Akzeptanzkriterien - [ ] Keine View-Datei über 300 Zeilen - [ ] Jede View in eigenem Verzeichnis mit `index.tsx` - [ ] Wiederverwendbare Komponenten in `components/` - [ ] Custom Hooks für komplexe State-Logik - [ ] Keine Funktionalitäts-Regression - [ ] TypeScript-Typen für alle Props ## Priorität Mittel - Technische Schulden, verbessert langfristige Wartbarkeit ## Verwandte Issues - #289 - Settings.tsx Refactoring (Detail-Issue)
Author
Owner

Ergänzung: Utility-Funktionen auslagern

Neben den Komponenten sollten auch Hilfsfunktionen in separate Utils-Dateien ausgelagert werden.

Typische Kandidaten

packages/ui/src/utils/
├── formatters.ts        # Datum, Zahlen, Bytes, Duration
├── filters.ts           # Filter-Logik für Listen
├── validators.ts        # Form-Validierung
├── api-helpers.ts       # API-Response-Handling, Error-Mapping
├── storage.ts           # LocalStorage-Wrapper
├── clipboard.ts         # Copy-to-Clipboard
└── constants.ts         # Shared Constants (Colors, Limits, etc.)

Beispiele

Vorher (in jeder View redundant):

// In Sessions.tsx
const formatDate = (date: string) => {
  return new Date(date).toLocaleDateString('de-DE', { ... });
};

// In Documents.tsx (gleicher Code)
const formatDate = (date: string) => {
  return new Date(date).toLocaleDateString('de-DE', { ... });
};

Nachher (zentral in utils):

// utils/formatters.ts
export function formatDate(date: string | Date, locale = 'de-DE'): string {
  return new Date(date).toLocaleDateString(locale, {
    day: '2-digit',
    month: '2-digit', 
    year: 'numeric',
    hour: '2-digit',
    minute: '2-digit'
  });
}

export function formatBytes(bytes: number): string {
  if (bytes === 0) return '0 B';
  const k = 1024;
  const sizes = ['B', 'KB', 'MB', 'GB'];
  const i = Math.floor(Math.log(bytes) / Math.log(k));
  return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;
}

export function formatDuration(ms: number): string {
  if (ms < 1000) return `${ms}ms`;
  if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
  return `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`;
}

export function formatTokens(count: number): string {
  if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`;
  if (count >= 1_000) return `${(count / 1_000).toFixed(1)}K`;
  return count.toString();
}

Weitere Utils

// utils/filters.ts
export function filterBySearch<T>(items: T[], query: string, keys: (keyof T)[]): T[] {
  if (!query.trim()) return items;
  const lower = query.toLowerCase();
  return items.filter(item => 
    keys.some(key => String(item[key]).toLowerCase().includes(lower))
  );
}

// utils/api-helpers.ts
export function handleApiError(error: unknown): string {
  if (error instanceof Error) return error.message;
  if (typeof error === 'string') return error;
  return 'Unknown error occurred';
}

// utils/storage.ts
export const storage = {
  get: <T>(key: string, fallback: T): T => {
    try {
      const item = localStorage.getItem(key);
      return item ? JSON.parse(item) : fallback;
    } catch {
      return fallback;
    }
  },
  set: (key: string, value: unknown): void => {
    localStorage.setItem(key, JSON.stringify(value));
  }
};

Aktualisierte Checkliste

Phase 4: Utils extrahieren

  • Formatter-Funktionen (Datum, Zahlen, Tokens, Bytes)
  • Filter-Funktionen
  • API-Helpers
  • Storage-Wrapper
  • Shared Constants
## Ergänzung: Utility-Funktionen auslagern Neben den Komponenten sollten auch **Hilfsfunktionen** in separate Utils-Dateien ausgelagert werden. ### Typische Kandidaten ``` packages/ui/src/utils/ ├── formatters.ts # Datum, Zahlen, Bytes, Duration ├── filters.ts # Filter-Logik für Listen ├── validators.ts # Form-Validierung ├── api-helpers.ts # API-Response-Handling, Error-Mapping ├── storage.ts # LocalStorage-Wrapper ├── clipboard.ts # Copy-to-Clipboard └── constants.ts # Shared Constants (Colors, Limits, etc.) ``` ### Beispiele **Vorher** (in jeder View redundant): ```typescript // In Sessions.tsx const formatDate = (date: string) => { return new Date(date).toLocaleDateString('de-DE', { ... }); }; // In Documents.tsx (gleicher Code) const formatDate = (date: string) => { return new Date(date).toLocaleDateString('de-DE', { ... }); }; ``` **Nachher** (zentral in utils): ```typescript // utils/formatters.ts export function formatDate(date: string | Date, locale = 'de-DE'): string { return new Date(date).toLocaleDateString(locale, { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }); } export function formatBytes(bytes: number): string { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`; } export function formatDuration(ms: number): string { if (ms < 1000) return `${ms}ms`; if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`; return `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`; } export function formatTokens(count: number): string { if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`; if (count >= 1_000) return `${(count / 1_000).toFixed(1)}K`; return count.toString(); } ``` ### Weitere Utils ```typescript // utils/filters.ts export function filterBySearch<T>(items: T[], query: string, keys: (keyof T)[]): T[] { if (!query.trim()) return items; const lower = query.toLowerCase(); return items.filter(item => keys.some(key => String(item[key]).toLowerCase().includes(lower)) ); } // utils/api-helpers.ts export function handleApiError(error: unknown): string { if (error instanceof Error) return error.message; if (typeof error === 'string') return error; return 'Unknown error occurred'; } // utils/storage.ts export const storage = { get: <T>(key: string, fallback: T): T => { try { const item = localStorage.getItem(key); return item ? JSON.parse(item) : fallback; } catch { return fallback; } }, set: (key: string, value: unknown): void => { localStorage.setItem(key, JSON.stringify(value)); } }; ``` ### Aktualisierte Checkliste **Phase 4: Utils extrahieren** - [ ] Formatter-Funktionen (Datum, Zahlen, Tokens, Bytes) - [ ] Filter-Funktionen - [ ] API-Helpers - [ ] Storage-Wrapper - [ ] Shared Constants
jack closed this issue 2026-01-25 16:07:57 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
customable/claude-mem#290
No description provided.