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.json

Usage

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-visible background 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 selectedLine lands on a non-bug, the row shakes horizontally for ~300ms; nothing persists in component state.

Props

PropTypeDefaultDescription
codestring | readonly string[]Code to render. Strings are split on newlines.
bugLinesreadonly number[][]1-indexed lines that contain a bug.
foundLinesreadonly number[][]1-indexed lines already flagged.
selectedLinenumber | nullnullLast line tapped — drives hit pop / miss shake.
onLineTap(line) => voidFires when the learner taps a line.
interactablebooleantrueWhen false, taps no longer fire.
langstringShiki language id. Omit for plain text.
headerTextReactNode"Find the bug"Label in the header strip.
hideHeaderbooleanfalseSuppress the header strip.
remainingBugsnumberCounter shown on the right of the header.
maxHeightstringmax-height on the scroll container.
showLineNumbersbooleantrueShow 1-indexed gutter line numbers.
classNamestringMerged onto the root via cn().

Accessibility

  • Each line is a <button> with an aria-label of 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 configurable aria-label so 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 interactable is false.
  • 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 the BugHuntContext coupling, the in-house useCodeTokens / renderTokens Shiki 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.