Download frontend/src/components/ui.tsx from omaib/MuSProt: direct link, hf CLI and curl.
- Browser
- Download file 7.79 kB
-
https://huggingface.co/spaces/omaib/MuSProt/resolve/main/frontend/src/components/ui.tsx
- Command line
-
hf download hf://spaces/omaib/MuSProt/frontend/src/components/ui.tsx
-
curl -L -o ui.tsx https://huggingface.co/spaces/omaib/MuSProt/resolve/main/frontend/src/components/ui.tsx
7.79 kB
| import { useState, type ReactNode } from 'react'; | |
| import { Link } from 'react-router-dom'; | |
| import { AlertTriangle, Check, ChevronLeft, ChevronRight, Copy, ExternalLink as ExtIcon, Inbox } from 'lucide-react'; | |
| import { FIDELITY_INFO, fidelityColor, fidelityKey } from '../lib/fidelity'; | |
| import { chainLabel, chainPath } from '../lib/format'; | |
| export function Section({ | |
| id, | |
| title, | |
| kicker, | |
| actions, | |
| children, | |
| className = '', | |
| flush = false, | |
| }: { | |
| id?: string; | |
| title?: ReactNode; | |
| kicker?: ReactNode; | |
| actions?: ReactNode; | |
| children: ReactNode; | |
| className?: string; | |
| flush?: boolean; | |
| }) { | |
| return ( | |
| <section id={id} className={`card ${flush ? 'card--flush' : ''} ${className}`}> | |
| {(title || actions) && ( | |
| <header className="card__head"> | |
| <div className="card__titles"> | |
| {title && <h2 className="card__title">{title}</h2>} | |
| {kicker && <p className="card__kicker">{kicker}</p>} | |
| </div> | |
| {actions && <div className="card__actions">{actions}</div>} | |
| </header> | |
| )} | |
| {children} | |
| </section> | |
| ); | |
| } | |
| export function Badge({ | |
| children, | |
| tone = 'neutral', | |
| title, | |
| }: { | |
| children: ReactNode; | |
| tone?: 'neutral' | 'accent' | 'outline'; | |
| title?: string; | |
| }) { | |
| return ( | |
| <span className={`badge badge--${tone}`} title={title}> | |
| {children} | |
| </span> | |
| ); | |
| } | |
| export function Swatch({ color, shape = 'square' }: { color: string; shape?: 'square' | 'dot' }) { | |
| return <span className={`swatch swatch--${shape}`} style={{ background: color }} aria-hidden />; | |
| } | |
| export function FidelityBadge({ value }: { value: string | null | undefined }) { | |
| const key = fidelityKey(value); | |
| const info = FIDELITY_INFO[key]; | |
| return ( | |
| <span className="fid" title={`${info.label}: ${info.range}`}> | |
| <Swatch color={fidelityColor(key)} /> | |
| {info.label} | |
| </span> | |
| ); | |
| } | |
| export function LigandBadge({ status }: { status: string | null | undefined }) { | |
| if (!status) return <span className="muted">—</span>; | |
| return ( | |
| <span | |
| className="fid" | |
| title={ | |
| status === 'holo' | |
| ? 'Holo: at least one small-molecule binder on this chain (DNA/RNA not counted)' | |
| : 'Apo: no small-molecule binder on this chain (DNA/RNA not counted)' | |
| } | |
| > | |
| <Swatch color={status === 'holo' ? 'var(--holo)' : 'var(--apo)'} shape="dot" /> | |
| {status === 'holo' ? 'Holo' : 'Apo'} | |
| </span> | |
| ); | |
| } | |
| export function ChainLink({ pdb, chain, strong = false }: { pdb: string; chain: string; strong?: boolean }) { | |
| return ( | |
| <Link to={chainPath(pdb, chain)} className={`chain-link ${strong ? 'chain-link--strong' : ''}`}> | |
| {chainLabel(pdb, chain)} | |
| </Link> | |
| ); | |
| } | |
| export function ExternalLink({ href, children, className = '' }: { href: string; children: ReactNode; className?: string }) { | |
| return ( | |
| <a href={href} target="_blank" rel="noopener noreferrer" className={`ext ${className}`}> | |
| {children} | |
| <ExtIcon aria-hidden className="ext__icon" /> | |
| </a> | |
| ); | |
| } | |
| export function CopyButton({ text, label = 'Copy' }: { text: string; label?: string }) { | |
| const [done, setDone] = useState(false); | |
| return ( | |
| <button | |
| type="button" | |
| className="btn btn--ghost btn--sm" | |
| onClick={async () => { | |
| try { | |
| await navigator.clipboard.writeText(text); | |
| setDone(true); | |
| setTimeout(() => setDone(false), 1600); | |
| } catch { | |
| /* clipboard unavailable */ | |
| } | |
| }} | |
| > | |
| {done ? <Check aria-hidden /> : <Copy aria-hidden />} | |
| {done ? 'Copied' : label} | |
| </button> | |
| ); | |
| } | |
| export function Spinner({ label = 'Loading…' }: { label?: string }) { | |
| return ( | |
| <div className="loading" role="status"> | |
| <span className="spinner" aria-hidden /> | |
| <span>{label}</span> | |
| </div> | |
| ); | |
| } | |
| export function Skeleton({ height = 16, width = '100%' }: { height?: number; width?: number | string }) { | |
| return <span className="skeleton" style={{ height, width }} aria-hidden />; | |
| } | |
| export function ErrorState({ | |
| title = 'Something went wrong', | |
| message, | |
| retry = true, | |
| }: { | |
| title?: string; | |
| message?: string; | |
| retry?: boolean; | |
| }) { | |
| return ( | |
| <div className="state state--error" role="alert"> | |
| <AlertTriangle aria-hidden /> | |
| <div> | |
| <strong>{title}</strong> | |
| {message && <p>{message}</p>} | |
| {retry && ( | |
| <button type="button" className="btn btn--ghost btn--sm state__retry" onClick={() => window.location.reload()}> | |
| Retry | |
| </button> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } | |
| export function EmptyState({ title, children }: { title: string; children?: ReactNode }) { | |
| return ( | |
| <div className="state"> | |
| <Inbox aria-hidden /> | |
| <div> | |
| <strong>{title}</strong> | |
| {children && <p>{children}</p>} | |
| </div> | |
| </div> | |
| ); | |
| } | |
| export function Pagination({ | |
| page, | |
| pageCount, | |
| onPage, | |
| total, | |
| pageSize, | |
| }: { | |
| page: number; | |
| pageCount: number; | |
| onPage: (p: number) => void; | |
| total: number; | |
| pageSize: number; | |
| }) { | |
| if (total === 0) return null; | |
| const from = page * pageSize + 1; | |
| const to = Math.min(total, (page + 1) * pageSize); | |
| return ( | |
| <nav className="pager" aria-label="Pagination"> | |
| <span className="pager__info tabular"> | |
| {from.toLocaleString()}–{to.toLocaleString()} of {total.toLocaleString()} | |
| </span> | |
| <div className="pager__btns"> | |
| <button type="button" className="btn btn--ghost btn--sm" disabled={page === 0} onClick={() => onPage(page - 1)}> | |
| <ChevronLeft aria-hidden /> Prev | |
| </button> | |
| <span className="pager__page tabular"> | |
| {page + 1} / {Math.max(1, pageCount)} | |
| </span> | |
| <button | |
| type="button" | |
| className="btn btn--ghost btn--sm" | |
| disabled={page >= pageCount - 1} | |
| onClick={() => onPage(page + 1)} | |
| > | |
| Next <ChevronRight aria-hidden /> | |
| </button> | |
| </div> | |
| </nav> | |
| ); | |
| } | |
| export function Segmented<T extends string>({ | |
| value, | |
| options, | |
| onChange, | |
| label, | |
| }: { | |
| value: T; | |
| options: Array<{ value: T; label: ReactNode; count?: number }>; | |
| onChange: (v: T) => void; | |
| label: string; | |
| }) { | |
| return ( | |
| <div className="segmented" role="radiogroup" aria-label={label}> | |
| {options.map((o) => ( | |
| <button | |
| key={o.value} | |
| type="button" | |
| role="radio" | |
| aria-checked={o.value === value} | |
| className={`segmented__opt ${o.value === value ? 'is-on' : ''}`} | |
| onClick={() => onChange(o.value)} | |
| > | |
| {o.label} | |
| {o.count != null && <span className="segmented__count tabular">{o.count.toLocaleString()}</span>} | |
| </button> | |
| ))} | |
| </div> | |
| ); | |
| } | |
| export function Fact({ label, children, hint }: { label: ReactNode; children: ReactNode; hint?: string }) { | |
| return ( | |
| <div className="fact"> | |
| <dt title={hint}>{label}</dt> | |
| <dd>{children}</dd> | |
| </div> | |
| ); | |
| } | |
| /** Small horizontal meter (0–1). */ | |
| export function Meter({ value, color = 'var(--accent)' }: { value: number | null | undefined; color?: string }) { | |
| const v = value == null ? 0 : Math.max(0, Math.min(1, value)); | |
| return ( | |
| <span className="meter" aria-hidden> | |
| <span className="meter__fill" style={{ width: `${v * 100}%`, background: color }} /> | |
| </span> | |
| ); | |
| } | |
| /** UniProt accession(s); multi-chain sequences can carry several joined by ';'. */ | |
| export function UniProtLinks({ ids }: { ids: string | null | undefined }) { | |
| if (!ids) return null; | |
| return ( | |
| <> | |
| {ids | |
| .split(';') | |
| .filter(Boolean) | |
| .map((id) => ( | |
| <ExternalLink key={id} href={`https://www.uniprot.org/uniprotkb/${id}`}> | |
| UniProt {id} | |
| </ExternalLink> | |
| ))} | |
| </> | |
| ); | |
| } | |