@rozenite/vite-plugin 1.8.1 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/bundle-dts.d.ts +1 -0
  2. package/dist/bundle-dts.d.ts.map +1 -1
  3. package/dist/client-plugin.d.ts.map +1 -1
  4. package/dist/dev-host/App.d.ts +3 -0
  5. package/dist/dev-host/App.d.ts.map +1 -0
  6. package/dist/dev-host/components/DispatchForm.d.ts +13 -0
  7. package/dist/dev-host/components/DispatchForm.d.ts.map +1 -0
  8. package/dist/dev-host/components/MessageDetailsPane.d.ts +18 -0
  9. package/dist/dev-host/components/MessageDetailsPane.d.ts.map +1 -0
  10. package/dist/dev-host/components/MessageLogPane.d.ts +10 -0
  11. package/dist/dev-host/components/MessageLogPane.d.ts.map +1 -0
  12. package/dist/dev-host/components/MessagePayloadDetail.d.ts +6 -0
  13. package/dist/dev-host/components/MessagePayloadDetail.d.ts.map +1 -0
  14. package/dist/dev-host/components/PanelTabs.d.ts +9 -0
  15. package/dist/dev-host/components/PanelTabs.d.ts.map +1 -0
  16. package/dist/dev-host/components/ResizeHandle.d.ts +12 -0
  17. package/dist/dev-host/components/ResizeHandle.d.ts.map +1 -0
  18. package/dist/dev-host/components/icons.d.ts +3 -0
  19. package/dist/dev-host/components/icons.d.ts.map +1 -0
  20. package/dist/dev-host/components/ui/ScrollArea.d.ts +6 -0
  21. package/dist/dev-host/components/ui/ScrollArea.d.ts.map +1 -0
  22. package/dist/dev-host/components/ui/Tabs.d.ts +5 -0
  23. package/dist/dev-host/components/ui/Tabs.d.ts.map +1 -0
  24. package/dist/dev-host/constants.d.ts +28 -0
  25. package/dist/dev-host/constants.d.ts.map +1 -0
  26. package/dist/dev-host/index.d.ts +1 -0
  27. package/dist/dev-host/index.d.ts.map +1 -0
  28. package/dist/dev-host/types.d.ts +29 -0
  29. package/dist/dev-host/types.d.ts.map +1 -0
  30. package/dist/dev-host/utils.d.ts +13 -0
  31. package/dist/dev-host/utils.d.ts.map +1 -0
  32. package/dist/index.cjs +101 -12
  33. package/dist/index.js +101 -12
  34. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  35. package/package.json +14 -3
  36. package/src/dev-host/App.tsx +403 -0
  37. package/src/dev-host/components/DispatchForm.tsx +87 -0
  38. package/src/dev-host/components/MessageDetailsPane.tsx +108 -0
  39. package/src/dev-host/components/MessageLogPane.tsx +84 -0
  40. package/src/dev-host/components/MessagePayloadDetail.tsx +32 -0
  41. package/src/dev-host/components/PanelTabs.tsx +22 -0
  42. package/src/dev-host/components/ResizeHandle.tsx +33 -0
  43. package/src/dev-host/components/icons.tsx +27 -0
  44. package/src/dev-host/components/ui/ScrollArea.tsx +46 -0
  45. package/src/dev-host/components/ui/Tabs.tsx +33 -0
  46. package/src/dev-host/constants.ts +29 -0
  47. package/src/dev-host/dev-host.html +11 -0
  48. package/src/dev-host/index.tsx +19 -0
  49. package/src/dev-host/styles.css +631 -0
  50. package/src/dev-host/types.ts +33 -0
  51. package/src/dev-host/utils.ts +96 -0
  52. package/templates/panel.ejs +1 -1
