WinslowFan's picture
Claude Opus 5.5
Serve SQLite from local disk instead of the bucket mount
3d86d3b
Raw History Blame Contribute Delete
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>
))}
</>
);
}