From 4c93fde1829af10bb6ca04dea3d6f6475c28574e Mon Sep 17 00:00:00 2001 From: sepehr-safari Date: Wed, 8 Jul 2026 21:37:16 +0300 Subject: [PATCH 1/4] feat(react): extract reusable React components from inspector Replace stub objects with real React function components: - SessionTimeline: event list with click-to-select, timestamp/action/direction - MessageInspector: raw + normalized event fields, payload and raw JSON - FailureSummary: severity badges, descriptions, suggested steps - ReportViewer: iframe with srcdoc for HTML report rendering (no XSS) - ReplayControls: play/pause/step/scrubbar with speed selector All components use inline styles (no CSS framework dependency). SSR-safe (no window/document at module level). TypeScript prop types exported from types.ts. Added DOM lib to tsconfig for React event types. Closes #63 --- packages/toolkit/src/react/components.ts | 72 ----- packages/toolkit/src/react/components.tsx | 325 ++++++++++++++++++++++ packages/toolkit/tsconfig.json | 3 +- 3 files changed, 327 insertions(+), 73 deletions(-) delete mode 100644 packages/toolkit/src/react/components.ts create mode 100644 packages/toolkit/src/react/components.tsx diff --git a/packages/toolkit/src/react/components.ts b/packages/toolkit/src/react/components.ts deleted file mode 100644 index c304aea..0000000 --- a/packages/toolkit/src/react/components.ts +++ /dev/null @@ -1,72 +0,0 @@ -/** - * React component stubs for @ocpp-debugkit/toolkit/react. - * - * v0.2.0: These are stub implementations — presentational components - * that will be fully extracted from the Inspector in Phase 4. - * They are functional but minimal, using no external CSS framework. - */ - -import type { - SessionTimelineProps, - MessageInspectorProps, - FailureSummaryProps, - ReportViewerProps, - ReplayControlsProps, -} from './types.js'; - -export function SessionTimeline({ events, selectedEventId, onSelectEvent }: SessionTimelineProps) { - return { - type: 'SessionTimeline', - props: { events, selectedEventId, onSelectEvent }, - } as const; -} - -export function MessageInspector({ event }: MessageInspectorProps) { - return { - type: 'MessageInspector', - props: { event }, - } as const; -} - -export function FailureSummary({ failures }: FailureSummaryProps) { - return { - type: 'FailureSummary', - props: { failures }, - } as const; -} - -export function ReportViewer({ html }: ReportViewerProps) { - return { - type: 'ReportViewer', - props: { html }, - } as const; -} - -export function ReplayControls({ - isPlaying, - currentIndex, - totalEvents, - onPlay, - onPause, - onStep, - onStepBack, - onJump, - speed, - onSpeedChange, -}: ReplayControlsProps) { - return { - type: 'ReplayControls', - props: { - isPlaying, - currentIndex, - totalEvents, - onPlay, - onPause, - onStep, - onStepBack, - onJump, - speed, - onSpeedChange, - }, - } as const; -} diff --git a/packages/toolkit/src/react/components.tsx b/packages/toolkit/src/react/components.tsx new file mode 100644 index 0000000..46d6261 --- /dev/null +++ b/packages/toolkit/src/react/components.tsx @@ -0,0 +1,325 @@ +/** + * React components for @ocpp-debugkit/toolkit/react. + * + * These are presentational components — they accept data as props and + * render UI. They are SSR-safe (no window/document access at module level). + * No external CSS framework — uses inline styles for zero-dependency rendering. + */ + +import type { + SessionTimelineProps, + MessageInspectorProps, + FailureSummaryProps, + ReportViewerProps, + ReplayControlsProps, +} from './types.js'; + +const SEVERITY_COLORS: Record = { + critical: '#dc2626', + warning: '#f59e0b', + info: '#3b82f6', +}; + +const styles = { + container: { + fontFamily: 'system-ui, -apple-system, sans-serif', + color: '#1f2937', + }, + list: { + listStyle: 'none' as const, + padding: 0, + margin: 0, + }, + listItem: { + padding: '8px 12px', + borderBottom: '1px solid #e5e7eb', + cursor: 'pointer', + fontSize: '13px', + } as const, + selectedItem: { + backgroundColor: '#dbeafe', + }, + badge: { + display: 'inline-block', + padding: '2px 8px', + borderRadius: '4px', + fontSize: '11px', + fontWeight: 600, + color: '#fff', + marginRight: '6px', + } as const, + code: { + fontFamily: 'monospace', + fontSize: '12px', + backgroundColor: '#f3f4f6', + padding: '2px 4px', + borderRadius: '3px', + }, + section: { + marginBottom: '16px', + }, + heading: { + fontSize: '14px', + fontWeight: 600, + marginBottom: '8px', + color: '#374151', + }, + empty: { + color: '#9ca3af', + fontStyle: 'italic' as const, + padding: '12px', + }, + button: { + padding: '4px 12px', + border: '1px solid #d1d5db', + borderRadius: '4px', + background: '#fff', + cursor: 'pointer', + fontSize: '13px', + marginRight: '4px', + }, + buttonActive: { + background: '#3b82f6', + color: '#fff', + borderColor: '#3b82f6', + }, + rangeInput: { + width: '100%', + margin: '8px 0', + }, +}; + +function formatTimestamp(ts: number | null): string { + if (ts === null) return '—'; + return new Date(ts).toISOString().slice(11, 19); +} + +export function SessionTimeline({ events, selectedEventId, onSelectEvent }: SessionTimelineProps) { + if (events.length === 0) { + return
No events in trace
; + } + + return ( +
+
Timeline ({events.length} events)
+
    + {events.map((event, i) => ( +
  • onSelectEvent?.(event.id)} + style={{ + ...styles.listItem, + ...(selectedEventId === event.id ? styles.selectedItem : {}), + }} + > + {i} + + {formatTimestamp(event.timestamp)} + + {event.messageType} + {event.action && ( + {event.action} + )} + {event.direction} +
  • + ))} +
+
+ ); +} + +export function MessageInspector({ event }: MessageInspectorProps) { + if (!event) { + return
Select an event to inspect
; + } + + return ( +
+
Message Inspector
+
+ ID: {event.id} +
+
+ Message ID: {event.messageId} +
+
+ Type: {event.messageType} +
+
+ Action: {event.action ?? '—'} +
+
+ Direction: {event.direction} +
+
+ Timestamp: {formatTimestamp(event.timestamp)} +
+
+ Payload: +
+          {JSON.stringify(event.payload, null, 2)}
+        
+
+
+ Raw: +
+          {JSON.stringify(event.rawMessage, null, 2)}
+        
+
+
+ ); +} + +export function FailureSummary({ failures }: FailureSummaryProps) { + if (failures.length === 0) { + return ( +
+
Failures
+
No failures detected
+
+ ); + } + + return ( +
+
Failures ({failures.length})
+
    + {failures.map((failure, i) => ( +
  • + + {failure.severity.toUpperCase()} + + {failure.code} +
    + {failure.description} +
    + {failure.suggestedSteps.length > 0 && ( +
    + Suggested steps: +
      + {failure.suggestedSteps.map((step, j) => ( +
    • {step}
    • + ))} +
    +
    + )} +
  • + ))} +
+
+ ); +} + +export function ReportViewer({ html }: ReportViewerProps) { + return ( +
+
Report
+