"use client";
import { Button, Badge } from "../ui";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip";
import { Check } from 'lucide-react';
import React from 'react';
export interface SelectableOption {
id: string; // Selection ID (UUID for platforms, value for others)
name: string; // Primary identifier (platform enum or item name)
displayName?: string; // Optional display name (for platforms)
description?: string;
isActive?: boolean;
createdAt?: string;
updatedAt?: string;
icon?: React.ReactNode;
color?: string;
disabled?: boolean; // If true, option is shown grayed out and not selectable
disabledReason?: string; // Tooltip shown on hover when disabled — explains WHY it's unavailable
section?: string; // Optional section ID to group options
}
export interface SectionDefinition {
id: string; // Section identifier (matches option.section)
label: string; // Display label for the section
icon?: React.ReactNode; // Optional icon for the section header
description?: string; // Optional description shown under section label
}
interface PushButtonSelectorProps {
options: SelectableOption[];
selectedIds: string[];
onSelectionChange: (selectedIds: string[]) => void;
multiSelect?: boolean;
title?: string;
helpText?: string;
className?: string;
selectionSummary?: boolean;
optional?: boolean;
isLoading?: boolean;
error?: string | null;
skeletonCount?: number;
sections?: SectionDefinition[]; // Optional sections for grouping options
}
// Skeleton component matching external pattern from announcement-form.tsx
function PushButtonSelectorSkeleton({ count = 3, hasTitle }: { count?: number; hasTitle?: boolean }) {
return (
{hasTitle && (
)}
{[...Array(count)].map((_, i) => (
))}
);
}
// Error component using ODS error tokens
function PushButtonSelectorError({ message, title }: { message: string; title?: string }) {
return (
);
}
export function PushButtonSelector({
options,
selectedIds,
onSelectionChange,
multiSelect = true,
title,
helpText,
className = '',
selectionSummary = false,
optional = false,
isLoading = false,
error = null,
skeletonCount = 3,
sections
}: PushButtonSelectorProps) {
// LOADING STATE
if (isLoading) {
return (
);
}
// ERROR STATE
if (error) {
return (
);
}
// VALIDATION: Only filter invalid selectedIds if options are loaded
const validSelectedIds = options.length > 0
? selectedIds.filter(id => options.some(option => option.id === id))
: selectedIds; // Keep all IDs if options not loaded yet
// Dev warning for debugging (only when options are loaded)
if (process.env.NODE_ENV === 'development' && options.length > 0 && validSelectedIds.length !== selectedIds.length) {
const invalidIds = selectedIds.filter(id => !options.some(opt => opt.id === id));
console.warn('[PushButtonSelector] Invalid selected IDs filtered:', invalidIds);
}
const toggleSelection = (optionId: string) => {
if (multiSelect) {
const isSelected = validSelectedIds.includes(optionId);
if (isSelected) {
onSelectionChange(validSelectedIds.filter(id => id !== optionId));
} else {
onSelectionChange([...validSelectedIds, optionId]);
}
} else {
// Single select mode
onSelectionChange(validSelectedIds.includes(optionId) ? [] : [optionId]);
}
};
const getSelectedOptions = () => options.filter(option => validSelectedIds.includes(option.id));
// Helper to render a single option
const renderOption = (option: SelectableOption) => {
const isSelected = validSelectedIds.includes(option.id);
const optionEl = (
(!option.disabled || isSelected) && toggleSelection(option.id)}
>
{option.icon && (
{option.icon}
)}
{option.displayName || option.name}
{option.description && (
{option.description}
)}
{/* Selection Indicator */}
{isSelected && (
)}
);
// Disabled options explain WHY on hover via the unified Tooltip.
if (option.disabled && option.disabledReason) {
return (
{optionEl}
{option.disabledReason}
);
}
return optionEl;
};
// Group options by section if sections are provided
const renderOptionsContent = () => {
if (!sections || sections.length === 0) {
// No sections - render flat list
return (
{options.map(renderOption)}
);
}
// Group options by section
const optionsBySection = new Map();
const ungroupedOptions: SelectableOption[] = [];
options.forEach(option => {
if (option.section) {
const existing = optionsBySection.get(option.section) || [];
optionsBySection.set(option.section, [...existing, option]);
} else {
ungroupedOptions.push(option);
}
});
return (
{/* Render sections in order */}
{sections.map(section => {
const sectionOptions = optionsBySection.get(section.id) || [];
if (sectionOptions.length === 0) return null;
return (
{/* Section Header */}
{section.icon && (
{section.icon}
)}
{section.label}
{section.description && (
{section.description}
)}
{/* Section Options */}
{sectionOptions.map(renderOption)}
);
})}
{/* Render ungrouped options at the end */}
{ungroupedOptions.length > 0 && (
{ungroupedOptions.map(renderOption)}
)}
);
};
return (
{title && (
{title}
)}
{renderOptionsContent()}
{/* Selection Summary */}
{selectionSummary && validSelectedIds.length > 0 && (
{validSelectedIds.length} {multiSelect ? 'items' : 'item'} selected
{getSelectedOptions().map(option => (
{option.displayName || option.name}
))}
)}
{/* Help Text */}
{helpText && (
{helpText}
)}
{/* Empty State Warning */}
{validSelectedIds.length === 0 && title && !optional && (
⚠️ Please select at least one option
)}
);
}