All files / web/src/utils beadTooltipUtils.ts

98.92% Statements 184/186
86.11% Branches 31/36
100% Functions 3/3
98.92% Lines 184/186

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 1871x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 14x 14x 14x 10x 10x 10x 10x 14x 1x 1x 9x 9x 9x 4x 4x 2x 2x 2x 2x 4x 1x 1x 1x 1x 4x 1x 1x     9x 9x 14x 14x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 33x 33x 33x 33x 33x 33x 33x 33x 33x 109x 109x 109x 109x 2x 2x 2x 109x 2x 2x 2x 105x 105x 105x 109x 11x 11x 109x 109x 109x 1x 1x 109x 104x 104x 104x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 6x 6x 6x 6x 6x 6x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 6x 6x 6x 1x 1x 1x 1x 1x 1x 1x 3x 3x 3x 3x 3x 3x 3x 6x 6x 6x 6x 6x 6x 6x 6x  
/**
 * Bead Tooltip Utilities
 *
 * Extracted from TutorialPlayer for reuse in practice help overlay.
 * Handles smart tooltip positioning to avoid covering active beads.
 */
 
import type { StepBeadHighlight } from '@soroban/abacus-react'
 
/**
 * Target specification for tooltip overlay
 */
export interface TooltipTarget {
  type: 'bead'
  columnIndex: number
  beadType: 'heaven' | 'earth'
  beadPosition: number | undefined
}
 
/**
 * Result of tooltip positioning calculation
 */
export interface TooltipPositioning {
  /** Which side to show tooltip on */
  side: 'top' | 'left'
  /** Target bead for the tooltip */
  target: TooltipTarget
  /** The topmost bead that was selected */
  topmostBead: StepBeadHighlight
  /** Column index of the target */
  targetColumnIndex: number
}
 
/**
 * Find the topmost bead with arrows in a step
 *
 * Priority order:
 * 1. Higher place value (leftmost columns = more significant)
 * 2. Heaven beads before earth beads
 * 3. Lower position number for earth beads (higher on abacus)
 *
 * @param stepBeadHighlights - Array of bead highlights for the current step
 * @returns The topmost bead, or null if none have arrows
 */
export function findTopmostBeadWithArrows(
  stepBeadHighlights: StepBeadHighlight[] | undefined
): StepBeadHighlight | null {
  if (!stepBeadHighlights || stepBeadHighlights.length === 0) return null
 
  // Filter only beads that have direction arrows
  const beadsWithArrows = stepBeadHighlights.filter((bead) => !!bead.direction)
 
  if (beadsWithArrows.length === 0) {
    return null
  }
 
  // Sort by priority
  const sortedBeads = [...beadsWithArrows].sort((a, b) => {
    // First sort by place value (higher place value = more significant = topmost priority)
    if (a.placeValue !== b.placeValue) {
      return b.placeValue - a.placeValue
    }
 
    // If same place value, heaven beads come before earth beads
    if (a.beadType !== b.beadType) {
      return a.beadType === 'heaven' ? -1 : 1
    }
 
    // If both earth beads in same column, lower position number = higher on abacus
    if (a.beadType === 'earth' && b.beadType === 'earth') {
      return (a.position || 0) - (b.position || 0)
    }

    return 0
  })
 
  return sortedBeads[0] || null
}
 
/**
 * Check if there are active beads to the left of a target column
 *
 * Active beads are:
 * - Beads against the reckoning bar (based on current value)
 * - Beads with direction arrows in the current step
 *
 * @param currentValue - Current abacus value
 * @param stepBeadHighlights - Bead highlights for current step
 * @param abacusColumns - Number of columns on the abacus
 * @param targetColumnIndex - Column index to check left of
 * @returns True if there are active beads to the left
 */
export function hasActiveBeadsToLeft(
  currentValue: number,
  stepBeadHighlights: StepBeadHighlight[] | undefined,
  abacusColumns: number,
  targetColumnIndex: number
): boolean {
  // Get current abacus state - check which beads are against the reckoning bar
  const abacusDigits = currentValue.toString().padStart(abacusColumns, '0').split('').map(Number)
 
  for (let col = 0; col < targetColumnIndex; col++) {
    const digitValue = abacusDigits[col]
 
    // Check if any beads are active (against reckoning bar) in this column
    if (digitValue >= 5) {
      // Heaven bead is active
      return true
    }
    if (digitValue % 5 > 0) {
      // Earth beads are active
      return true
    }
 
    // Also check if this column has beads with direction arrows
    const hasArrowsInColumn =
      stepBeadHighlights?.some((bead) => {
        const beadColumnIndex = abacusColumns - 1 - bead.placeValue
        return beadColumnIndex === col && !!bead.direction
      }) ?? false
 
    if (hasArrowsInColumn) {
      return true
    }
  }
 
  return false
}
 
/**
 * Calculate smart tooltip positioning
 *
 * Determines which side to show the tooltip on and which bead to target,
 * avoiding covering active beads.
 *
 * @param currentValue - Current abacus value
 * @param stepBeadHighlights - Bead highlights for current step
 * @param abacusColumns - Number of columns on the abacus
 * @returns Positioning info, or null if no beads with arrows
 */
export function calculateTooltipPositioning(
  currentValue: number,
  stepBeadHighlights: StepBeadHighlight[] | undefined,
  abacusColumns: number
): TooltipPositioning | null {
  const topmostBead = findTopmostBeadWithArrows(stepBeadHighlights)
  if (!topmostBead) return null
 
  // Convert placeValue to columnIndex
  const targetColumnIndex = abacusColumns - 1 - topmostBead.placeValue
 
  // Check if there are active beads to the left
  const activeToLeft = hasActiveBeadsToLeft(
    currentValue,
    stepBeadHighlights,
    abacusColumns,
    targetColumnIndex
  )
 
  // Determine tooltip position and target
  const shouldPositionAbove = activeToLeft
  const side = shouldPositionAbove ? 'top' : 'left'
 
  const target: TooltipTarget = shouldPositionAbove
    ? {
        // Target the heaven bead position for the column
        type: 'bead',
        columnIndex: targetColumnIndex,
        beadType: 'heaven',
        beadPosition: 0, // Heaven beads are always at position 0
      }
    : {
        // Target the actual bead
        type: 'bead',
        columnIndex: targetColumnIndex,
        beadType: topmostBead.beadType,
        beadPosition: topmostBead.position,
      }
 
  return {
    side,
    target,
    topmostBead,
    targetColumnIndex,
  }
}