@@ -0,0 +1,403 @@
1
+ import {
2
+ useEffect,
3
+ useRef,
4
+ useState,
5
+ type CSSProperties,
6
+ type FormEvent,
7
+ type PointerEvent as ReactPointerEvent,
8
+ } from 'react';
9
+ import {
10
+ DEFAULT_COMMAND_WIDTH,
11
+ DEFAULT_DEVTOOLS_HEIGHT,
12
+ DETAILS_PANEL_WIDTH,
13
+ MIN_COMMAND_WIDTH,
14
+ MIN_DETAILS_WIDTH,
15
+ MIN_DEVTOOLS_HEIGHT,
16
+ MIN_IFRAME_HEIGHT,
17
+ SPLITTER_SIZE,
18
+ } from './constants.js';
19
+ import type {
20
+ DevHostPanelEntry,
21
+ DevHostState,
22
+ MessageEntry,
23
+ ResizeHandleId,
24
+ ResizeSession,
25
+ } from './types.js';
26
+ import { clamp, createMessageEntry, getInitialPanel, isPluginMessage } from './utils.js';
27
+ import { DispatchForm } from './components/DispatchForm.js';
28
+ import {
29
+ MessageDetailsPane,
30
+ getDispatcherValuesFromMessage,
31
+ } from './components/MessageDetailsPane.js';
32
+ import { MessageLogPane } from './components/MessageLogPane.js';
33
+ import { PanelTabs } from './components/PanelTabs.js';
34
+ import { ResizeHandle } from './components/ResizeHandle.js';
35
+
36
+ type CSSVariables = CSSProperties & Record<`--${string}`, string>;
37
+
38
+ const getViewportMatch = () => {
39
+ return window.matchMedia('(max-width: 960px)').matches;
40
+ };
41
+
42
+ export const App = ({ packageName, packageDescription, panels }: DevHostState) => {
43
+ const [activePanel, setActivePanel] = useState<DevHostPanelEntry | null>(() => {
44
+ return getInitialPanel(panels);
45
+ });
46
+ const [commandType, setCommandType] = useState('');
47
+ const [commandPayload, setCommandPayload] = useState('');
48
+ const [messages, setMessages] = useState<MessageEntry[]>([]);
49
+ const [selectedMessageId, setSelectedMessageId] = useState<string | null>(null);
50
+ const [isDetailsOpen, setIsDetailsOpen] = useState(false);
51
+ const [devToolsHeight, setDevToolsHeight] = useState(DEFAULT_DEVTOOLS_HEIGHT);
52
+ const [commandWidth, setCommandWidth] = useState(DEFAULT_COMMAND_WIDTH);
53
+ const [detailsWidth, setDetailsWidth] = useState(DETAILS_PANEL_WIDTH);
54
+ const [activeResizeHandle, setActiveResizeHandle] = useState<ResizeHandleId | null>(null);
55
+ const [isNarrowViewport, setIsNarrowViewport] = useState(getViewportMatch);
56
+ const workspaceRef = useRef<HTMLElement | null>(null);
57
+ const logWorkspaceRef = useRef<HTMLDivElement | null>(null);
58
+ const devtoolsRef = useRef<HTMLElement | null>(null);
59
+ const resizeSessionRef = useRef<ResizeSession | null>(null);
60
+ const iframeRef = useRef<HTMLIFrameElement | null>(null);
61
+
62
+ const activeSource = activePanel?.source ?? '';
63
+ const activeLabel = activePanel?.label ?? '';
64
+ const emptyState = panels.length === 0;
65
+ const selectedMessage = messages.find((message) => message.id === selectedMessageId) ?? null;
66
+ const trimmedCommandType = commandType.trim();
67
+ const trimmedCommandPayload = commandPayload.trim();
68
+ const hasCommandType = trimmedCommandType.length > 0;
69
+ const hasValidCommandPayload = (() => {
70
+ if (!trimmedCommandPayload) {
71
+ return false;
72
+ }
73
+
74
+ try {
75
+ JSON.parse(commandPayload);
76
+ return true;
77
+ } catch {
78
+ return false;
79
+ }
80
+ })();
81
+ const canDispatch = hasCommandType && hasValidCommandPayload;
82
+ const panelDescription = packageDescription.trim();
83
+
84
+ useEffect(() => {
85
+ document.title = `${packageName} Dev Host`;
86
+ }, [packageName]);
87
+
88
+ useEffect(() => {
89
+ const mediaQuery = window.matchMedia('(max-width: 960px)');
90
+
91
+ const handleChange = (event: MediaQueryListEvent) => {
92
+ setIsNarrowViewport(event.matches);
93
+ };
94
+
95
+ setIsNarrowViewport(mediaQuery.matches);
96
+ mediaQuery.addEventListener('change', handleChange);
97
+
98
+ return () => {
99
+ mediaQuery.removeEventListener('change', handleChange);
100
+ };
101
+ }, []);
102
+
103
+ const selectPanel = (value: string) => {
104
+ const nextPanel = panels.find((panel) => panel.source === value);
105
+ if (!nextPanel) {
106
+ return;
107
+ }
108
+
109
+ setActivePanel(nextPanel);
110
+ const nextUrl = new URL(window.location.href);
111
+ nextUrl.searchParams.set('panel', nextPanel.label);
112
+ window.history.replaceState(null, '', nextUrl);
113
+ };
114
+
115
+ const appendMessage = (input: Omit<MessageEntry, 'id' | 'date'>) => {
116
+ const nextEntry = createMessageEntry(input);
117
+
118
+ setMessages((current) => [nextEntry, ...current]);
119
+ setSelectedMessageId(nextEntry.id);
120
+ setIsDetailsOpen(true);
121
+ };
122
+
123
+ useEffect(() => {
124
+ const handlePointerMove = (event: PointerEvent) => {
125
+ resizeSessionRef.current?.onMove(event);
126
+ };
127
+
128
+ const stopDragging = () => {
129
+ const session = resizeSessionRef.current;
130
+ if (!session) {
131
+ return;
132
+ }
133
+
134
+ if (session.element.hasPointerCapture(session.pointerId)) {
135
+ session.element.releasePointerCapture(session.pointerId);
136
+ }
137
+
138
+ resizeSessionRef.current = null;
139
+ setActiveResizeHandle(null);
140
+ };
141
+
142
+ window.addEventListener('pointermove', handlePointerMove);
143
+ window.addEventListener('pointerup', stopDragging);
144
+ window.addEventListener('pointercancel', stopDragging);
145
+
146
+ return () => {
147
+ window.removeEventListener('pointermove', handlePointerMove);
148
+ window.removeEventListener('pointerup', stopDragging);
149
+ window.removeEventListener('pointercancel', stopDragging);
150
+ };
151
+ }, []);
152
+
153
+ const startResize = (
154
+ handleId: ResizeHandleId,
155
+ event: ReactPointerEvent<HTMLDivElement>,
156
+ onMove: (event: PointerEvent) => void,
157
+ ) => {
158
+ event.preventDefault();
159
+ event.currentTarget.setPointerCapture(event.pointerId);
160
+ resizeSessionRef.current = {
161
+ handleId,
162
+ pointerId: event.pointerId,
163
+ element: event.currentTarget,
164
+ onMove,
165
+ };
166
+ setActiveResizeHandle(handleId);
167
+ };
168
+
169
+ useEffect(() => {
170
+ const handleMessage = (event: MessageEvent) => {
171
+ if (event.source !== iframeRef.current?.contentWindow) {
172
+ return;
173
+ }
174
+
175
+ if (
176
+ typeof event.data !== 'object' ||
177
+ event.data === null ||
178
+ !('type' in event.data) ||
179
+ event.data.type !== 'rozenite-message' ||
180
+ !('payload' in event.data)
181
+ ) {
182
+ return;
183
+ }
184
+
185
+ const payload = event.data.payload;
186
+
187
+ if (!isPluginMessage(payload)) {
188
+ return;
189
+ }
190
+
191
+ appendMessage({
192
+ direction: 'out',
193
+ type: payload.type,
194
+ payload: payload.payload,
195
+ });
196
+ };
197
+
198
+ window.addEventListener('message', handleMessage);
199
+
200
+ return () => {
201
+ window.removeEventListener('message', handleMessage);
202
+ };
203
+ }, []);
204
+
205
+ const resetForm = () => {
206
+ setCommandType('');
207
+ setCommandPayload('');
208
+ };
209
+
210
+ const clearMessages = () => {
211
+ setMessages([]);
212
+ setSelectedMessageId(null);
213
+ setIsDetailsOpen(false);
214
+ };
215
+
216
+ const resizeDevtoolsHeight = (event: PointerEvent) => {
217
+ const workspace = workspaceRef.current;
218
+ if (!workspace) {
219
+ return;
220
+ }
221
+
222
+ const bounds = workspace.getBoundingClientRect();
223
+ const nextHeight = bounds.bottom - event.clientY;
224
+ const maxHeight = Math.max(MIN_DEVTOOLS_HEIGHT, bounds.height - MIN_IFRAME_HEIGHT - 12);
225
+
226
+ setDevToolsHeight(clamp(nextHeight, MIN_DEVTOOLS_HEIGHT, maxHeight));
227
+ };
228
+
229
+ const resizeCommandPane = (event: PointerEvent) => {
230
+ const devtools = devtoolsRef.current;
231
+ if (!devtools) {
232
+ return;
233
+ }
234
+
235
+ const bounds = devtools.getBoundingClientRect();
236
+ const nextWidth = isNarrowViewport
237
+ ? bounds.bottom - event.clientY
238
+ : bounds.right - event.clientX;
239
+ const maxWidth = Math.max(MIN_COMMAND_WIDTH, bounds.width - 280);
240
+
241
+ setCommandWidth(clamp(nextWidth, MIN_COMMAND_WIDTH, maxWidth));
242
+ };
243
+
244
+ const resizeDetailsPane = (event: PointerEvent) => {
245
+ const logWorkspace = logWorkspaceRef.current;
246
+ if (!logWorkspace || !isDetailsOpen || !selectedMessage) {
247
+ return;
248
+ }
249
+
250
+ const bounds = logWorkspace.getBoundingClientRect();
251
+ const nextWidth = isNarrowViewport
252
+ ? bounds.bottom - event.clientY
253
+ : bounds.right - event.clientX;
254
+ const maxWidth = Math.max(MIN_DETAILS_WIDTH, bounds.width - 280 - SPLITTER_SIZE);
255
+
256
+ setDetailsWidth(clamp(nextWidth, MIN_DETAILS_WIDTH, maxWidth));
257
+ };
258
+
259
+ const handleDispatch = (event: FormEvent<HTMLFormElement>) => {
260
+ event.preventDefault();
261
+
262
+ if (!canDispatch) {
263
+ return;
264
+ }
265
+
266
+ const type = trimmedCommandType;
267
+
268
+ let payload: unknown;
269
+
270
+ try {
271
+ payload = JSON.parse(commandPayload);
272
+ } catch (error) {
273
+ window.alert(
274
+ error instanceof Error
275
+ ? `Payload must be valid JSON. ${error.message}`
276
+ : 'Payload must be valid JSON.',
277
+ );
278
+ return;
279
+ }
280
+
281
+ iframeRef.current?.contentWindow?.postMessage(
282
+ {
283
+ pluginId: packageName,
284
+ type,
285
+ payload,
286
+ },
287
+ '*',
288
+ );
289
+
290
+ appendMessage({
291
+ direction: 'in',
292
+ type,
293
+ payload,
294
+ });
295
+
296
+ resetForm();
297
+ };
298
+
299
+ return (
300
+ <div className="rz-shell">
301
+ <header className="rz-topbar">
302
+ <div title={panelDescription || undefined}>
303
+ <PanelTabs panels={panels} activeSource={activeSource} onValueChange={selectPanel} />
304
+ </div>
305
+ </header>
306
+
307
+ <main
308
+ ref={workspaceRef}
309
+ className="rz-workspace"
310
+ style={{ '--rz-devtools-height': `${devToolsHeight}px` } as CSSVariables}
311
+ >
312
+ <section className="rz-card">
313
+ {emptyState ? (
314
+ <div className="rz-empty-state">No panels were defined in rozenite.config.ts.</div>
315
+ ) : (
316
+ <iframe
317
+ key={activeSource}
318
+ ref={iframeRef}
319
+ title={activeLabel || 'Rozenite panel preview'}
320
+ src={activeSource}
321
+ className="rz-iframe"
322
+ data-resizing={activeResizeHandle === 'devtools-height'}
323
+ />
324
+ )}
325
+ </section>
326
+
327
+ <ResizeHandle
328
+ className="rz-resize-handle"
329
+ isDragging={activeResizeHandle === 'devtools-height'}
330
+ orientation="horizontal"
331
+ label="Resize DevTools"
332
+ onPointerDown={(event) => startResize('devtools-height', event, resizeDevtoolsHeight)}
333
+ />
334
+
335
+ <section
336
+ ref={devtoolsRef}
337
+ className="rz-devtools"
338
+ style={
339
+ {
340
+ '--rz-command-width': `${commandWidth}px`,
341
+ '--rz-command-splitter-width': `${SPLITTER_SIZE}px`,
342
+ } as CSSVariables
343
+ }
344
+ >
345
+ <div
346
+ ref={logWorkspaceRef}
347
+ className="rz-log-workspace"
348
+ style={
349
+ {
350
+ '--rz-details-width':
351
+ isDetailsOpen && selectedMessage ? `${detailsWidth}px` : '0px',
352
+ '--rz-details-splitter-width':
353
+ isDetailsOpen && selectedMessage ? `${SPLITTER_SIZE}px` : '0px',
354
+ } as CSSVariables
355
+ }
356
+ >
357
+ <MessageLogPane
358
+ messages={messages}
359
+ selectedMessageId={selectedMessageId}
360
+ onSelectMessage={(messageId) => {
361
+ setSelectedMessageId(messageId);
362
+ setIsDetailsOpen(true);
363
+ }}
364
+ onClearMessages={clearMessages}
365
+ />
366
+
367
+ <MessageDetailsPane
368
+ selectedMessage={selectedMessage}
369
+ isOpen={isDetailsOpen}
370
+ isNarrowViewport={isNarrowViewport}
371
+ activeResizeHandle={activeResizeHandle}
372
+ onClose={() => setIsDetailsOpen(false)}
373
+ onUseMessage={(message) => {
374
+ const nextValues = getDispatcherValuesFromMessage(message);
375
+ setCommandType(nextValues.commandType);
376
+ setCommandPayload(nextValues.commandPayload);
377
+ }}
378
+ onResizeStart={(event) => startResize('details-width', event, resizeDetailsPane)}
379
+ />
380
+ </div>
381
+
382
+ <ResizeHandle
383
+ className="rz-column-resize-handle"
384
+ isDragging={activeResizeHandle === 'command-width'}
385
+ orientation={isNarrowViewport ? 'horizontal' : 'vertical'}
386
+ label="Resize command dispatcher"
387
+ onPointerDown={(event) => startResize('command-width', event, resizeCommandPane)}
388
+ />
389
+
390
+ <DispatchForm
391
+ commandType={commandType}
392
+ commandPayload={commandPayload}
393
+ canDispatch={canDispatch}
394
+ onCommandTypeChange={setCommandType}
395
+ onCommandPayloadChange={setCommandPayload}
396
+ onReset={resetForm}
397
+ onSubmit={handleDispatch}
398
+ />
399
+ </section>
400
+ </main>
401
+ </div>
402
+ );
403
+ };
@@ -0,0 +1,87 @@
1
+ import type { FormEvent } from 'react';
2
+ import { ClearIcon, SendIcon } from './icons.js';
3
+ import { ScrollArea } from './ui/ScrollArea.js';
4
+
5
+ type DispatchFormProps = {
6
+ commandType: string;
7
+ commandPayload: string;
8
+ canDispatch: boolean;
9
+ onCommandTypeChange: (value: string) => void;
10
+ onCommandPayloadChange: (value: string) => void;
11
+ onReset: () => void;
12
+ onSubmit: (event: FormEvent<HTMLFormElement>) => void;
13
+ };
14
+
15
+ export const DispatchForm = ({
16
+ commandType,
17
+ commandPayload,
18
+ canDispatch,
19
+ onCommandTypeChange,
20
+ onCommandPayloadChange,
21
+ onReset,
22
+ onSubmit,
23
+ }: DispatchFormProps) => {
24
+ return (
25
+ <div className="rz-pane">
26
+ <div className="rz-sidebar">
27
+ <div className="rz-sidebar-header">
28
+ <div className="rz-sidebar-title">Dispatch Message</div>
29
+ </div>
30
+
31
+ <ScrollArea className="rz-sidebar-scroll">
32
+ <form className="rz-command-form" onSubmit={onSubmit}>
33
+ <div className="rz-field">
34
+ <label className="rz-label" htmlFor="command-type">
35
+ Command
36
+ </label>
37
+ <input
38
+ id="command-type"
39
+ className="rz-input"
40
+ value={commandType}
41
+ onChange={(event) => onCommandTypeChange(event.currentTarget.value)}
42
+ placeholder="get-snapshot"
43
+ spellCheck={false}
44
+ />
45
+ </div>
46
+
47
+ <div className="rz-field">
48
+ <label className="rz-label" htmlFor="command-payload">
49
+ Payload
50
+ </label>
51
+ <textarea
52
+ id="command-payload"
53
+ className="rz-textarea"
54
+ value={commandPayload}
55
+ onChange={(event) => onCommandPayloadChange(event.currentTarget.value)}
56
+ placeholder='{"example": true}'
57
+ spellCheck={false}
58
+ />
59
+ </div>
60
+
61
+ <div className="rz-button-row">
62
+ <button
63
+ type="button"
64
+ className="rz-sidebar-close"
65
+ aria-label="Reset dispatcher"
66
+ title="Reset dispatcher"
67
+ onClick={onReset}
68
+ >
69
+ <ClearIcon />
70
+ </button>
71
+
72
+ <button
73
+ type="submit"
74
+ className="rz-sidebar-close rz-sidebar-action-primary"
75
+ aria-label="Dispatch message"
76
+ title="Dispatch message"
77
+ disabled={!canDispatch}
78
+ >
79
+ <SendIcon />
80
+ </button>
81
+ </div>
82
+ </form>
83
+ </ScrollArea>
84
+ </div>
85
+ </div>
86
+ );
87
+ };
@@ -0,0 +1,108 @@
1
+ import type { PointerEvent as ReactPointerEvent } from 'react';
2
+ import type { MessageEntry, ResizeHandleId } from '../types.js';
3
+ import { formatMessageDate, formatPayloadForCommandInput } from '../utils.js';
4
+ import { MessagePayloadDetail } from './MessagePayloadDetail.js';
5
+ import { ResizeHandle } from './ResizeHandle.js';
6
+ import { ScrollArea } from './ui/ScrollArea.js';
7
+ import { SendIcon } from './icons.js';
8
+
9
+ type MessageDetailsPaneProps = {
10
+ selectedMessage: MessageEntry | null;
11
+ isOpen: boolean;
12
+ isNarrowViewport: boolean;
13
+ activeResizeHandle: ResizeHandleId | null;
14
+ onClose: () => void;
15
+ onUseMessage: (message: MessageEntry) => void;
16
+ onResizeStart: (event: ReactPointerEvent<HTMLDivElement>) => void;
17
+ };
18
+
19
+ export const MessageDetailsPane = ({
20
+ selectedMessage,
21
+ isOpen,
22
+ isNarrowViewport,
23
+ activeResizeHandle,
24
+ onClose,
25
+ onUseMessage,
26
+ onResizeStart,
27
+ }: MessageDetailsPaneProps) => {
28
+ const isHidden = !isOpen || !selectedMessage;
29
+
30
+ return (
31
+ <>
32
+ <ResizeHandle
33
+ className="rz-column-resize-handle"
34
+ isDragging={activeResizeHandle === 'details-width'}
35
+ isHidden={isHidden}
36
+ orientation={isNarrowViewport ? 'horizontal' : 'vertical'}
37
+ label="Resize message details"
38
+ onPointerDown={onResizeStart}
39
+ />
40
+
41
+ <div className="rz-pane" data-hidden={isHidden} aria-hidden={isHidden}>
42
+ <div className="rz-sidebar">
43
+ <div className="rz-sidebar-header">
44
+ <div className="rz-sidebar-title">Message Details</div>
45
+ <div className="rz-header-actions">
46
+ <button
47
+ type="button"
48
+ className="rz-sidebar-close"
49
+ aria-label="Use message in dispatcher"
50
+ title="Use message in dispatcher"
51
+ onClick={() => {
52
+ if (selectedMessage) {
53
+ onUseMessage(selectedMessage);
54
+ }
55
+ }}
56
+ >
57
+ <SendIcon />
58
+ </button>
59
+
60
+ <button
61
+ type="button"
62
+ className="rz-sidebar-close"
63
+ aria-label="Close message details"
64
+ onClick={onClose}
65
+ >
66
+ ×
67
+ </button>
68
+ </div>
69
+ </div>
70
+
71
+ <ScrollArea className="rz-sidebar-scroll">
72
+ {selectedMessage ? (
73
+ <div className="rz-message-detail">
74
+ <div className="rz-detail-section">
75
+ <div className="rz-label">Date</div>
76
+ <div className="rz-detail-value rz-detail-mono">
77
+ {formatMessageDate(selectedMessage.date)}
78
+ </div>
79
+ </div>
80
+
81
+ <div className="rz-detail-section">
82
+ <div className="rz-label">Type</div>
83
+ <div className="rz-detail-value rz-detail-mono">{selectedMessage.type}</div>
84
+ </div>
85
+
86
+ <div className="rz-detail-section">
87
+ <div className="rz-label">Payload</div>
88
+ <div className="rz-detail-payload">
89
+ <MessagePayloadDetail payload={selectedMessage.payload} />
90
+ </div>
91
+ </div>
92
+ </div>
93
+ ) : (
94
+ <div className="rz-empty-state">Select a message to inspect its details.</div>
95
+ )}
96
+ </ScrollArea>
97
+ </div>
98
+ </div>
99
+ </>
100
+ );
101
+ };
102
+
103
+ export const getDispatcherValuesFromMessage = (message: MessageEntry) => {
104
+ return {
105
+ commandType: message.type,
106
+ commandPayload: formatPayloadForCommandInput(message.payload),
107
+ };
108
+ };
@@ -0,0 +1,84 @@
1
+ import type { MessageEntry } from '../types.js';
2
+ import { cn, formatMessageTableDate, formatPayloadPreview } from '../utils.js';
3
+ import { ScrollArea } from './ui/ScrollArea.js';
4
+ import { ClearIcon } from './icons.js';
5
+
6
+ type MessageLogPaneProps = {
7
+ messages: MessageEntry[];
8
+ selectedMessageId: string | null;
9
+ onSelectMessage: (messageId: string) => void;
10
+ onClearMessages: () => void;
11
+ };
12
+
13
+ export const MessageLogPane = ({
14
+ messages,
15
+ selectedMessageId,
16
+ onSelectMessage,
17
+ onClearMessages,
18
+ }: MessageLogPaneProps) => {
19
+ return (
20
+ <div className="rz-pane">
21
+ <div className="rz-log-pane">
22
+ <div className="rz-sidebar-header">
23
+ <div className="rz-sidebar-title">Message Log</div>
24
+ <div className="rz-header-actions">
25
+ <button
26
+ type="button"
27
+ className="rz-sidebar-close"
28
+ onClick={onClearMessages}
29
+ disabled={messages.length === 0}
30
+ aria-label="Clear message log"
31
+ title="Clear message log"
32
+ >
33
+ <ClearIcon />
34
+ </button>
35
+ </div>
36
+ </div>
37
+
38
+ <ScrollArea className="rz-sidebar-scroll">
39
+ <div className="rz-message-list">
40
+ <div className="rz-message-list-header">
41
+ <div className="rz-message-header-cell">Dir</div>
42
+ <div className="rz-message-header-cell">Date</div>
43
+ <div className="rz-message-header-cell">Type</div>
44
+ <div className="rz-message-header-cell">Payload</div>
45
+ </div>
46
+
47
+ {messages.map((message) => (
48
+ <button
49
+ key={message.id}
50
+ type="button"
51
+ className="rz-message-row"
52
+ data-selected={message.id === selectedMessageId}
53
+ onClick={() => onSelectMessage(message.id)}
54
+ >
55
+ <div
56
+ className={cn(
57
+ 'rz-message-cell rz-message-direction',
58
+ message.direction === 'in' ? 'rz-message-dir-in' : 'rz-message-dir-out',
59
+ )}
60
+ aria-label={message.direction === 'in' ? 'Sent message' : 'Received message'}
61
+ title={message.direction === 'in' ? 'Sent message' : 'Received message'}
62
+ >
63
+ <span className="rz-message-arrow" aria-hidden="true">
64
+ {message.direction === 'in' ? '↑' : '↓'}
65
+ </span>
66
+ </div>
67
+
68
+ <div className="rz-message-cell rz-message-date">
69
+ {formatMessageTableDate(message.date)}
70
+ </div>
71
+
72
+ <div className="rz-message-cell rz-message-type">{message.type}</div>
73
+
74
+ <pre className="rz-message-cell rz-message-preview">
75
+ {formatPayloadPreview(message.payload)}
76
+ </pre>
77
+ </button>
78
+ ))}
79
+ </div>
80
+ </ScrollArea>
81
+ </div>
82
+ </div>
83
+ );
84
+ };