Files
property-match/src/components/ops/SourceList.tsx
T
Benjamin Sutter da50f3b5ea feat: premium redesign — DM Serif, refined palette, flat score badges
- Design tokens (ds.ts, theme.ts, scoreTheme.ts): new warm palette
  (#152642 navy, #f9f8f6 warm white, #e8e7e4 borders, #b8975a gold accent),
  flat score tier badges replacing CSS gradients, Inter + DM Serif Display typography
- Card components: white-background cards, DM Serif score numbers, max-2 badge
  chips with +N overflow tooltip, editorial score badge positioning
- Layout shell: gold left-accent nav active state, 64px top bar, outlined
  workspace chip, DM Serif page titles
- Shared atoms: GenericBadge (outlined/solid variants), ResultFilterBar
  (simplified chip styles), DecisionContextPanel (dot metrics, no left accent)
- Global replacement (118 files): #1e3a5f→#152642, #e2e8f0→#e8e7e4,
  #f4f6f9→#f9f8f6 — all handled via Node.js for proper UTF-8 safety

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 22:26:30 +02:00

123 lines
4.3 KiB
TypeScript

import { Box, Chip, MenuItem, Select, TextField, Typography } from '@mui/material'
import { Search } from 'lucide-react'
import type { DataSource, SourceFilters } from '../../domain/dataSource'
import {
DataSourceType,
SourceStatus,
DATA_SOURCE_TYPE_LABELS,
SOURCE_STATUS_LABELS,
} from '../../domain/dataSource'
import { SourceCard } from './SourceCard'
import { MarketSignalSkeleton } from './MarketSignalSkeleton'
import { MarketSignalEmptyState } from './MarketSignalEmptyState'
interface SourceListProps {
sources: DataSource[]
isLoading: boolean
selectedId: string | null
onSelect: (id: string) => void
filters: SourceFilters
onFiltersChange: (f: SourceFilters) => void
}
export function SourceList({
sources,
isLoading,
selectedId,
onSelect,
filters,
onFiltersChange,
}: SourceListProps) {
const hasActiveFilters = filters.sourceType || filters.status || filters.search
return (
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
{/* Header */}
<Box sx={{ px: 1.5, py: 1.25, borderBottom: '1px solid #e2e8f0', display: 'flex', alignItems: 'center', gap: 1, flexShrink: 0 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, flex: 1 }}>
Datenquellen
</Typography>
<Chip
label={isLoading ? '…' : sources.length}
size="small"
sx={{ bgcolor: '#152642', color: '#fff', fontSize: '0.7rem', height: 20 }}
/>
</Box>
{/* Search */}
<Box sx={{ px: 1.5, py: 1, borderBottom: '1px solid #f1f5f9', flexShrink: 0 }}>
<TextField
size="small"
placeholder="Suchen..."
fullWidth
value={filters.search ?? ''}
onChange={(e) => onFiltersChange({ ...filters, search: e.target.value || undefined })}
slotProps={{
input: {
startAdornment: <Search size={14} color="#94a3b8" style={{ marginRight: 6 }} />,
},
}}
sx={{ '& .MuiOutlinedInput-root': { fontSize: '0.8rem' } }}
/>
</Box>
{/* Filters */}
<Box sx={{ px: 1.5, py: 1, borderBottom: '1px solid #f1f5f9', display: 'flex', gap: 1, flexShrink: 0 }}>
<Select
size="small"
displayEmpty
value={filters.sourceType ?? ''}
onChange={(e) => onFiltersChange({ ...filters, sourceType: (e.target.value as typeof DataSourceType[keyof typeof DataSourceType]) || undefined })}
sx={{ flex: 1, fontSize: '0.75rem', '& .MuiSelect-select': { py: 0.75 } }}
>
<MenuItem value=""><em>Alle Typen</em></MenuItem>
{Object.values(DataSourceType).map((t) => (
<MenuItem key={t} value={t} sx={{ fontSize: '0.8rem' }}>{DATA_SOURCE_TYPE_LABELS[t]}</MenuItem>
))}
</Select>
<Select
size="small"
displayEmpty
value={filters.status ?? ''}
onChange={(e) => onFiltersChange({ ...filters, status: (e.target.value as typeof SourceStatus[keyof typeof SourceStatus]) || undefined })}
sx={{ flex: 1, fontSize: '0.75rem', '& .MuiSelect-select': { py: 0.75 } }}
>
<MenuItem value=""><em>Alle Status</em></MenuItem>
{Object.values(SourceStatus).map((s) => (
<MenuItem key={s} value={s} sx={{ fontSize: '0.8rem' }}>{SOURCE_STATUS_LABELS[s]}</MenuItem>
))}
</Select>
</Box>
{hasActiveFilters && (
<Box sx={{ px: 1.5, py: 0.5, flexShrink: 0 }}>
<Chip
size="small"
label="Filter zurücksetzen"
onClick={() => onFiltersChange({})}
sx={{ fontSize: '0.7rem', cursor: 'pointer' }}
/>
</Box>
)}
{/* List */}
<Box sx={{ flex: 1, overflowY: 'auto' }}>
{isLoading ? (
Array.from({ length: 4 }).map((_, i) => <MarketSignalSkeleton key={i} />)
) : sources.length === 0 ? (
<MarketSignalEmptyState variant={hasActiveFilters ? 'no-results' : 'empty-inbox'} />
) : (
sources.map((source) => (
<SourceCard
key={source.id}
source={source}
selected={source.id === selectedId}
onClick={() => onSelect(source.id)}
/>
))
)}
</Box>
</Box>
)
}