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;
}