import { Layers, Check, AlertTriangle, Square, CheckSquare } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import type { PlateSelectorProps } from './types';
import { formatDuration } from '../../utils/date';
import { withStreamToken } from '../../api/client';
import { getBedTypeInfo } from '../../utils/bedType';
/**
* Plate selection grid for multi-plate 3MF files.
* Shows thumbnails, names, objects, and print times for each plate.
* In multi-select mode, plates have checkboxes for selecting a subset.
* In single-select mode, only one plate can be selected at a time.
*/
export function PlateSelector({
plates,
isMultiPlate,
selectedPlates,
onToggle,
onSelectAll,
onDeselectAll,
multiSelect,
quantities,
onQuantityChange,
}: PlateSelectorProps) {
const { t } = useTranslation();
// Only show for multi-plate files with multiple plates
if (!isMultiPlate || plates.length <= 1) {
return null;
}
const allSelected = selectedPlates.size === plates.length;
const showQuantities = !!quantities && !!onQuantityChange;
const totalRuns = showQuantities
? plates.reduce((sum, p) => (selectedPlates.has(p.index) ? sum + (quantities[p.index] ?? 1) : sum), 0)
: 0;
return (
Select Plate{multiSelect ? 's' : ''} to Print
{selectedPlates.size === 0 && (
Selection required
)}
{multiSelect && onSelectAll && onDeselectAll && (
{allSelected
? t('queue.deselectAll')
: t('queue.selectAllPlates', { count: plates.length })}
)}
{plates.map((plate) => {
const isSelected = selectedPlates.has(plate.index);
return (
/* The quantity stepper can't live inside the selection button —
nesting an input in a button is invalid and every keystroke
would toggle the plate. The card is a div; the selectable
region stays a button beside the stepper. */
onToggle(plate.index)}
className="flex items-center gap-2 text-left min-w-0 flex-1"
>
{multiSelect && (
isSelected
?
:
)}
{plate.has_thumbnail && plate.thumbnail_url != null ? (
) : (
)}
{plate.name || `Plate ${plate.index}`}
{plate.objects.length > 0
? plate.objects.slice(0, 3).join(', ') +
(plate.objects.length > 3 ? '...' : '')
: `${plate.filaments.length} filament${plate.filaments.length !== 1 ? 's' : ''}`}
{plate.print_time_seconds != null ? ` • ${formatDuration(plate.print_time_seconds)}` : ''}
{(() => {
// Per-plate bed-type badge so multi-plate prints make the
// required plate explicit at scheduling time (#1281).
const bed = getBedTypeInfo(plate.bed_type);
if (!bed) return null;
return (
{bed.label}
);
})()}
{!multiSelect && isSelected && (
)}
{showQuantities && isSelected && (
×
onQuantityChange(plate.index, Math.max(1, Math.min(999, parseInt(e.target.value) || 1)))
}
className="w-14 px-1.5 py-1 text-sm bg-bambu-dark border border-bambu-dark-tertiary rounded text-white text-center focus:outline-none focus:ring-1 focus:ring-bambu-green"
/>
)}
);
})}
{showQuantities && totalRuns > selectedPlates.size && (
{t('queue.plateQuantityTotal', { count: totalRuns })}
)}
);
}