Spaces:
Running
Running
File size: 3,441 Bytes
2abf76f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | import {useEffect,useMemo,useRef,useState} from 'react';
import {createPortal} from 'react-dom';
import {Check,Copy,Download,Maximize2,Minimize2,X} from 'lucide-react';
import {highlightPython} from './python-highlight';
type Props={code:string;error:string;url:string;id:string;version:string;onClose:()=>void};
export default function CodePanel({code,error,url,id,version,onClose}:Props){
const [copied,setCopied]=useState(false),[copyError,setCopyError]=useState(false),[expanded,setExpanded]=useState(false);
const panel=useRef<HTMLElement>(null),expandButton=useRef<HTMLButtonElement>(null);
const collapse=()=>{setExpanded(false);requestAnimationFrame(()=>expandButton.current?.focus());};
useEffect(()=>{
if(!expanded)return;
const app=document.querySelector<HTMLElement>('.app-shell'),wasInert=app?.inert;
if(app)app.inert=true;
panel.current?.querySelector<HTMLElement>('pre,button')?.focus();
const key=(e:KeyboardEvent)=>{
if(e.key==='Escape'){e.preventDefault();collapse();}
if(e.key==='Tab'){
const nodes=Array.from(panel.current?.querySelectorAll<HTMLElement>('button:not(:disabled),a[href],pre[tabindex]')??[]),first=nodes[0],last=nodes.at(-1);
if(e.shiftKey&&document.activeElement===first){e.preventDefault();last?.focus();}
else if(!e.shiftKey&&document.activeElement===last){e.preventDefault();first?.focus();}
}
};
document.addEventListener('keydown',key);return()=>{document.removeEventListener('keydown',key);if(app)app.inert=wasInert??false;};
},[expanded]);
const html=useMemo(()=>highlightPython(code),[code]);
const copy=async()=>{try{await navigator.clipboard.writeText(code);setCopied(true);setCopyError(false);}catch{setCopied(false);setCopyError(true);}};
const content=<section ref={panel} id="selection-details" className={`dock-panel selection-details code-panel ${expanded?'code-expanded':''}`} aria-label="Python program" role={expanded?'dialog':undefined} aria-modal={expanded||undefined} data-ui>
<header className="code-heading"><div><h2>The program</h2><p>Python · sand3d {version}{code?` · ${code.split('\n').length} lines`:''}</p></div>
<div className="code-actions">
<button className="icon-button" aria-label={copied?'Python copied':'Copy Python'} title={copied?'Copied':'Copy Python'} disabled={!code} onClick={copy}>{copied?<Check size={16}/>:<Copy size={16}/>}</button>
<a className="icon-button" aria-label="Download Python" title="Download Python" href={url} download={`${id}.py`}><Download size={16}/></a>
<button ref={expandButton} className="icon-button" aria-label={expanded?'Collapse code':'Expand code'} title={expanded?'Collapse code':'Expand code'} onClick={()=>expanded?collapse():setExpanded(true)}>{expanded?<Minimize2 size={16}/>:<Maximize2 size={16}/>}</button>
<button className="icon-button" aria-label="Close Python program" title="Close Python program" onClick={onClose}><X size={16}/></button>
</div>
</header>
{copyError&&<p role="alert">Clipboard unavailable. Select the code below to copy it.</p>}
{error?<p role="alert">{error}</p>:code?<pre tabIndex={0} aria-label="Python source code"><span className="code-gutter" aria-hidden="true">{code.split('\n').map((_,i)=>i+1).join('\n')}</span><code dangerouslySetInnerHTML={{__html:html}}/></pre>:<p role="status">Loading program…</p>}
</section>;
return expanded?createPortal(<div className="code-scrim">{content}</div>,document.body):content;
}
|