Skip to content

Commit e1a5423

Browse files
AmirMohammad CheraghaliAmirMohammad Cheraghali
authored andcommitted
feat: Add custom residue styling functionality
1 parent a102e91 commit e1a5423

4 files changed

Lines changed: 193 additions & 22 deletions

File tree

src/components/Controls.tsx

Lines changed: 133 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -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';
4040
import type { DataSource } from '../utils/pdbUtils';
4141
import type { HistoryItem } from '../hooks/useHistory';
4242
import { 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

402406
export 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

Comments
 (0)