CodeLines
uses window
Code as numbered lines. A line's number links to it (#L12); shift-clicking another makes a range (#L12-L20).
The lines the address names are marked and scrolled to.
import { useEffect, useRef, useState } from "react"; import type { CSSProperties, MouseEvent } from "react"; export function CodeLines({ lines }: { lines: string[] }) { const [sel, setSel] = useState<[number, number] | undefined>(undefined); const from = useRef<number | undefined>(undefined); useEffect(() => { const read = () => { const m = /^#L(\d+)(?:-L?(\d+))?$/.exec(window.location.hash); if (!m) return setSel(undefined); const a = Number(m[1]); const b = Number(m[2] ?? m[1]); const r: [number, number] = [Math.min(a, b), Math.max(a, b)]; setSel(r); from.current = r[0]; document .getElementById(`L${r[0]}`) ?.scrollIntoView({ block: r[0] === r[1] ? "center" : "start" }); }; read(); window.addEventListener("hashchange", read); return () => window.removeEventListener("hashchange", read); }, []); const pick = (e: MouseEvent, n: number) => { e.preventDefault(); const r: [number, number] = e.shiftKey && from.current !== undefined ? [Math.min(from.current, n), Math.max(from.current, n)] : [n, n]; if (!e.shiftKey) from.current = n; setSel(r); const { pathname, search } = window.location; window.history.replaceState( window.history.state, "", `${pathname}${search}#L${r[0]}${r[1] > r[0] ? `-L${r[1]}` : ""}`, ); }; return ( <div className="lines" style={{ "--nw": `${String(lines.length).length}ch` } as CSSProperties}> {lines.map((l, i) => { const n = i + 1; return ( <div key={n} id={`L${n}`} className={sel && n >= sel[0] && n <= sel[1] ? "ln on" : "ln"}> <a className="num" href={`#L${n}`} data-n={n} aria-label={`Line ${n}`} onClick={(e) => pick(e, n)} /> <span className="lc" dangerouslySetInnerHTML={{ __html: l }} /> </div> ); })} </div> ); }