diff --git a/src/docs/routes/experimental/periodic-table.tsx b/src/docs/routes/experimental/periodic-table.tsx index 8456db2..253f9f0 100644 --- a/src/docs/routes/experimental/periodic-table.tsx +++ b/src/docs/routes/experimental/periodic-table.tsx @@ -122,6 +122,45 @@ function OutputTypesDemo() { const outputTypesSource = __SOURCE__ +/* DEMO_START */ +function EnabledSymbolsDemo() { + const [selectedElement, setSelectedElement] = useState(null) + + const enabledSymbols = [ + "Mn", "Fe", "Co", "Ni", "Cu", "Zn", "Ga", "Ge", "As", "Se", + "Br", "Kr", "Rb", "Sr", "Y", "Zr", "Nb", "Mo", "Tc", "Ru", + "Rh", "Pd", "Ag", "Cd", "In", "Sn", "Sb", "Pr", "Nd", "Pm", + "Sm", "Eu", "Gd", "Tb", "Dy", "Ho", "Er", "Tm", "Yb", "Lu", + "Hf", "Ta", "W", "Re", "Os", "Ir", "Pt", "Au", "Hg", "Tl", + "Pb", "Bi", "Po", "At", "Rn", "Fr", "Ra", "Ac", "Th", "Pa", + "U", "Np", "Pu", "Am", "Cm", "Bk", "Cf", + ] + + return ( +
+ + + {selectedElement && ( +
+

Selected Element:

+ {selectedElement.symbol} + {selectedElement.name} +
+ )} +
+ ) +} +/* DEMO_END */ + +const enabledSymbolsSource = __SOURCE__ + function PeriodicTablePage() { @@ -160,6 +199,19 @@ function PeriodicTablePage() { + + + Grid Variant with Enabled Symbols + Restrict selectable elements to a subset using enabledSymbols + + + } + source={enabledSymbolsSource} + /> + + + Output Types diff --git a/src/ui/experimental/periodic-table.tsx b/src/ui/experimental/periodic-table.tsx index da9d9f6..1a13faf 100644 --- a/src/ui/experimental/periodic-table.tsx +++ b/src/ui/experimental/periodic-table.tsx @@ -1,53 +1,88 @@ -import * as React from "react" -import { Check, ChevronsUpDown, Search } from "lucide-react" -import { cn } from "../../lib/utils" -import { Button } from "@/ui/elements/button" -import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/ui/elements/command" -import { Popover, PopoverContent, PopoverTrigger } from "@/ui/elements/popover" -import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/ui/elements/hover-card" -import { Badge } from "@/ui/elements/badge" -import { Separator } from "@/ui/elements/separator" +import * as React from 'react' +import { Check, ChevronsUpDown, Search } from 'lucide-react' +import { cn } from '../../lib/utils' +import { Button } from '@/ui/elements/button' +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from '@/ui/elements/command' +import { Popover, PopoverContent, PopoverTrigger } from '@/ui/elements/popover' +import { + HoverCard, + HoverCardContent, + HoverCardTrigger, +} from '@/ui/elements/hover-card' +import { Badge } from '@/ui/elements/badge' +import { Separator } from '@/ui/elements/separator' import { type Element, type ElementCategory, PERIODIC_TABLE_DATA, ELEMENT_CATEGORIES, getElementBySymbol, -} from "../../lib/periodic-table-data" +} from '../../lib/periodic-table-data' -export type PeriodicTableOutputType = "element" | "symbol" | "name" | "atomicNumber" | "atomicMass" +export type PeriodicTableOutputType = + | 'element' + | 'symbol' + | 'name' + | 'atomicNumber' + | 'atomicMass' export interface PeriodicTableProps { - variant?: "compact" | "grid" + variant?: 'compact' | 'grid' outputType?: PeriodicTableOutputType - outputFormat?: "string" | "number" | "object" + outputFormat?: 'string' | 'number' | 'object' onElementSelect?: (value: any) => void value?: string placeholder?: string className?: string disabled?: boolean + enabledSymbols?: string[] } -function ElementCard({ element, onClick }: { element: Element; onClick?: () => void }) { +function ElementCard({ + element, + onClick, + disabled, +}: { + element: Element + onClick?: () => void + disabled?: boolean +}) { const categoryInfo = ELEMENT_CATEGORIES[element.category] - - const truncatedName = element.name.length > 10 ? element.name.slice(0, 9) + "..." : element.name + + const truncatedName = + element.name.length > 10 ? element.name.slice(0, 9) + '...' : element.name return ( - +
-
{element.atomicNumber}
-
{element.symbol}
-
{truncatedName}
+
+ {element.atomicNumber} +
+
+ {element.symbol} +
+
+ {truncatedName} +
{element.atomicMass.toFixed(element.atomicMass % 1 === 0 ? 0 : 1)}
@@ -58,15 +93,17 @@ function ElementCard({ element, onClick }: { element: Element; onClick?: () => v
{element.symbol}

{element.name}

-

Atomic Number: {element.atomicNumber}

+

+ Atomic Number: {element.atomicNumber} +

@@ -84,13 +121,17 @@ function ElementCard({ element, onClick }: { element: Element; onClick?: () => v {element.meltingPoint && (
Melting Point: -

{element.meltingPoint}°C

+

+ {element.meltingPoint}°C +

)} {element.boilingPoint && (
Boiling Point: -

{element.boilingPoint}°C

+

+ {element.boilingPoint}°C +

)} {element.discoveryYear && ( @@ -128,15 +169,15 @@ function CategoryLegend({ return ( onCategoryToggle(category)} > -
+
{info.name} ) @@ -149,57 +190,188 @@ function PeriodicTableGrid({ elements, onElementSelect, selectedCategories, + enabledSymbols, + searchQuery = '', }: { elements: Element[] onElementSelect: (element: Element) => void selectedCategories: Set + enabledSymbols?: string[] + searchQuery?: string }) { - const filteredElements = elements.filter( - (element) => selectedCategories.size === 0 || selectedCategories.has(element.category), + const availableSet = React.useMemo( + () => new Set(enabledSymbols ?? []), + [enabledSymbols] ) + const filteredElements = elements.filter((element) => { + const matchesCategory = + selectedCategories.size === 0 || selectedCategories.has(element.category) + const query = searchQuery.toLowerCase().trim() + const matchesSearch = + query === '' || + element.name.toLowerCase().includes(query) || + element.symbol.toLowerCase().includes(query) || + String(element.atomicNumber).includes(query) + return matchesCategory && matchesSearch + }) // Create the classic periodic table layout // Each array represents the atomic numbers for that period, with null for empty spaces const periodicTableLayout = [ // Period 1 - [1, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, 2], - // Period 2 - [3, 4, null, null, null, null, null, null, null, null, null, null, 5, 6, 7, 8, 9, 10], + [ + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 2, + ], + // Period 2 + [ + 3, + 4, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 5, + 6, + 7, + 8, + 9, + 10, + ], // Period 3 - [11, 12, null, null, null, null, null, null, null, null, null, null, 13, 14, 15, 16, 17, 18], + [ + 11, + 12, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 13, + 14, + 15, + 16, + 17, + 18, + ], // Period 4 [19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36], // Period 5 [37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54], // Period 6 (with lanthanide placeholder) - [55, 56, "La-Lu", 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86], + [ + 55, + 56, + 'La-Lu', + 72, + 73, + 74, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 84, + 85, + 86, + ], // Period 7 (with actinide placeholder) - [87, 88, "Ac-Lr", 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118], + [ + 87, + 88, + 'Ac-Lr', + 104, + 105, + 106, + 107, + 108, + 109, + 110, + 111, + 112, + 113, + 114, + 115, + 116, + 117, + 118, + ], + ] + + const lanthanides = [ + 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, ] - const lanthanides = [57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71] - - const actinides = [89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103] + const actinides = [ + 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, + ] const renderElement = (atomicNumber: number | string | null) => { if (atomicNumber === null) { return
} - + if (typeof atomicNumber === 'string') { return ( -
- {atomicNumber} +
+ + {atomicNumber} +
) } - const element = filteredElements.find((el) => el.atomicNumber === atomicNumber) + const element = filteredElements.find( + (el) => el.atomicNumber === atomicNumber + ) if (!element) { return
} - return onElementSelect(element)} /> + const isDisabled = availableSet.size > 0 && !availableSet.has(element.symbol) + + return ( + onElementSelect(element)} + disabled={isDisabled} + /> + ) } return ( @@ -223,21 +395,17 @@ function PeriodicTableGrid({ La-Lu
{lanthanides.map((atomicNumber) => ( -
- {renderElement(atomicNumber)} -
+
{renderElement(atomicNumber)}
))}
- + {/* Actinides */}
Ac-Lr
{actinides.map((atomicNumber) => ( -
- {renderElement(atomicNumber)} -
+
{renderElement(atomicNumber)}
))}
@@ -246,20 +414,29 @@ function PeriodicTableGrid({ } export function PeriodicTable({ - variant = "compact", - outputType = "element", - outputFormat = "object", + variant = 'compact', + outputType = 'element', + outputFormat = 'object', onElementSelect, value, - placeholder = "Select element...", + placeholder = 'Select element...', className, disabled = false, + enabledSymbols, }: PeriodicTableProps) { const [open, setOpen] = React.useState(false) const [selectedElement, setSelectedElement] = React.useState( - value ? getElementBySymbol(value) || null : null, + value ? getElementBySymbol(value) || null : null + ) + const [selectedCategories, setSelectedCategories] = React.useState< + Set + >(new Set()) + const [searchQuery, setSearchQuery] = React.useState('') + + const availableSet = React.useMemo( + () => new Set(enabledSymbols ?? []), + [enabledSymbols] ) - const [selectedCategories, setSelectedCategories] = React.useState>(new Set()) const handleElementSelect = (element: Element) => { setSelectedElement(element) @@ -269,27 +446,27 @@ export function PeriodicTable({ let outputValue: any switch (outputType) { - case "symbol": + case 'symbol': outputValue = element.symbol break - case "name": + case 'name': outputValue = element.name break - case "atomicNumber": + case 'atomicNumber': outputValue = element.atomicNumber break - case "atomicMass": + case 'atomicMass': outputValue = element.atomicMass break - case "element": + case 'element': default: outputValue = element break } - if (outputFormat === "string") { + if (outputFormat === 'string') { outputValue = String(outputValue) - } else if (outputFormat === "number" && typeof outputValue !== "number") { + } else if (outputFormat === 'number' && typeof outputValue !== 'number') { outputValue = Number(outputValue) || 0 } @@ -307,7 +484,7 @@ export function PeriodicTable({ setSelectedCategories(newCategories) } - if (variant === "compact") { + if (variant === 'compact') { return ( @@ -315,15 +492,15 @@ export function PeriodicTable({ variant="outline" role="combobox" aria-expanded={open} - className={cn("w-full justify-between", className)} + className={cn('w-full justify-between', className)} disabled={disabled} > {selectedElement ? (
{selectedElement.symbol} @@ -341,100 +518,124 @@ export function PeriodicTable({ No element found. - {Object.entries(ELEMENT_CATEGORIES).map(([categoryKey, categoryInfo]) => { - const category = categoryKey as ElementCategory - const categoryElements = PERIODIC_TABLE_DATA.filter((el) => el.category === category) - - if (categoryElements.length === 0) return null - - return ( - - {categoryElements.map((element) => ( - handleElementSelect(element)} - > -
-
- {element.symbol} -
-
-
{element.name}
-
- #{element.atomicNumber} • {element.atomicMass} u + {Object.entries(ELEMENT_CATEGORIES).map( + ([categoryKey, categoryInfo]) => { + const category = categoryKey as ElementCategory + const categoryElements = PERIODIC_TABLE_DATA.filter( + (el) => el.category === category + ) + + if (categoryElements.length === 0) return null + + return ( + + {categoryElements.map((element) => ( + handleElementSelect(element)} + disabled={availableSet.size > 0 && !availableSet.has(element.symbol)} + > +
+
+ {element.symbol} +
+
+
{element.name}
+
+ #{element.atomicNumber} • {element.atomicMass} u +
+
- -
- - ))} - - ) - })} + + ))} + + ) + } + )} ) } - if (variant === "grid") { - return ( - - - + + +
+
+ + setSearchQuery(e.target.value)} + />
- ) : ( - placeholder - )} - - - - -
-
- -

Select an Element

-
- + - -
-
- + +
+
+
) } - return null + return null }