@@ -588,8 +588,9 @@ function StructureCard({
588588
589589 return (
590590 < div
591- className = { `group rounded-2xl transition-all duration-200 hover:shadow-xl hover:shadow-black/30 flex flex-col relative z-0 hover:z-50
592- ${ selected ? 'shadow-blue-500/10 shadow-lg' : '' } ` }
591+ className = { `structure-card group rounded-2xl transition-all duration-200 hover:shadow-xl hover:shadow-black/30 flex flex-col relative z-0 hover:z-50
592+ ${ selected ? 'shadow-blue-500/10 shadow-lg ring-2 ring-blue-500/50' : '' } ` }
593+ data-id = { item . id }
593594 onClick = { e => onSelect ( e , item . id ) }
594595 onDoubleClick = { ( ) => onDoubleClick ?.( item . id ) }
595596 onContextMenu = { e => onContextMenu ( e , 'structure' , item ) }
@@ -602,9 +603,20 @@ function StructureCard({
602603 { hovered && < HoverPreview item = { item } /> }
603604
604605 { /* Inner clipping wrapper */ }
605- < div className = { `flex-1 flex flex-col bg-[var(--bg-header)] border rounded-2xl overflow-hidden transition-colors
606+ < div className = { `flex-1 flex flex-col bg-[var(--bg-header)] border rounded-2xl overflow-hidden transition-colors relative
606607 ${ selected ? 'border-blue-500/60' : 'border-[var(--border-main)] group-hover:border-neutral-600' } ` } >
607608
609+ { /* Global Selection Checkbox */ }
610+ < div className = { `absolute top-2 left-2 z-20
611+ ${ selected ? 'opacity-100' : 'opacity-0 group-hover:opacity-100' }
612+ transition-opacity duration-200` } >
613+ < button onClick = { ( e ) => { e . stopPropagation ( ) ; onSelect ( e , item . id ) ; } }
614+ className = { `p-1.5 rounded-md backdrop-blur-md border shadow-sm transition-all
615+ ${ selected ? 'bg-blue-500/90 border-blue-400 text-white' : 'bg-black/40 border-white/20 text-[var(--text-secondary)] hover:bg-black/60 hover:text-[var(--text-primary)] hover:border-white/40' } ` } >
616+ < CheckSquare className = "w-4 h-4" />
617+ </ button >
618+ </ div >
619+
608620 { /* Gradient strip or RCSB Thumbnail */ }
609621 { hasThumbnail ? (
610622 < div className = "relative h-36 overflow-hidden bg-[var(--input-bg)]" >
@@ -617,26 +629,11 @@ function StructureCard({
617629 el . parentElement ! . style . display = 'none' ;
618630 } }
619631 />
620- < div className = "absolute inset-0 bg-gradient-to-t from-neutral-900/70 via-transparent to-transparent" />
632+ < div className = "absolute inset-0 bg-gradient-to-t from-neutral-900/70 via-transparent to-transparent pointer-events-none " />
621633 < span className = { `absolute top-2 right-2 text-[10px] font-bold px-2 py-0.5 rounded-md border backdrop-blur-sm ${ badge } ` } > { item . file_type } </ span >
622634 </ div >
623635 ) : (
624- < div className = { `h-1 w-full bg-gradient-to-r ${ strip } ` } >
625- < div className = { `absolute top-2 left-2 z-10
626- ${ selected ? 'opacity-100' : 'opacity-0 group-hover:opacity-100' }
627- transition-opacity duration-200` } >
628- < button onClick = { ( e ) => {
629- e . stopPropagation ( ) ;
630- onSelect ( e , item . id ) ;
631- } }
632- className = { `p-1.5 rounded-md backdrop-blur-md border shadow-sm transition-all
633- ${ selected
634- ? 'bg-blue-500/90 border-blue-400 text-white'
635- : 'bg-black/40 border-white/20 text-[var(--text-secondary)] hover:bg-black/60 hover:text-[var(--text-primary)] hover:border-white/40' } `} >
636- < CheckSquare className = "w-4 h-4" />
637- </ button >
638- </ div >
639- </ div >
636+ < div className = { `h-1 w-full bg-gradient-to-r ${ strip } ` } />
640637 ) }
641638
642639 < div className = { `p-5 flex flex-col flex-1` } >
@@ -966,9 +963,61 @@ export const MyStructures = () => {
966963 const [ isWindowDragOver , setIsWindowDragOver ] = useState ( false ) ;
967964 const [ dragOverBreadcrumb , setDragOverBreadcrumb ] = useState < string | null | undefined > ( undefined ) ;
968965
969- // Selection
966+ // Selection & Lasso
970967 const [ selected , setSelected ] = useState < Set < string > > ( new Set ( ) ) ;
971968 const [ lastSelectedId , setLastSelectedId ] = useState < string | null > ( null ) ;
969+ const [ lasso , setLasso ] = useState < { startX : number , startY : number , currX : number , currY : number } | null > ( null ) ;
970+ const [ lassoSelected , setLassoSelected ] = useState < Set < string > > ( new Set ( ) ) ;
971+ const gridRef = useRef < HTMLDivElement > ( null ) ;
972+
973+ // Lasso Engine Intersections
974+ useEffect ( ( ) => {
975+ if ( ! lasso || ! gridRef . current ) return ;
976+ const rect = {
977+ left : Math . min ( lasso . startX , lasso . currX ) ,
978+ right : Math . max ( lasso . startX , lasso . currX ) ,
979+ top : Math . min ( lasso . startY , lasso . currY ) ,
980+ bottom : Math . max ( lasso . startY , lasso . currY )
981+ } ;
982+
983+ const cards = gridRef . current . querySelectorAll ( '.structure-card' ) ;
984+ const newLasso = new Set < string > ( ) ;
985+
986+ cards . forEach ( card => {
987+ const cardRect = card . getBoundingClientRect ( ) ;
988+ // A simple overlap test
989+ if (
990+ rect . left < cardRect . right &&
991+ rect . right > cardRect . left &&
992+ rect . top < cardRect . bottom &&
993+ rect . bottom > cardRect . top
994+ ) {
995+ const id = card . getAttribute ( 'data-id' ) ;
996+ if ( id ) newLasso . add ( id ) ;
997+ }
998+ } ) ;
999+
1000+ // Only update if changed to prevent thrashing
1001+ setLassoSelected ( prev => {
1002+ if ( prev . size !== newLasso . size ) return newLasso ;
1003+ for ( let id of newLasso ) if ( ! prev . has ( id ) ) return newLasso ;
1004+ return prev ;
1005+ } ) ;
1006+ } , [ lasso ?. currX , lasso ?. currY , lasso ?. startX , lasso ?. startY ] ) ;
1007+
1008+ useEffect ( ( ) => {
1009+ const handlePointerUp = ( ) => {
1010+ if ( lasso ) {
1011+ if ( lassoSelected . size > 0 ) {
1012+ setSelected ( prev => new Set ( [ ...prev , ...lassoSelected ] ) ) ;
1013+ }
1014+ setLasso ( null ) ;
1015+ setLassoSelected ( new Set ( ) ) ;
1016+ }
1017+ } ;
1018+ window . addEventListener ( 'pointerup' , handlePointerUp ) ;
1019+ return ( ) => window . removeEventListener ( 'pointerup' , handlePointerUp ) ;
1020+ } , [ lasso , lassoSelected ] ) ;
9721021
9731022 // Inspector Sidebar
9741023 const [ showInspector , setShowInspector ] = useState ( ( ) => localStorage . getItem ( 'quercus_show_inspector' ) === 'true' ) ;
@@ -1790,7 +1839,18 @@ export const MyStructures = () => {
17901839
17911840 { /* Grid */ }
17921841 { ! loading && viewMode === 'grid' && (
1793- < div className = "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-3 lg:gap-4" >
1842+ < div
1843+ ref = { gridRef }
1844+ className = "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-3 lg:gap-4 select-none touch-none"
1845+ onPointerDown = { e => {
1846+ // Only activate lasso if clicking explicitly on the blank gaps
1847+ if ( ( e . target as HTMLElement ) . closest ( 'button, a, input, .structure-card' ) ) return ;
1848+ setLasso ( { startX : e . clientX , startY : e . clientY , currX : e . clientX , currY : e . clientY } ) ;
1849+ } }
1850+ onPointerMove = { e => {
1851+ if ( lasso ) setLasso ( prev => ( { ...prev ! , currX : e . clientX , currY : e . clientY } ) ) ;
1852+ } }
1853+ >
17941854 { /* Render Subfolders */ }
17951855 { activeSubfolders . map ( ( sub : Collection ) => (
17961856 < FolderCard key = { sub . id } collection = { sub } count = { collectionCounts [ sub . id ] || 0 } onOpen = { ( ) => setActiveCollection ( sub . id ) } onDropStructure = { handleDropMove } onContextMenu = { handleContextMenu } previews = { structures . filter ( s => s . collection_id === sub . id ) . slice ( 0 , 3 ) } />
@@ -1799,7 +1859,7 @@ export const MyStructures = () => {
17991859 { /* Render Structures */ }
18001860 { filtered . map ( item => (
18011861 < StructureCard key = { item . id } item = { item }
1802- selected = { selected . has ( item . id ) } onSelect = { toggleSelect }
1862+ selected = { selected . has ( item . id ) || lassoSelected . has ( item . id ) } onSelect = { toggleSelect }
18031863 onToggleStar = { handleToggleStar } onDelete = { handleDelete }
18041864 onRename = { handleRename } onNotesChange = { handleNotesChange }
18051865 onTagsChange = { handleTagsChange } onDuplicate = { handleDuplicate }
@@ -1941,11 +2001,24 @@ export const MyStructures = () => {
19412001 />
19422002 ) }
19432003
2004+ { /* Lasso Drag Box (Overlay) */ }
2005+ { lasso && (
2006+ < div
2007+ className = "fixed z-[100] bg-blue-500/20 border border-blue-500/50 pointer-events-none"
2008+ style = { {
2009+ left : Math . min ( lasso . startX , lasso . currX ) ,
2010+ top : Math . min ( lasso . startY , lasso . currY ) ,
2011+ width : Math . abs ( lasso . currX - lasso . startX ) ,
2012+ height : Math . abs ( lasso . currY - lasso . startY )
2013+ } }
2014+ />
2015+ ) }
2016+
19442017 { /* Bulk action floating bar */ }
19452018 { selected . size > 0 && (
1946- < div className = "fixed bottom-6 left-1/2 -translate-x-1/2 z-50 flex items-center gap-3 bg-[var(--input-bg)] border border-neutral-600 rounded-2xl px-5 py-3 shadow-2xl shadow-black/50" >
1947- < span className = "text-sm font-medium text-[var(--text-primary)]" > { selected . size } selected</ span >
1948- < div className = "w-px h-4 bg-neutral-600 " />
2019+ < div className = "fixed bottom-6 left-1/2 -translate-x-1/2 z-50 flex items-center gap-3 bg-[var(--input-bg)]/90 backdrop-blur-xl border border-white/10 rounded-full px-5 py-3 shadow-2xl shadow-black/50 animate-in slide-in-from-bottom-8 fade-in duration-300 ease-out " >
2020+ < span className = "text-sm font-semibold text-[var(--text-primary)]" > { selected . size } selected</ span >
2021+ < div className = "w-px h-5 bg-white/10 mx-1 " />
19492022 { activeCollection === '__trash__' ? (
19502023 < >
19512024 < button onClick = { handleBulkRestore }
0 commit comments