/** * Component Info Panel * * Floating "datasheet" popover shown when the user hovers a card in the * Component Picker. It combines two data sources: * * 1. The already-loaded ComponentMetadata (name, category, pin count, live * default properties, tags) — always available, no network. * 2. An optional hand-authored Markdown datasheet (see `componentDocs.ts` * and `component-docs/`) with the richer prose, pinout, wiring tips, * plus the component's brand and a purchase link. Lazy-loaded + cached. * * The panel is INTERACTIVE: the mouse can move off the card onto the panel to * scroll a long datasheet or click the Buy link without it closing. This is * driven from the modal via a grace-period hide timer — `onPanelEnter` cancels * the pending hide, `onPanelLeave` re-arms it. Rendered through a portal to *
so the modal's `overflow` never clips it, and flipped/clamped to stay * inside the viewport. */ import React from 'react'; import { createPortal } from 'react-dom'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import type { PropertyDescriptor } from '../types/component-metadata'; import { loadDoc, type ComponentDoc } from './componentDocs'; export interface PanelData { id: string; // component / board id — used to look up the Markdown doc name: string; category: string; // already display-formatted (e.g. "Sensors") description?: string; pinCount: number; properties: PropertyDescriptor[]; tags: string[]; thumbnail?: string; pro_only?: boolean; } export interface HoverTarget { data: PanelData; rect: DOMRect; // bounding box of the hovered card, in viewport coords } /** Delay before the panel appears — long enough to not flash on a fly-by. */ export const HOVER_DELAY = 160; // Bulk / opaque properties that are never useful in a datasheet popover // (base64 blobs, embedded source, framebuffers, …). const HIDDEN_PROPS = new Set([ 'imageData', 'wasmBase64', 'sourceC', 'romBytes', 'chipJson', 'programFile', 'programTarget', ]); /** Only allow real web links through to the Buy button (no javascript:, etc.). */ function safeHref(url?: string): string | null { if (!url) return null; return /^https?:\/\//i.test(url) ? url : null; } function formatValue(p: PropertyDescriptor): string { const raw = p.defaultValue; let def = raw === undefined || raw === null || raw === '' ? '' : String(raw); if (def.length > 40) def = def.slice(0, 39) + '…'; if (p.min !== undefined || p.max !== undefined) { const range = `${p.min ?? '?'}–${p.max ?? '?'}`; return def ? `${def} (${range})` : range; } if (p.options && p.options.length) { const opts = p.options.join(' / '); // A long option list would blow out the row — fall back to the default. return opts.length > 44 ? def || String(p.options[0]) : opts; } return def || '—'; } interface ComponentInfoPanelProps { target: HoverTarget; /** Called when the pointer enters the panel — cancels the pending hide. */ onPanelEnter: () => void; /** Called when the pointer leaves the panel — re-arms the hide timer. */ onPanelLeave: () => void; } export const ComponentInfoPanel: React.FC