import type { IntentName } from "@charcuterie/tokens"; import type { ReactNode } from "react"; export type LogLine = { intent?: IntentName; key: string; text: string; }; export type LogViewerProps = { className?: string; /** * Announce new lines to a screen reader. **Off by default**, and * that is a considered choice rather than an oversight — see * below. */ isAnnounced?: boolean; /** The pane's accessible name. Required. */ label: string; lines: LogLine[]; /** * Oldest lines past this are dropped from the DOM. A rip in * progress emits a line every few hundred milliseconds for an * hour, and the browser keeps every node. */ maxLines?: number; }; /** * Three repos render a scrolling log pane — * mux-magic's `JobLogsDisclosure` and `StepLogs`, rip-deck's * `LogModal` — and between them they have four defects that this * component exists to not have. A fifth, below the effects, is not * theirs: it is defect 1 coming back through the front door, in this * component, found by the first app to consume it. * * ### 1. The one that follows nothing * * ```tsx * useEffect(() => { * const pane = paneRef.current * if (pane) pane.scrollTop = pane.scrollHeight * }, []) // ← empty deps * ``` * * That is mux-magic's auto-scroll, and it runs **once**, on mount, * when the pane is empty and `scrollHeight` is `clientHeight`. Every * line after it arrives below the fold. It reads as correct, it is * the shape everyone writes, and the log simply never follows. * * ### 2. Following that cannot be escaped * * The naive fix — scroll to the bottom on every render — is worse: * a user scrolling back to read the line that mentioned an error is * yanked to the end twice a second. Following has to be *pinned to * the user's own position*, which is what `isFollowing` below is: it * turns off the moment the user scrolls away and back on when they * return to the bottom, and there is a button for when they do not * want to scroll all that way. * * ### 3. `data-log-id` * * mux-magic's pane carries one. It is a `data-testid` under a * different name — a handle only the test suite can see — and this * package's `sourceRules.test.ts` bans the practice rather than the * spelling. The pane here is found by its `label`, the same query * Playwright makes. * * ### 4. Announcing an hour of build output * * `role="log"` is the correct role and `aria-live` is off unless * asked for. A live log pane emitting a line every 300 ms is a * screen reader that cannot be interrupted and a user who cannot * read anything else on the page — the accessible answer for a * long-running job is a `LiveStatusIndicator` announcing *state* * and a log that is there to be read when wanted. `isAnnounced` * exists for the short case: a five-line result panel where the * lines *are* the outcome. * * ### Not every boolean is a state kind * * `isFollowing` is `useState`, not `useVisibility`, and this is the * first component in the library to say so out loud. The kinds in * `@charcuterie/logic` earn their keep when state is **registered** * (members join a group), **composed** (focus and selection * disagreeing), or **shared** (a trigger and a panel at opposite * ends of a tree). None of that is true here: one component reads * one boolean it wrote itself. Reaching for `useVisibility` because * it is also a boolean would make `isVisible` mean "is following", * which is how a state layer stops meaning anything. */ export declare const LogViewer: ({ className, isAnnounced, label, lines, maxLines, }: LogViewerProps) => ReactNode; //# sourceMappingURL=LogViewer.d.ts.map