11'use client' ;
22
3- import { Fragment } from 'react' ;
3+ import { Fragment , useEffect , useMemo , useState } from 'react' ;
44import { Dialog , Transition } from '@headlessui/react' ;
55
66/*
@@ -29,6 +29,19 @@ interface ImageModalProps {
2929 /** Tailwind class controlling Dialog width (e.g. max-w-2xl) */
3030 maxWidthClass ?: string ;
3131}
32+ function buildAnnulusPath (
33+ original : string ,
34+ r_in : number | string ,
35+ r_out : number | string ,
36+ withLabel = false
37+ ) {
38+ const clean = ( v : number | string ) => String ( v ) . trim ( ) . replace ( / \. 0 + $ / , '' ) ;
39+ const label = withLabel ? '_label' : '' ;
40+ return original . replace (
41+ / ( \. (?: p n g | j p e ? g | w e b p ) ) (? = ( $ | [ ? # ] ) ) / i,
42+ `_rin${ clean ( r_in ) } _rout${ clean ( r_out ) } ${ label } $1`
43+ ) ;
44+ }
3245
3346export function ImageModal ( {
3447 isOpen,
@@ -38,29 +51,85 @@ export function ImageModal({
3851 title = 'Details' ,
3952 maxWidthClass = 'max-w-2xl' ,
4053} : ImageModalProps ) {
54+ const [ previewSrc , setPreviewSrc ] = useState < string | undefined > ( imgSrc ) ;
55+ const [ focusedRow , setFocusedRow ] = useState < number | null > ( null ) ;
56+ const [ labelOn , setLabelOn ] = useState ( false ) ;
57+ const [ selectedPair , setSelectedPair ] = useState < { r_in : string ; r_out : string } | null > ( null ) ;
58+
59+ useEffect ( ( ) => {
60+ if ( isOpen ) {
61+ setPreviewSrc ( imgSrc ) ;
62+ setFocusedRow ( null ) ;
63+ setSelectedPair ( null ) ;
64+ // keep previous labelOn state (user preference)
65+ }
66+ } , [ isOpen , imgSrc ] ) ;
67+
68+ const baseSrc = useMemo ( ( ) => imgSrc ?? '' , [ imgSrc ] ) ;
69+
70+ function showAnnulus ( r_in : string , r_out : string , rowIndex : number ) {
71+ if ( ! baseSrc ) return ;
72+ setSelectedPair ( { r_in, r_out } ) ;
73+ setPreviewSrc ( buildAnnulusPath ( baseSrc , r_in , r_out , labelOn ) ) ;
74+ setFocusedRow ( rowIndex ) ;
75+ }
76+
77+ function showOriginal ( ) {
78+ setPreviewSrc ( baseSrc ) ;
79+ setFocusedRow ( null ) ;
80+ setSelectedPair ( null ) ;
81+ }
82+
83+ function toggleLabel ( ) {
84+ setLabelOn ( prev => {
85+ const next = ! prev ;
86+ // Only affects annulus view; original remains unchanged
87+ if ( focusedRow !== null && selectedPair && baseSrc ) {
88+ setPreviewSrc ( buildAnnulusPath ( baseSrc , selectedPair . r_in , selectedPair . r_out , next ) ) ;
89+ }
90+ return next ;
91+ } ) ;
92+ }
4193 return (
4294 < Transition show = { isOpen } as = { Fragment } >
4395 < Dialog onClose = { onClose } className = "relative z-99999" >
4496 < div className = "fixed inset-0 bg-black/30" aria-hidden = "true" />
4597 < div className = "fixed inset-0 flex items-center justify-center p-4" >
46- < Dialog . Panel
47- className = { `${ maxWidthClass } w-full rounded-xl bg-white p-6 shadow-xl` }
48- >
49- { imgSrc && (
50- < img src = { imgSrc } alt = "preview" className = "w-full mb-4 rounded" />
98+ < Dialog . Panel className = { `${ maxWidthClass } w-full rounded-xl bg-white p-6 shadow-xl` } >
99+ { /* Label toggle row (separate line, top-right) */ }
100+ < div className = "mb-2 flex items-center justify-end" >
101+ < button
102+ type = "button"
103+ onClick = { toggleLabel }
104+ className = { `px-3 py-1 rounded-full text-sm shadow
105+ ${ labelOn ? 'bg-indigo-600 text-white' : 'bg-gray-200 text-gray-800 hover:bg-gray-300' } ` }
106+ title = "Toggle label on annulus images"
107+ >
108+ Label: { labelOn ? 'ON' : 'OFF' }
109+ </ button >
110+ </ div >
111+
112+ { /* Image below the toggle */ }
113+ { previewSrc && (
114+ < img src = { previewSrc } alt = "preview" className = "w-full mb-4 rounded" />
51115 ) }
116+
52117 { title && (
53118 < Dialog . Title className = "text-lg font-semibold mb-2 text-black" >
54119 { title }
55120 </ Dialog . Title >
56121 ) }
122+
57123 { details && (
58124 < div className = "mt-4 text-m space-y-4 text-black" >
59- { /* Metadata Summary */ }
60125 < div className = "space-y-1" >
61126 < p > < span className = "font-semibold" > Label:</ span > { details . label } </ p >
62- { details . phase && < p > < span className = "font-semibold" > Phase:</ span > { details . phase } </ p > }
63- { details . mjd && < p > < span className = "font-semibold" > MJD:</ span > { details . mjd } </ p > }
127+ { details . phase !== undefined && (
128+ < p > < span className = "font-semibold" > Phase:</ span > { details . phase } </ p >
129+ ) }
130+ { details . mjd !== undefined && (
131+ < p > < span className = "font-semibold" > MJD:</ span > { details . mjd } </ p >
132+ ) }
64133 { details . filename && (
65134 < p >
66135 < span className = "font-semibold" > Filename:</ span > { ' ' }
@@ -69,7 +138,6 @@ export function ImageModal({
69138 ) }
70139 </ div >
71140
72- { /* Table of Values */ }
73141 { Array . isArray ( details . rows ) && details . rows . length > 0 && (
74142 < div className = "border-t pt-3 overflow-x-auto" >
75143 < table className = "min-w-full text-sm border border-gray-300 rounded overflow-hidden" >
@@ -80,37 +148,63 @@ export function ImageModal({
80148 < th className = "px-3 py-2 text-left" > r_in</ th >
81149 < th className = "px-3 py-2 text-left" > r_out</ th >
82150 < th className = "px-3 py-2 text-left" > y</ th >
151+ < th className = "px-3 py-2 text-right" > Annulus</ th >
83152 </ tr >
84153 </ thead >
85154 < tbody >
86- { details . rows . map ( ( row , idx ) => (
87- < tr key = { idx } className = "border-t hover:bg-gray-50" >
88- < td className = "px-3 py-1" >
89- { row . color ? (
90- < div
91- className = "w-4 h-4 rounded-full border border-gray-300"
92- style = { { backgroundColor : row . color } }
93- title = { row . color }
94- />
95- ) : (
96- < div className = "w-4 h-4 rounded-full border border-gray-200 bg-white" title = "No color" />
97- ) }
98- </ td >
99- < td className = "px-3 py-1 font-mono text-black" > { row . epoch } </ td >
100- < td className = "px-3 py-1 font-mono text-black" > { row . r_in } </ td >
101- < td className = "px-3 py-1 font-mono text-black" > { row . r_out } </ td >
102- < td className = "px-3 py-1 text-black font-semibold" > { row . y . toFixed ( 2 ) } </ td >
103- { /* <td className="px-3 py-1 text-blue-600 font-semibold">{row.y.toFixed(2)}</td> */ }
104- </ tr >
105- ) ) }
155+ { details . rows . map ( ( row , idx ) => {
156+ const isFocused = focusedRow === idx ;
157+ return (
158+ < tr
159+ key = { idx }
160+ className = { `border-t ${ isFocused ? 'bg-indigo-50 ring-1 ring-indigo-300' : 'hover:bg-gray-50' } ` }
161+ >
162+ < td className = "px-3 py-1" >
163+ { row . color ? (
164+ < div
165+ className = "w-4 h-4 rounded-full border border-gray-300"
166+ style = { { backgroundColor : row . color } }
167+ title = { row . color }
168+ />
169+ ) : (
170+ < div className = "w-4 h-4 rounded-full border border-gray-200 bg-white" title = "No color" />
171+ ) }
172+ </ td >
173+ < td className = "px-3 py-1 font-mono text-black" > { row . epoch } </ td >
174+ < td className = "px-3 py-1 font-mono text-black" > { row . r_in } </ td >
175+ < td className = "px-3 py-1 font-mono text-black" > { row . r_out } </ td >
176+ < td className = "px-3 py-1 text-black font-semibold" > { row . y . toFixed ( 2 ) } </ td >
177+ < td className = "px-3 py-1 text-right" >
178+ { isFocused ? (
179+ < button
180+ type = "button"
181+ onClick = { showOriginal }
182+ className = "px-2 py-1 rounded bg-gray-800 text-white hover:bg-gray-700"
183+ title = "Back to original image"
184+ >
185+ Original
186+ </ button >
187+ ) : (
188+ < button
189+ type = "button"
190+ onClick = { ( ) => showAnnulus ( row . r_in , row . r_out , idx ) }
191+ className = "px-2 py-1 rounded bg-blue-600 text-white hover:bg-blue-700"
192+ title = "Show annulus image"
193+ >
194+ Annulus
195+ </ button >
196+ ) }
197+ </ td >
198+ </ tr >
199+ ) ;
200+ } ) }
106201 </ tbody >
107202 </ table >
108203 </ div >
109204 ) }
110205 </ div >
111206 ) }
112207
113-
114208 < button
115209 onClick = { onClose }
116210 className = "mt-6 px-4 py-2 rounded bg-gray-800 text-white"
@@ -122,4 +216,4 @@ export function ImageModal({
122216 </ Dialog >
123217 </ Transition >
124218 ) ;
125- }
219+ }
0 commit comments