Bug Hunt Code
The code-panel half of a bug-hunt puzzle. Every line of source code renders as a <button>; the parent owns the puzzle state (which lines are bugs, which are found, which one is the current wrong tap) and the component owns nothing but the render and the gesture.
Pair with a bespoke shell when you need your own feedback rail or fix phase. Reach for BugHunt when a single panel plus stacked description cards is enough.
Preview
Find the bug1 remaining
Customize
Options
Installation
npx shadcn@latest add https://craftbits.dev/r/bug-hunt-code.jsonUsage
import { useState } from "react";
import { BugHuntCode } from "@craft-bits/core";
const BUG_LINES = [4];
export function FindTheBug({ code }: { code: readonly string[] }) {
const [foundLines, setFoundLines] = useState<readonly number[]>([]);
const [selectedLine, setSelectedLine] = useState<number | null>(null);
return (
<BugHuntCode
code={code}
lang="typescript"
bugLines={BUG_LINES}
foundLines={foundLines}
selectedLine={selectedLine}
remainingBugs={BUG_LINES.length - foundLines.length}
onLineTap={(line) => {
setSelectedLine(line);
if (BUG_LINES.includes(line) && !foundLines.includes(line)) {
setFoundLines((prev) => [...prev, line]);
}
}}
/>
);
}Drop lang to render plain monospace text without loading Shiki.
Anatomy
- Header strip — a small bug glyph plus a label (defaults to "Find the bug"), with an optional remaining-bug counter on the right. Hidden via
hideHeader. - Code lines — every line is a
<button>. A:focus-visiblebackground tint replaces the default outline; tap or Enter to flag a line. - Found highlight — flagged lines paint in the error tone and announce as
aria-pressed. Tokens inside a found line are recolored so syntax colors do not fight the error tint. - Miss shake — when
selectedLinelands on a non-bug, the row shakes horizontally for ~300ms; nothing persists in component state.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
code | string | readonly string[] | — | Code to render. Strings are split on newlines. |
bugLines | readonly number[] | [] | 1-indexed lines that contain a bug. |
foundLines | readonly number[] | [] | 1-indexed lines already flagged. |
selectedLine | number | null | null | Last line tapped — drives hit pop / miss shake. |
onLineTap | (line) => void | — | Fires when the learner taps a line. |
interactable | boolean | true | When false, taps no longer fire. |
lang | string | — | Shiki language id. Omit for plain text. |
headerText | ReactNode | "Find the bug" | Label in the header strip. |
hideHeader | boolean | false | Suppress the header strip. |
remainingBugs | number | — | Counter shown on the right of the header. |
maxHeight | string | — | max-height on the scroll container. |
showLineNumbers | boolean | true | Show 1-indexed gutter line numbers. |
className | string | — | Merged onto the root via cn(). |
Accessibility
- Each line is a
<button>with anaria-labelof the form "Line 4 — bug found", so screen-reader users always hear both the line number and the resolved state. - The code container is
role="group"with a configurablearia-labelso the whole panel is one stop on the screen-reader rotor. - Already-found lines disable to prevent double-flagging the same bug; the panel becomes fully read-only when
interactableisfalse. - Miss feedback is a single short horizontal shake — short enough to feel like a "no" without violating reduced-motion expectations.
Credits
- Extracted from:
AlgoFlashcards(src/lessons/primitives/BugHunt/BugHuntCode.tsx). Stripped theBugHuntContextcoupling, the in-houseuseCodeTokens/renderTokensShiki shim, the phase-aware interactability guard, and the project-specific text-size token — generalized into a fully controlled puzzle renderer that any bug-hunt shell can host.