@@ -36,7 +36,7 @@ import {
3636 Wrench ,
3737 X
3838} from 'lucide-react' ;
39- import type { RepresentationType , ColoringType , ChainInfo , Snapshot , Movie , ColorPalette , PDBMetadata , CustomColorRule , Measurement } from '../types' ;
39+ import type { RepresentationType , ColoringType , ChainInfo , Snapshot , Movie , ColorPalette , PDBMetadata , CustomColorRule , CustomStyleRule , Measurement } from '../types' ;
4040import type { DataSource } from '../utils/pdbUtils' ;
4141import type { HistoryItem } from '../hooks/useHistory' ;
4242import { formatChemicalId } from '../utils/pdbUtils' ;
@@ -397,6 +397,10 @@ interface ControlsProps {
397397 visualizerEngine ?: 'ngl' | 'molstar' ;
398398 setVisualizerEngine ?: ( engine : 'ngl' | 'molstar' ) => void ;
399399 onResetCamera ?: ( ) => void ;
400+
401+ // Custom Residue Styles
402+ customStyles ?: CustomStyleRule [ ] ;
403+ setCustomStyles ?: ( styles : CustomStyleRule [ ] | ( ( prev : CustomStyleRule [ ] ) => CustomStyleRule [ ] ) ) => void ;
400404}
401405
402406export const Controls : React . FC < ControlsProps > = ( {
@@ -416,6 +420,8 @@ export const Controls: React.FC<ControlsProps> = ({
416420 setCustomColors,
417421 chainStyles, // Destructure
418422 setChainStyle, // Destructure
423+ customStyles,
424+ setCustomStyles,
419425
420426 chains,
421427 ligands,
@@ -501,6 +507,23 @@ export const Controls: React.FC<ControlsProps> = ({
501507 const [ selectedChainForStyle , setSelectedChainForStyle ] = useState < string > ( "" ) ;
502508 const [ selectedStyleForChain , setSelectedStyleForChain ] = useState < RepresentationType > ( "cartoon" ) ;
503509
510+ // Custom Residue Style State
511+ const [ customResStyleChain , setCustomResStyleChain ] = useState < string > ( "All" ) ;
512+ const [ customResStyleRange , setCustomResStyleRange ] = useState < string > ( "" ) ;
513+ const [ customResStyleType , setCustomResStyleType ] = useState < RepresentationType > ( "spacefill" ) ;
514+
515+ const handleAddCustomStyle = ( ) => {
516+ if ( ! setCustomStyles || ! customResStyleRange ) return ;
517+ const newRule : CustomStyleRule = {
518+ id : crypto . randomUUID ( ) ,
519+ chain : customResStyleChain ,
520+ residues : customResStyleRange ,
521+ style : customResStyleType
522+ } ;
523+ setCustomStyles ( ( prev : CustomStyleRule [ ] ) => [ ...prev , newRule ] ) ;
524+ setCustomResStyleRange ( '' ) ;
525+ } ;
526+
504527 // State for Residue-Specific Coloring UI
505528
506529 // State for Screenshot Modal
@@ -1464,23 +1487,118 @@ export const Controls: React.FC<ControlsProps> = ({
14641487
14651488 { /* Active Chain Styles List */ }
14661489 { Object . keys ( chainStyles ) . length > 0 && (
1467- < div className = "space-y-1 pt-2 border-t border-dashed border-neutral-200 dark:border-neutral-800" >
1468- < div className = "flex justify-between items-center mb-2" >
1469- < span className = { `text-[9px] font-bold uppercase tracking-wider ${ subtleText } ` } > Overrides ({ Object . keys ( chainStyles ) . length } )</ span >
1470- </ div >
1471- < div className = "space-y-1.5 max-h-32 overflow-y-auto pr-1 scrollbar-thin" >
1490+ < div className = "space-y-1 pt-1" >
1491+ < div className = { `text-[9px] font-bold uppercase tracking-wider ${ subtleText } opacity-70` } > Active Styles</ div >
1492+ < div className = "space-y-1" >
14721493 { Object . entries ( chainStyles ) . map ( ( [ chain , style ] ) => (
1473- < div key = { chain } className = { `group flex items-center justify-between text-xs p-2 rounded-lg border transition-all ${ isLightMode ? 'bg-white border-neutral-200 hover:border-blue-300 ' : 'bg-white/5 border-transparent hover:bg- white/10 ' } ` } >
1474- < div className = "flex items-center gap-3 " >
1475- < span className = { `font-mono font-bold ${ isLightMode ? 'text-black ' : 'text-white ' } ` } > :{ chain } </ span >
1476- < span className = { ` text-[10px] opacity-70 uppercase tracking-wide ${ isLightMode ? 'text-neutral-600' : 'text-neutral-400' } ` } > { style } </ span >
1494+ < div key = { chain } className = { `flex items-center justify-between p-2 rounded border ${ isLightMode ? 'bg-white border-neutral-200' : 'bg-neutral-800 border-white/5 ' } ` } >
1495+ < div className = "flex items-center gap-2 " >
1496+ < span className = { `px-1.5 py-0.5 rounded text-[10px] font-mono font-bold ${ isLightMode ? 'bg-neutral-100 text-neutral-700 ' : 'bg-neutral-700 text-neutral-300 ' } ` } > :{ chain } </ span >
1497+ < span className = " text-[10px] capitalize" > { style } </ span >
14771498 </ div >
14781499 < button
14791500 onClick = { ( ) => setChainStyle ( chain , null ) }
1480- className = "opacity-0 group-hover:opacity-100 text-neutral-400 hover:text-red-500 transition-all p-1 hover:bg-red-500/10 rounded"
1481- title = "Remove Override"
1501+ className = "p-1 hover:bg-red-500/20 text-neutral-400 hover:text-red-500 rounded transition-colors"
14821502 >
1483- < X className = "w-3 h-3" />
1503+ < X size = { 12 } />
1504+ </ button >
1505+ </ div >
1506+ ) ) }
1507+ </ div >
1508+ </ div >
1509+ ) }
1510+
1511+ </ div >
1512+ </ div >
1513+ ) }
1514+
1515+ { /* Custom Residue/Range Styles UI */ }
1516+ { setCustomStyles && customStyles && (
1517+ < div className = "col-span-2 pt-2 border-t border-white/5 space-y-2" >
1518+ < div className = { `w-full text-[10px] font-bold uppercase tracking-wider ${ subtleText } opacity-80 mb-1` } >
1519+ Custom Residue Styles
1520+ </ div >
1521+
1522+ < div className = { `p-3 rounded-xl border space-y-3 ${ isLightMode ? 'bg-neutral-50/50 border-neutral-200' : 'bg-black/20 border-white/5' } ` } >
1523+ < div className = "flex gap-2" >
1524+ < div className = "w-[80px]" >
1525+ < label className = { `text-[9px] font-bold uppercase tracking-wider mb-1 block ${ subtleText } opacity-70` } > Chain</ label >
1526+ < div className = { `relative flex items-center rounded-lg border transition-all ${ inputBg } ` } >
1527+ < select
1528+ value = { customResStyleChain }
1529+ onChange = { ( e ) => setCustomResStyleChain ( e . target . value ) }
1530+ className = "w-full appearance-none bg-transparent py-1.5 pl-2 pr-4 text-xs font-mono outline-none"
1531+ >
1532+ < option value = "All" > All</ option >
1533+ { chains . map ( c => (
1534+ < option key = { c . name } value = { c . name } > :{ c . name } </ option >
1535+ ) ) }
1536+ </ select >
1537+ < ChevronDown className = "absolute right-1 top-1/2 -translate-y-1/2 w-3 h-3 opacity-50 pointer-events-none" />
1538+ </ div >
1539+ </ div >
1540+ < div className = "flex-1" >
1541+ < label className = { `text-[9px] font-bold uppercase tracking-wider mb-1 block ${ subtleText } opacity-70` } > Residues</ label >
1542+ < input
1543+ type = "text"
1544+ value = { customResStyleRange }
1545+ onChange = { ( e ) => setCustomResStyleRange ( e . target . value ) }
1546+ placeholder = "e.g. 50-60"
1547+ className = { `w-full py-1.5 px-2 rounded-lg border text-xs outline-none transition-all ${ inputBg } ` }
1548+ />
1549+ </ div >
1550+ </ div >
1551+
1552+ < div className = "flex gap-2 items-end" >
1553+ < div className = "flex-1" >
1554+ < label className = { `text-[9px] font-bold uppercase tracking-wider mb-1 block ${ subtleText } opacity-70` } > Style</ label >
1555+ < div className = { `relative flex items-center rounded-lg border transition-all ${ inputBg } ` } >
1556+ < select
1557+ value = { customResStyleType }
1558+ onChange = { ( e ) => setCustomResStyleType ( e . target . value as RepresentationType ) }
1559+ className = "w-full appearance-none bg-transparent py-1.5 pl-2 pr-6 text-xs outline-none"
1560+ >
1561+ < option value = "cartoon" > Cartoon</ option >
1562+ < option value = "ball+stick" > Ball & Stick </ option >
1563+ < option value = "licorice" > Licorice</ option >
1564+ < option value = "spacefill" > Spacefill</ option >
1565+ < option value = "surface" > Surface</ option >
1566+ < option value = "line" > Line</ option >
1567+ </ select >
1568+ < ChevronDown className = "absolute right-2 top-1/2 -translate-y-1/2 w-3 h-3 opacity-50 pointer-events-none" />
1569+ </ div >
1570+ </ div >
1571+ < button
1572+ onClick = { handleAddCustomStyle }
1573+ disabled = { ! customResStyleRange }
1574+ className = "h-[30px] px-3 bg-blue-600 hover:bg-blue-500 disabled:opacity-50 disabled:cursor-not-allowed text-white rounded-lg text-xs font-bold transition-all shadow-sm active:scale-95 flex items-center justify-center shrink-0"
1575+ >
1576+ < Plus size = { 14 } className = "mr-1" />
1577+ Add
1578+ </ button >
1579+ </ div >
1580+
1581+ { /* Active Custom Styles List */ }
1582+ { customStyles . length > 0 && (
1583+ < div className = "space-y-1 pt-1" >
1584+ < div className = { `text-[9px] font-bold uppercase tracking-wider ${ subtleText } opacity-70` } > Active Rules</ div >
1585+ < div className = "space-y-1 max-h-[100px] overflow-y-auto custom-scrollbar" >
1586+ { customStyles . map ( ( rule : CustomStyleRule ) => (
1587+ < div key = { rule . id } className = { `flex items-center justify-between p-2 rounded border ${ isLightMode ? 'bg-white border-neutral-200' : 'bg-neutral-800 border-white/5' } ` } >
1588+ < div className = "flex flex-col gap-0.5" >
1589+ < div className = "flex items-center gap-2" >
1590+ < span className = { `px-1.5 py-0.5 rounded text-[9px] font-mono font-bold ${ isLightMode ? 'bg-neutral-100 text-neutral-700' : 'bg-neutral-700 text-neutral-300' } ` } >
1591+ { rule . chain === 'All' ? '*' : `:${ rule . chain } ` }
1592+ </ span >
1593+ < span className = "text-[10px] opacity-70 font-mono" > { rule . residues } </ span >
1594+ </ div >
1595+ < span className = "text-[9px] capitalize text-blue-500 font-medium" > { rule . style } </ span >
1596+ </ div >
1597+ < button
1598+ onClick = { ( ) => setCustomStyles ! ( ( prev : CustomStyleRule [ ] ) => prev . filter ( ( r : CustomStyleRule ) => r . id !== rule . id ) ) }
1599+ className = "p-1 hover:bg-red-500/20 text-neutral-400 hover:text-red-500 rounded transition-colors"
1600+ >
1601+ < Trash2 size = { 12 } />
14841602 </ button >
14851603 </ div >
14861604 ) ) }
@@ -1491,6 +1609,8 @@ export const Controls: React.FC<ControlsProps> = ({
14911609 </ div >
14921610 ) }
14931611
1612+
1613+
14941614 </ div >
14951615
14961616 { /* Tools: Publication & Measure */ }
0 commit comments