da50f3b5ea
- 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>
123 lines
4.3 KiB
TypeScript
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>
|
|
)
|
|
}
|