All files / web/src/components/toys/euclid/chat EuclidEntitySpan.tsx

0% Statements 0/126
0% Branches 0/1
0% Functions 0/1
0% Lines 0/126

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                                                                                                                                                                                                                                                             
'use client'

/**
 * Renders a single Euclid entity span with per-type styling.
 *
 * - Geometric entities (seg, tri, ang, pt): blue, dotted underline → canvas glow on hover
 * - Definitions: red → CitationPopover on hover
 * - Postulates: gold → CitationPopover on hover
 * - Common Notions: blue → CitationPopover on hover
 * - Propositions: dark → CitationPopover on hover
 */

import { useRef, useState, useCallback } from 'react'
import { BYRNE } from '../types'
import type { EuclidEntityRef, FoundationEntityRef } from './parseGeometricEntities'
import {
  isGeometricEntity,
  isFoundationEntity,
  foundationToCitationKey,
} from './parseGeometricEntities'

/** Color for each entity type, using the Byrne palette. */
function entityColor(entity: EuclidEntityRef): string {
  if (isGeometricEntity(entity)) return BYRNE.blue
  switch (entity.type) {
    case 'definition':
      return BYRNE.red
    case 'postulate':
      return BYRNE.yellow
    case 'commonNotion':
      return BYRNE.blue
    case 'proposition':
      return BYRNE.given
  }
}

/** Underline opacity — geometric gets dotted, foundations get solid but subtle */
function entityUnderline(entity: EuclidEntityRef): string {
  const color = entityColor(entity)
  if (isGeometricEntity(entity)) return `1px dotted ${color}66`
  return `1px solid ${color}55`
}

interface EuclidEntitySpanProps {
  entity: EuclidEntityRef
  displayText: string
  onHighlightGeometric: (entity: EuclidEntityRef | null) => void
  onHighlightFoundation: (entity: FoundationEntityRef, anchorRect: DOMRect) => void
  onUnhighlightFoundation: () => void
  /** When true, inherit parent styling but still trigger glow/popover on hover */
  subtle?: boolean
}

export function EuclidEntitySpan({
  entity,
  displayText,
  onHighlightGeometric,
  onHighlightFoundation,
  onUnhighlightFoundation,
  subtle,
}: EuclidEntitySpanProps) {
  const spanRef = useRef<HTMLSpanElement>(null)
  const [hovered, setHovered] = useState(false)

  const handleMouseEnter = useCallback(() => {
    setHovered(true)
    if (isGeometricEntity(entity)) {
      onHighlightGeometric(entity)
    } else if (isFoundationEntity(entity)) {
      const rect = spanRef.current?.getBoundingClientRect()
      if (rect) onHighlightFoundation(entity, rect)
    }
  }, [entity, onHighlightGeometric, onHighlightFoundation])

  const handleMouseLeave = useCallback(() => {
    setHovered(false)
    if (isGeometricEntity(entity)) {
      onHighlightGeometric(null)
    } else if (isFoundationEntity(entity)) {
      onUnhighlightFoundation()
    }
  }, [entity, onHighlightGeometric, onUnhighlightFoundation])

  const handlePointerDown = useCallback(
    (e: React.PointerEvent) => {
      // Mobile: tap foundation entities to show popover
      if (!isFoundationEntity(entity)) return
      e.stopPropagation()
      const rect = spanRef.current?.getBoundingClientRect()
      if (rect) onHighlightFoundation(entity, rect)
    },
    [entity, onHighlightFoundation]
  )

  const color = entityColor(entity)

  const style: React.CSSProperties = subtle
    ? {
        color: hovered ? color : 'inherit',
        fontWeight: 'inherit',
        cursor: 'pointer',
        borderBottom: 'none',
        transition: 'color 0.15s ease',
      }
    : {
        color,
        fontWeight: 600,
        cursor: 'pointer',
        borderBottom: entityUnderline(entity),
      }

  return (
    <span
      ref={spanRef}
      data-element="entity-ref"
      data-entity-type={entity.type}
      data-citation-key={isFoundationEntity(entity) ? foundationToCitationKey(entity) : undefined}
      style={style}
      onMouseEnter={handleMouseEnter}
      onMouseLeave={handleMouseLeave}
      onPointerDown={handlePointerDown}
    >
      {displayText}
    </span>
  )
}