import React, { useCallback, useEffect, useRef, useState } from "react"; import { createRoot } from "react-dom/client"; import "./react.css"; type Status = "pending" | "approved"; type NavigationLevel = "card" | "task" | "block" | "phase" | "step" | "snapshot"; type NavigationPosition = { task_index: number; block_index: number; phase_index: number; step_index: number; global_step_index: number; snapshot_index: number | null; global_snapshot_index: number | null; }; type ApiNavigation = { revision: number; actor?: string; focus_level: NavigationLevel; task: { id: string; label: string; index: number }; block: { id: string; label: string; index: number }; phase: { id: string; label: string; index: number }; step: { id: string; label: string; number: number; index: number; global_index: number }; snapshot_ref: string | null; position: NavigationPosition; sync_requested: boolean; }; type ApiViewerState = { revision: number; actor?: string | null; scale: number; scroll: { x: number; y: number; page_index: number; sheet_scroll_top?: number; }; viewport: { width: number; height: number }; allocator?: { visible: boolean }; nvim: { visible: boolean; sync: boolean; control: boolean; expanded: boolean; fit: boolean; connection: string; screen_cursor: { row: number; column: number } | null; grid: { columns: number; rows: number } | null; }; }; type InteractiveArea = "compile" | "bss" | "stack" | "registers" | "text"; type InteractiveSymbol = { id: string; area: InteractiveArea; declaration: string; description: string; }; type UmlStage = { id: string; label: string; description: string; code_ref?: string; progress_id?: string; svg_label?: string; }; type UmlSnapshotRegister = { name: string; value: string; note?: string }; type UmlSnapshotFrameField = { name: string; address: string; value: string; size_bytes?: number; state: "initialised" | "uninitialised" | "saved"; note?: string; }; type NvimAnnotation = { id: string; anchor_ref?: string; label?: string; tone?: "danger" | "info" | "success" | "warning"; shape?: "outline" | "underline"; anchor: { kind: "screen-text" | "grid-range"; text?: string; occurrence?: number; row?: number; column?: number; rows?: number; columns?: number; padding_cells?: number; }; }; type NvimStepView = { connection_ref?: string; layout?: string; recorded_grid_ref?: string; reveal?: { anchor_ref?: string; align?: "top" | "center" | "bottom"; }; annotations?: NvimAnnotation[]; }; type UmlSnapshot = { target?: string; captured_at?: string; captured_with?: string; elf?: string; pc: string; registers: UmlSnapshotRegister[]; frame: { name: string; sp: string; fp: string; stack_top?: string; size_bytes: number; fields: UmlSnapshotFrameField[]; }; memory: { items: { name: string; address: string; value: string; note?: string }[]; arena?: { base: string; size_bytes: number; cursor: string; offset: number | null; bytes: string; }; }; code?: { source: string; source_ref?: string; assembly: string; note?: string; }; description: string; }; type UmlSequenceStep = { id: string; number: number; label: string; description: string; code_ref?: string; progress_id?: string; svg_label?: string; snapshot_ref?: string; view?: NvimStepView; snapshot: UmlSnapshot; }; type UmlSequencePhase = { id: string; label: string; description?: string; svg_label?: string; steps: UmlSequenceStep[]; }; type InteractiveAsset = { kind: "allocator-memory-flow" | "uml-sequence"; storage_key?: string; title?: string; task?: { id: string; label: string }; block?: { id: string; label: string; description?: string }; snapshot_context?: { target: string; captured_at: string; captured_with?: string; elf?: string; stack_top?: string; }; terminal_view?: { path?: string; href?: string; title?: string; alt: string; caption?: string; }; arena_size?: number; allocations?: number[]; symbols?: InteractiveSymbol[]; stages?: UmlStage[]; phases?: UmlSequencePhase[]; }; type Asset = { label: string; caption: string; alt: string; href: string; width: number; kind: string; source_href?: string; interactive?: InteractiveAsset | null; }; type FlowStep = { id: string; title: string; content_html: string }; type FlowBlock = { id: string; kind: "block" | "exercise"; title: string; content_html: string; steps: FlowStep[]; }; type Task = { ref: string; title: string; uuid?: string; criterion: string; conclusion_html: string; prompt_html?: string; flow: FlowBlock[]; }; type Section = { id: string; index: number; title: string; show_heading: boolean; header_html: string; left_margin_html: string; right_margin_html: string; content_html: string; tasks: Task[]; assets: Asset[]; steps: { id: string; title: string }[]; }; type Progress = { items: Record; summary: { total: number; counts: Record }; }; type SetProgressStatus = (id: string, status: Status) => Promise; type CardModel = { schema: string; card: Record; total_pages: number; front: any; sections: Section[]; dictionary: { header_html: string; content_html: string } | null; toc: { id: string; label: string }[]; }; type NvimPanelSummary = { state: | "hidden" | "connecting" | "connected" | "replaying" | "verifying" | "ready" | "failed" | "offline" | "unselected"; message?: string; instance?: string; detail?: string; syncMode: boolean; syncReady: boolean; controlMode: boolean; controlEnabled: boolean; screenCursor?: { row: number; column: number }; grid?: { columns: number; rows: number }; }; const nvimStatusLabel: Record = { hidden: "UKRYTY", connecting: "ŁĄCZENIE", connected: "ONLINE · LIVE", replaying: "ONLINE · REPLAY", verifying: "ONLINE · WERYFIKACJA", ready: "ONLINE · GOTOWY", failed: "ONLINE · BŁĄD STANU", offline: "OFFLINE · ZAPIS", unselected: "OFFLINE · BRAK CELU", }; const UML_SCROLLOFF_RATIO = 0.25; const UML_SCROLLOFF_EPSILON_PX = 1; const UML_SCROLLOFF_MAX_PASSES = 4; type VerticalViewport = { top: number; bottom: number }; function clipsVertically(element: HTMLElement) { const overflow = window.getComputedStyle(element).overflowY; return overflow === "auto" || overflow === "scroll" || overflow === "hidden" || overflow === "clip"; } function elementClientViewport(element: HTMLElement): VerticalViewport { const bounds = element.getBoundingClientRect(); const layoutHeight = element.offsetHeight || element.clientHeight; const scale = layoutHeight > 0 ? bounds.height / layoutHeight : 1; const top = bounds.top + element.clientTop * scale; return { top, bottom: top + element.clientHeight * scale, }; } function chromeAwareWindowViewport(target: Element): VerticalViewport | null { const chrome = document.querySelector(".viewer-chrome"); if (chrome?.classList.contains("is-nvim-fit")) return null; let top = 0; let bottom = window.innerHeight; if (chrome) { const chromeBounds = chrome.getBoundingClientRect(); const targetBounds = target.getBoundingClientRect(); const overlapsHorizontally = chromeBounds.left < targetBounds.right && chromeBounds.right > targetBounds.left; if ( overlapsHorizontally && chromeBounds.bottom > 0 && chromeBounds.top < window.innerHeight ) { top = Math.max(top, chromeBounds.bottom); } } return bottom - top > UML_SCROLLOFF_EPSILON_PX ? { top, bottom } : null; } function visibleVerticalViewport(target: Element): VerticalViewport | null { const windowViewport = chromeAwareWindowViewport(target); if (!windowViewport) return null; let { top, bottom } = windowViewport; for ( let ancestor = target.parentElement; ancestor && ancestor !== document.body && ancestor !== document.documentElement; ancestor = ancestor.parentElement ) { if (!clipsVertically(ancestor)) continue; const client = elementClientViewport(ancestor); top = Math.max(top, client.top); bottom = Math.min(bottom, client.bottom); } return bottom - top > UML_SCROLLOFF_EPSILON_PX ? { top, bottom } : null; } function documentScroller() { return (document.scrollingElement as HTMLElement | null) ?? document.documentElement; } function verticalScrollChain(target: Element) { const chain: HTMLElement[] = []; for ( let ancestor = target.parentElement; ancestor && ancestor !== document.body && ancestor !== document.documentElement; ancestor = ancestor.parentElement ) { const overflow = window.getComputedStyle(ancestor).overflowY; const acceptsScroll = overflow === "auto" || overflow === "scroll"; if (acceptsScroll && ancestor.scrollHeight - ancestor.clientHeight > 1) { chain.push(ancestor); } } const root = documentScroller(); if (!chain.includes(root)) chain.push(root); return chain; } function visualScrollScale(scroller: HTMLElement) { if (scroller === documentScroller()) return 1; const bounds = scroller.getBoundingClientRect(); const layoutHeight = scroller.offsetHeight || scroller.clientHeight; const scale = layoutHeight > 0 ? bounds.height / layoutHeight : 1; return Number.isFinite(scale) && scale > 0 ? scale : 1; } function setScrollPositionInstant( scroller: HTMLElement, position: { top?: number; left?: number }, ) { const previousBehavior = scroller.style.scrollBehavior; scroller.style.scrollBehavior = "auto"; if (position.left != null) scroller.scrollLeft = position.left; if (position.top != null) scroller.scrollTop = position.top; scroller.style.scrollBehavior = previousBehavior; } function bringTargetSheetIntoWindow(target: Element, viewport: VerticalViewport) { const anchor = target.closest(".sheet-content"); if (!anchor) return 0; const bounds = elementClientViewport(anchor); let delta = 0; if (bounds.bottom <= viewport.top) { delta = bounds.bottom - viewport.bottom; } else if (bounds.top >= viewport.bottom) { delta = bounds.top - viewport.top; } if (Math.abs(delta) <= UML_SCROLLOFF_EPSILON_PX) return 0; const root = documentScroller(); const previous = root.scrollTop; const maximum = Math.max(0, root.scrollHeight - root.clientHeight); setScrollPositionInstant(root, { top: Math.max(0, Math.min(maximum, previous + delta)), }); return root.scrollTop - previous; } function applyVerticalScrollDelta(target: Element, delta: number) { let remaining = delta; for (const scroller of verticalScrollChain(target)) { if (Math.abs(remaining) <= UML_SCROLLOFF_EPSILON_PX) break; const scale = visualScrollScale(scroller); const maximum = Math.max(0, scroller.scrollHeight - scroller.clientHeight); const previous = scroller.scrollTop; const next = Math.max(0, Math.min(maximum, previous + remaining / scale)); setScrollPositionInstant(scroller, { top: next }); remaining -= (scroller.scrollTop - previous) * scale; } return delta - remaining; } function umlScrolloffDelta(target: Element, viewport: VerticalViewport) { const bounds = target.getBoundingClientRect(); const cursor = (bounds.top + bounds.bottom) / 2; const height = viewport.bottom - viewport.top; const upper = viewport.top + height * UML_SCROLLOFF_RATIO; const lower = viewport.bottom - height * UML_SCROLLOFF_RATIO; if (cursor < upper) return cursor - upper; if (cursor > lower) return cursor - lower; return 0; } const Html = ({ html, className }: { html: string; className?: string }) => (
); function Header({ html }: { html: string }) { return ; } function Margin({ side, html }: { side: "left" | "right"; html: string }) { return (