import { h } from 'preact' import { useEffect, useState } from 'preact/hooks' import type { TraceUnitMessage } from '../hooks/useConnection' import { clearBlockHighlight, highlightBlock } from '../blockHighlight' function formatMs(ms: number): string { if (ms >= 1000) { return `${(ms / 1000).toFixed(2)}s` } if (ms >= 1) { return `${ms.toFixed(2)}ms` } return `${(ms * 1000).toFixed(1)}µs` } function formatKind(kind: string): string { return kind.replaceAll('-', ' ').replaceAll('.', ' ') } function shortNodeId(nodeId: string): string { const segments = nodeId.split('::') return segments[segments.length - 1] } // --- Block reference detection --- interface BlockReference { readonly id: string readonly variant: string readonly blockType: string readonly properties: Record } function isBlockReference(value: unknown): value is BlockReference { if (value === null || typeof value !== 'object') { return false } const obj = value as Record return typeof obj.id === 'string' && typeof obj.variant === 'string' && typeof obj.blockType === 'string' } function findMatchingUnit(traceUnits: readonly TraceUnitMessage[], blockRef: BlockReference): TraceUnitMessage | undefined { return traceUnits.find(unit => unit.kind === 'resolve.block' && unit.nodeId === blockRef.id) } // --- Property tree --- interface PropertyContext { readonly traceUnits: readonly TraceUnitMessage[] readonly onSelectUnit: (unit: TraceUnitMessage | undefined) => void } function BlockRefChip({ blockRef, context }: { readonly blockRef: BlockReference; readonly context: PropertyContext }) { const childUnit = findMatchingUnit(context.traceUnits, blockRef) const handleClick = () => { if (childUnit) { context.onSelectUnit(childUnit) } } return ( ) } function PropertyValue({ value }: { readonly value: unknown }) { if (value === undefined) { return undefined } if (value === null) { return null } if (typeof value === 'string') { return "{value}" } if (typeof value === 'number') { return {value} } if (typeof value === 'boolean') { return {String(value)} } return {String(value)} } function PropertyRow({ name, value, context }: { readonly name: string; readonly value: unknown; readonly context: PropertyContext }) { const [expanded, setExpanded] = useState(false) if (isBlockReference(value)) { return (
{name}
) } if (value !== null && value !== undefined && typeof value === 'object' && !Array.isArray(value)) { const entries = Object.keys(value as Record) const toggleClass = `property-row__toggle${expanded ? ' property-row__toggle--expanded' : ''}` const toggleLabel = expanded ? `Collapse ${name}` : `Expand ${name}` return (
{expanded && (
{Object.entries(value as Record).map(([k, v]) => ( ))}
)}
) } if (Array.isArray(value)) { const toggleClass = `property-row__toggle${expanded ? ' property-row__toggle--expanded' : ''}` const toggleLabel = expanded ? `Collapse ${name}` : `Expand ${name}` const allBlocks = value.length > 0 && value.every(isBlockReference) return (
{expanded && (
{value.map((item, i) => ( ))}
)}
) } return (
{name}
) } // --- Unit detail --- interface UnitDetailProps { readonly unit: TraceUnitMessage readonly swatchColor: { readonly fill: string; readonly border: string } readonly traceUnits: readonly TraceUnitMessage[] readonly onSelectUnit: (unit: TraceUnitMessage | undefined) => void } export default function UnitDetail({ unit, swatchColor, traceUnits, onSelectUnit }: UnitDetailProps) { // Clear any page highlight when the shown unit changes or the panel closes. useEffect(() => () => clearBlockHighlight(), [unit]) const context: PropertyContext = { traceUnits, onSelectUnit, } const ownProperties = unit.properties ?? {} const hasOwnProps = Object.keys(ownProperties).length > 0 // Render block units carry no properties payload of their own, but share their nodeId with the // resolve.block unit that holds the resolved block properties — borrow those when we have none. const borrowedUnit = !hasOwnProps && unit.nodeId !== undefined ? traceUnits.find( candidate => candidate.kind === 'resolve.block' && candidate.nodeId === unit.nodeId && candidate.properties !== undefined && Object.keys(candidate.properties).length > 0, ) : undefined const properties = hasOwnProps ? ownProperties : borrowedUnit?.properties ?? {} const hasProps = Object.keys(properties).length > 0 const sectionTitle = !hasOwnProps && borrowedUnit !== undefined ? 'Props (resolved block)' : 'Props' return (
{formatKind(unit.kind)}
{sectionTitle}
{hasProps ? (
{Object.entries(properties).map(([key, value]) => ( ))}
) : (
No props
)}
) }