Files
Rpa_Skills/rpa-dashboard-ai/templates/reference/web/components/Tooltip.tsx
T
VasanthandClaude Opus 5.5 8951ac05a5 AI skill: SKILL.md, AI contract, scrubbed references, eval fixture (d)
- rpa-dashboard-ai/SKILL.md: trigger description and the shared question flow;
  points at the no-AI frontend.md, themes and palette instead of copying them.
- templates/contract.md: runs, review rows, endpoints and the server approve gate.
- Superbill extracts: patient names and the client name removed, every file
  carries an EXAMPLE ONLY banner. tests/test_reference_scan.py checks both skills
  for patient data, internal IPs, credentials and keys.
- tests/fixtures/d-ai-extraction and its eval entry, which runs page_check_ai
  on the built folder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 15:13:55 +05:30

135 lines
5.3 KiB
TypeScript

/* EXAMPLE ONLY: an extract from the Superbill Review prototype, kept as reference for porting. ECW/eClinicalWorks, Tebra, 'Example Client', SPB_* tables and patient/ticket wording are that project's examples. Don't copy them into a new project. */
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
const GAP = 8
type Rect = { top: number; bottom: number; left: number; width: number }
type Placed = { top: number; left: number }
/**
* A tooltip that is actually part of the design.
*
* Replaces `title=""`, which cannot be styled, cannot be themed, appears after a
* delay the page does not control, and renders in the OS's own font at the
* cursor rather than against the thing it describes.
*
* Positioning is deliberately in two stages, and the order matters:
*
* 1. The anchor's rect is captured in the open handler, and the panel is drawn
* immediately from it using a transform (`translate(-50%, -100%)`) — the
* browser centres and lifts it with no measurement at all, so it is
* correctly placed on the very first paint.
* 2. A layout effect then measures the rendered panel and only refines: flip
* below when there is no room above, clamp into the viewport at the edges.
*
* An earlier version skipped straight to stage 2 — render at 0,0 hidden, measure,
* then position. When that measurement did not land the tooltip stayed invisible
* in the corner, which is the worst failure available: the information silently
* disappears. Now a failed refinement costs at most some clipping at a screen
* edge, and the tooltip is always visible.
*
* Because the pane behind it scrolls, the captured rect can go stale — so any
* scroll closes the tooltip rather than leaving it pointing at nothing.
*
* It is `aria-hidden` on purpose: the same fact is in the trigger's own
* accessible name (see the field input's `aria-label`), so a screen reader loses
* nothing when this never appears. Opens on hover AND on keyboard focus.
*/
export function Tooltip({
label,
className,
children,
}: {
label: string
/** Applied to the wrapper, which stands in for the element it replaces. */
className?: string
children: React.ReactNode
}) {
const anchor = useRef<HTMLSpanElement>(null)
const panel = useRef<HTMLDivElement>(null)
const [rect, setRect] = useState<Rect | null>(null)
const [placed, setPlaced] = useState<Placed | null>(null)
const open = () => {
const r = anchor.current?.getBoundingClientRect()
if (r) setRect({ top: r.top, bottom: r.bottom, left: r.left, width: r.width })
}
const close = () => {
setRect(null)
setPlaced(null)
}
useLayoutEffect(() => {
if (!rect) return
const p = panel.current?.getBoundingClientRect()
if (!p) return
const flip = rect.top - p.height - GAP < GAP
const top = flip ? rect.bottom + GAP : rect.top - p.height - GAP
const wanted = rect.left + rect.width / 2 - p.width / 2
const left = Math.max(GAP, Math.min(wanted, window.innerWidth - p.width - GAP))
// Only take over from the transform when the transform would actually be
// wrong; otherwise leave the first-paint placement exactly as it is.
const drifted = Math.abs(left - wanted) > 0.5 || flip
setPlaced(drifted ? { top, left } : null)
}, [rect, label])
useEffect(() => {
if (!rect) return
// `true` for the capture phase: the scroll happens inside the review pane,
// and scroll events on an inner element do not bubble to window.
window.addEventListener('scroll', close, true)
window.addEventListener('resize', close)
return () => {
window.removeEventListener('scroll', close, true)
window.removeEventListener('resize', close)
}
}, [rect])
return (
<>
<span
ref={anchor}
className={className}
// focus/blur do not bubble natively, but React synthesises these from
// focusin/focusout, which do — so focusing the inner input opens it.
onMouseEnter={open}
onMouseLeave={close}
onFocus={open}
onBlur={close}
>
{children}
</span>
{rect &&
createPortal(
<div
ref={panel}
role="presentation"
aria-hidden="true"
className="pointer-events-none fixed z-50 w-max max-w-[18rem] rounded-lg px-2.5 py-1.5 font-plex text-[11px] font-medium leading-snug shadow-lg"
style={
placed
? { top: placed.top, left: placed.left, backgroundColor: 'var(--ink)', color: 'var(--surface)' }
: {
top: rect.top - GAP,
left: rect.left + rect.width / 2,
// Centres and lifts with no measurement — this is what makes
// the first paint correct rather than hidden.
transform: 'translate(-50%, -100%)',
// Inverted from the surface it floats over: a panel sharing
// its host's background reads as part of the page instead of
// above it. Both directions clear AAA against their own text.
backgroundColor: 'var(--ink)',
color: 'var(--surface)',
}
}
>
{label}
</div>,
document.body,
)}
</>
)
}