import { t, tList } from '../../i18n/index.js'; // "Development Roadmap" — milestone timeline section. // Translated from the Claude Design component (Roadmap.dc.html): a full-bleed // dark band with a shipped-progress bar and an expandable, status-coded timeline. function Roadmap() { const [isMobile, setIsMobile] = React.useState( typeof window !== 'undefined' && window.matchMedia('(max-width:767px)').matches ); React.useEffect(() => { const mq = window.matchMedia('(max-width:767px)'); const onChange = () => setIsMobile(mq.matches); mq.addEventListener ? mq.addEventListener('change', onChange) : mq.addListener(onChange); return () => { mq.removeEventListener ? mq.removeEventListener('change', onChange) : mq.removeListener(onChange); }; }, []); const MONO = "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace"; const SANS = "'Manrope', system-ui, -apple-system, sans-serif"; const DATA = [ { v: "v1.0", k: 'r1', status: "released" }, { v: "v1.5", k: 'r2', status: "released" }, { v: "v2.0", k: 'r3', status: "released" }, { v: "v3.0", k: 'r4', status: "released" }, { v: "v3.5", k: 'r5', status: "released" }, { v: "v4.5", k: 'r6', status: "released" }, { v: "v5.0", k: 'r7', status: "released" }, { v: "v5.5", k: 'r8', status: "released" }, { v: "v6.0", k: 'r9', status: "current" }, { v: "v6.5", k: 'r10', status: "dev" }, { v: "v7.0", k: 'r11', status: "planned" }, { v: "v7.5", k: 'r12', status: "research" }, { v: "v8.0", k: 'r13', status: "research" } ].map((d) => ({ ...d, // Version tag and status are identifiers, not copy; everything a reader // actually reads comes from the locale file. title: t(`roadmap.${d.k}.title`), sub: t(`roadmap.${d.k}.sub`), date: t(`roadmap.${d.k}.date`), features: tList(`roadmap.${d.k}.features`) })); // The status of a milestone is shown by its marker on the timeline and the colour of // its bullets, not by a word: the labels (Released, In development, ...) were taken // out. `solid` is the dot colour, `line` the spine below it. const META = { released: { solid: "var(--sb-green-solid)", line: "rgba(var(--sb-green-rgb), 0.32)" }, current: { solid: "var(--sb-orange-solid)", line: "rgba(var(--sb-orange-rgb), 0.32)" }, dev: { solid: "var(--sb-yellow-2-solid)", line: "rgba(var(--sb-ink), 0.08)" }, planned: { solid: "var(--sb-text-7)", line: "rgba(var(--sb-ink), 0.08)" }, research: { solid: "var(--sb-text-9)", line: "rgba(var(--sb-ink), 0.08)" } }; const [open, setOpen] = React.useState({}); const isOpen = (i) => (open[i] === undefined ? DATA[i].status === 'current' : open[i]); const toggle = (i) => setOpen((s) => ({ ...s, [i]: !isOpen(i) })); const total = DATA.length; const shipped = DATA.filter((d) => d.status === 'released' || d.status === 'current').length; const upcoming = total - shipped; const shippedPct = (shipped / total * 100).toFixed(1) + '%'; // The shipped count is highlighted, so the sentence is split around it rather than // interpolated whole. Passing only `total` leaves the {shipped} placeholder in the // string, which is where the highlighted number goes — a translation is free to put // it anywhere in the sentence, and the highlight follows it there. const [progressBefore, progressAfter] = t('roadmap.progress', { total }).split('{shipped}'); const renderNode = (status) => { if (status === 'released') { return (
{t('roadmap.subheading')}