@noya-app/noya-multiplayer-react 0.1.62 → 0.1.63

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.
@@ -1,246 +1,35 @@
1
1
  "use client";
2
2
 
3
3
  import { Input, OutputTransform } from "@noya-app/noya-schemas";
4
- import { memoGeneric } from "@noya-app/react-utils";
5
- import { ExtendedPathKey, NoyaManager } from "@noya-app/state-manager";
4
+ import { downloadBlob, memoGeneric } from "@noya-app/react-utils";
5
+ import { NoyaManager } from "@noya-app/state-manager";
6
6
  import React, {
7
7
  CSSProperties,
8
8
  ComponentPropsWithoutRef,
9
- FC,
9
+ useCallback,
10
10
  useEffect,
11
11
  useLayoutEffect,
12
12
  } from "react";
13
- import {
14
- ObjectInspector,
15
- ObjectLabel,
16
- ObjectName,
17
- ObjectPreview,
18
- chromeDark,
19
- chromeLight,
20
- } from "react-inspector";
13
+ import { ObjectInspector } from "react-inspector";
21
14
  import { useManagedHistory } from "../hooks";
22
15
  import { useObservable } from "../useObservable";
16
+ import { ColoredDot } from "./ColoredDot";
17
+ import {
18
+ getStateInspectorBorderColor,
19
+ getStateInspectorTheme,
20
+ } from "./inspectorTheme";
21
+ import { EventsSection } from "./sections/EventsSection";
22
+ import { HistorySection } from "./sections/HistorySection";
23
+ import { exportAll, importAll } from "./serialization";
24
+ import { StateInspectorButton } from "./StateInspectorButton";
25
+ import {
26
+ StateInspectorDisclosureRowInner,
27
+ StateInspectorDisclosureSection,
28
+ } from "./StateInspectorDisclosureSection";
29
+ import { StateInspectorRow } from "./StateInspectorRow";
30
+ import { StateInspectorToggleButton } from "./StateInspectorToggleButton";
23
31
  import { useLocalStorageState } from "./useLocalStorageState";
24
-
25
- const lightTheme = {
26
- ...chromeLight,
27
- BASE_BACKGROUND_COLOR: "transparent",
28
- OBJECT_NAME_COLOR: "rgba(0,0,0,0.7)",
29
- } as any;
30
-
31
- const darkTheme = {
32
- ...chromeDark,
33
- BASE_BACKGROUND_COLOR: "transparent",
34
- OBJECT_NAME_COLOR: "rgba(255,255,255,0.7)",
35
- } as any;
36
-
37
- const styles = {
38
- sectionInner: {
39
- flex: "1 1 0",
40
- overflowY: "auto",
41
- overflowX: "hidden",
42
- display: "flex",
43
- flexDirection: "column",
44
- },
45
- } as const;
46
-
47
- function ToggleButton({
48
- showInspector,
49
- setShowInspector,
50
- theme,
51
- anchor,
52
- }: {
53
- showInspector: boolean;
54
- setShowInspector: (value: boolean) => void;
55
- theme: typeof chromeLight;
56
- anchor: StateInspectorAnchor;
57
- }) {
58
- const isRightAnchor = anchor === "right";
59
-
60
- const rightIcon = (
61
- <svg
62
- xmlns="http://www.w3.org/2000/svg"
63
- fill="none"
64
- viewBox="0 0 24 24"
65
- strokeWidth={1.5}
66
- stroke="currentColor"
67
- className="size-6"
68
- >
69
- <path
70
- strokeLinecap="round"
71
- strokeLinejoin="round"
72
- d="m5.25 4.5 7.5 7.5-7.5 7.5m6-15 7.5 7.5-7.5 7.5"
73
- />
74
- </svg>
75
- );
76
-
77
- const leftIcon = (
78
- <svg
79
- xmlns="http://www.w3.org/2000/svg"
80
- fill="none"
81
- viewBox="0 0 24 24"
82
- strokeWidth={1.5}
83
- stroke="currentColor"
84
- className="size-6"
85
- >
86
- <path
87
- strokeLinecap="round"
88
- strokeLinejoin="round"
89
- d="m18.75 4.5-7.5 7.5 7.5 7.5m-6-15L5.25 12l7.5 7.5"
90
- />
91
- </svg>
92
- );
93
-
94
- return (
95
- <span
96
- role="button"
97
- style={{
98
- flex: "0",
99
- appearance: "none",
100
- color: theme.BASE_COLOR,
101
- border: "none",
102
- fontSize: "12px",
103
- whiteSpace: "nowrap",
104
- display: "inline-flex",
105
- alignItems: "center",
106
- justifyContent: "center",
107
- padding: "2px 0",
108
- }}
109
- onClick={(event) => {
110
- event.stopPropagation();
111
-
112
- setShowInspector(!showInspector);
113
- }}
114
- >
115
- {showInspector ? (
116
- "Hide Inspector"
117
- ) : (
118
- <span style={{ width: "12px", height: "12px" }}>
119
- {isRightAnchor ? rightIcon : leftIcon}
120
- </span>
121
- )}
122
- </span>
123
- );
124
- }
125
-
126
- function DisclosureSection({
127
- open,
128
- setOpen,
129
- title,
130
- right,
131
- children,
132
- colorScheme,
133
- isFirst,
134
- style,
135
- }: {
136
- open: boolean;
137
- setOpen?: (value: boolean) => void;
138
- title: React.ReactNode;
139
- right?: React.ReactNode;
140
- children: React.ReactNode;
141
- colorScheme: "light" | "dark";
142
- isFirst?: boolean;
143
- style?: CSSProperties;
144
- }) {
145
- const theme = colorScheme === "light" ? lightTheme : darkTheme;
146
-
147
- const borderColor =
148
- colorScheme === "light" ? "rgba(0,0,0,0.1)" : "rgba(255,255,255,0.1)";
149
-
150
- return (
151
- <div
152
- style={{
153
- flex: open ? "1 1 0" : "0",
154
- display: "flex",
155
- flexDirection: "column",
156
- ...style,
157
- }}
158
- >
159
- <div
160
- onClick={() => setOpen?.(!open)}
161
- style={{
162
- cursor: "default",
163
- fontSize: "12px",
164
- padding: "4px 10px",
165
- display: "flex",
166
- alignItems: "center",
167
- ...(!isFirst && { borderTop: `1px solid ${borderColor}` }),
168
- ...(open && { borderBottom: `1px solid ${borderColor}` }),
169
- }}
170
- >
171
- {setOpen && (
172
- <Arrow
173
- expanded={open}
174
- style={{
175
- fontSize: theme.ARROW_FONT_SIZE,
176
- marginRight: theme.ARROW_MARGIN_RIGHT + 1,
177
- color: theme.ARROW_COLOR,
178
- }}
179
- />
180
- )}
181
- <span style={{ flex: "1 1 0", userSelect: "none" }}>{title}</span>
182
- {right}
183
- </div>
184
- {open && children}
185
- </div>
186
- );
187
- }
188
-
189
- function InspectorRow({
190
- children,
191
- colorScheme,
192
- selected,
193
- style,
194
- variant,
195
- }: {
196
- children: React.ReactNode;
197
- colorScheme: "light" | "dark";
198
- selected?: boolean;
199
- style?: CSSProperties;
200
- variant?: "up" | "down";
201
- }) {
202
- const solidBorderColor =
203
- colorScheme === "light" ? "rgb(223 223 223)" : "rgb(29 29 29)";
204
-
205
- return (
206
- <div
207
- style={{
208
- borderBottom: `1px solid ${solidBorderColor}`,
209
- fontSize: "12px",
210
- fontFamily: "Menlo, monospace",
211
- padding: "2px 12px 1px",
212
- display: "flex",
213
- alignItems: "center",
214
- background:
215
- variant === "up"
216
- ? "rgba(0,255,0,0.1)"
217
- : variant === "down"
218
- ? "transparent"
219
- : selected
220
- ? "rgb(59 130 246 / 15%)"
221
- : undefined,
222
- // background:
223
- // colorScheme === "light"
224
- // ? "rgba(0,0,0,0.05)"
225
- // : "rgba(255,255,255,0.05)",
226
- ...style,
227
- }}
228
- >
229
- <span
230
- style={{
231
- fontFamily: "Menlo, monospace",
232
- fontSize: "11px",
233
- borderRadius: 4,
234
- display: "inline-block",
235
- }}
236
- >
237
- {children}
238
- </span>
239
- </div>
240
- );
241
- }
242
-
243
- const HISTORY_ELEMENT_PREFIX = "noya-multiplayer-history-";
32
+ import { ellipsis, uploadFile } from "./utils";
244
33
 
245
34
  export type StateInspectorAnchor =
246
35
  | "left"
@@ -290,7 +79,6 @@ export const StateInspector = memoGeneric(function StateInspector<
290
79
  }, []);
291
80
 
292
81
  const eventsContainerRef = React.useRef<HTMLDivElement>(null);
293
- const historyContainerRef = React.useRef<HTMLDivElement>(null);
294
82
 
295
83
  const [showInspector, setShowInspector] = useLocalStorageState(
296
84
  "noya-multiplayer-react-show-inspector",
@@ -363,34 +151,8 @@ export const StateInspector = memoGeneric(function StateInspector<
363
151
  ioManager.outputTransformsInitialized$
364
152
  );
365
153
 
366
- useEffect(() => {
367
- if (historyContainerRef.current) {
368
- historyContainerRef.current.scrollTop =
369
- historyContainerRef.current.scrollHeight;
370
- }
371
- }, [historySnapshot]);
372
-
373
- // If history index changes, scroll to the new history entry into view
374
- useEffect(() => {
375
- if (!historyContainerRef.current) return;
376
-
377
- const historyEntry = historyContainerRef.current.querySelector(
378
- `#${HISTORY_ELEMENT_PREFIX}${historySnapshot.historyIndex}`
379
- );
380
-
381
- if (historyEntry) {
382
- historyEntry.scrollIntoView({
383
- block: "nearest",
384
- inline: "nearest",
385
- });
386
- }
387
- }, [historySnapshot.historyIndex]);
388
-
389
- const theme = colorScheme === "light" ? lightTheme : darkTheme;
390
- // const borderColor =
391
- // colorScheme === "light" ? "rgba(0,0,0,0.1)" : "rgba(255,255,255,0.1)";
392
- const solidBorderColor =
393
- colorScheme === "light" ? "rgb(223 223 223)" : "rgb(29 29 29)";
154
+ const theme = getStateInspectorTheme(colorScheme);
155
+ const solidBorderColor = getStateInspectorBorderColor(colorScheme);
394
156
 
395
157
  const baseStyle: CSSProperties = {
396
158
  position: "fixed",
@@ -413,6 +175,25 @@ export const StateInspector = memoGeneric(function StateInspector<
413
175
  lineHeight: "13px",
414
176
  };
415
177
 
178
+ const handleExportAll = useCallback(async () => {
179
+ const buffer = await exportAll(noyaManager);
180
+
181
+ downloadBlob(new Blob([buffer]), "state.zip");
182
+ }, [noyaManager]);
183
+
184
+ const handleImportAll = useCallback(async () => {
185
+ const file = await uploadFile();
186
+ const buffer = await file.arrayBuffer();
187
+
188
+ importAll(buffer, noyaManager, {
189
+ shouldAllowSchemaChange: () => {
190
+ return confirm(
191
+ `The imported state uses a different schema than the schema this app expects. Do you want to continue?`
192
+ );
193
+ },
194
+ });
195
+ }, [noyaManager]);
196
+
416
197
  if (!didMount) return null;
417
198
 
418
199
  if (!showInspector) {
@@ -429,7 +210,7 @@ export const StateInspector = memoGeneric(function StateInspector<
429
210
  }}
430
211
  onClick={() => setShowInspector(true)}
431
212
  >
432
- <ToggleButton
213
+ <StateInspectorToggleButton
433
214
  showInspector={showInspector}
434
215
  setShowInspector={setShowInspector}
435
216
  theme={theme}
@@ -447,7 +228,7 @@ export const StateInspector = memoGeneric(function StateInspector<
447
228
  ...props.style,
448
229
  }}
449
230
  >
450
- <DisclosureSection
231
+ <StateInspectorDisclosureSection
451
232
  isFirst
452
233
  open={showUsers}
453
234
  setOpen={setShowUsers}
@@ -458,7 +239,7 @@ export const StateInspector = memoGeneric(function StateInspector<
458
239
  maxHeight: "200px",
459
240
  }}
460
241
  right={
461
- <ToggleButton
242
+ <StateInspectorToggleButton
462
243
  showInspector={showInspector}
463
244
  setShowInspector={setShowInspector}
464
245
  theme={theme}
@@ -466,9 +247,9 @@ export const StateInspector = memoGeneric(function StateInspector<
466
247
  />
467
248
  }
468
249
  >
469
- <div style={{ ...styles.sectionInner, flex: "0 0 auto" }}>
250
+ <StateInspectorDisclosureRowInner style={{ flex: "0 0 auto" }}>
470
251
  {connectedUsers?.map((user) => (
471
- <InspectorRow
252
+ <StateInspectorRow
472
253
  key={user.id}
473
254
  colorScheme={colorScheme}
474
255
  variant={user.id === userId ? "up" : undefined}
@@ -490,7 +271,7 @@ export const StateInspector = memoGeneric(function StateInspector<
490
271
  />
491
272
  )}
492
273
  {user.name} ({ellipsis(user.id.toString(), 8, "middle")})
493
- </InspectorRow>
274
+ </StateInspectorRow>
494
275
  ))}
495
276
  {!connectedUsers && (
496
277
  <div
@@ -505,9 +286,9 @@ export const StateInspector = memoGeneric(function StateInspector<
505
286
  <span>No connected users</span>
506
287
  </div>
507
288
  )}
508
- </div>
509
- </DisclosureSection>
510
- <DisclosureSection
289
+ </StateInspectorDisclosureRowInner>
290
+ </StateInspectorDisclosureSection>
291
+ <StateInspectorDisclosureSection
511
292
  title={
512
293
  <TitleLabel>
513
294
  Data
@@ -523,10 +304,16 @@ export const StateInspector = memoGeneric(function StateInspector<
523
304
  <span
524
305
  style={{
525
306
  display: "flex",
526
- gap: "4px",
307
+ gap: "12px",
527
308
  }}
528
309
  >
529
- <SmallButton
310
+ <StateInspectorButton theme={theme} onClick={handleImportAll}>
311
+ Import
312
+ </StateInspectorButton>
313
+ <StateInspectorButton theme={theme} onClick={handleExportAll}>
314
+ Export
315
+ </StateInspectorButton>
316
+ <StateInspectorButton
530
317
  // disabled={!historySnapshot.canUndo}
531
318
  theme={theme}
532
319
  onClick={() => {
@@ -534,207 +321,49 @@ export const StateInspector = memoGeneric(function StateInspector<
534
321
  }}
535
322
  >
536
323
  Reset
537
- </SmallButton>
324
+ </StateInspectorButton>
538
325
  </span>
539
326
  }
540
327
  >
541
- <div style={styles.sectionInner}>
542
- <InspectorRow colorScheme={colorScheme}>
328
+ <StateInspectorDisclosureRowInner>
329
+ <StateInspectorRow colorScheme={colorScheme}>
543
330
  <ObjectInspector
544
331
  name={multiplayerStateManager.schema ? "state" : undefined}
545
332
  data={state}
546
333
  theme={theme}
547
334
  />
548
- </InspectorRow>
335
+ </StateInspectorRow>
549
336
  {multiplayerStateManager.schema && (
550
- <InspectorRow colorScheme={colorScheme}>
337
+ <StateInspectorRow colorScheme={colorScheme}>
551
338
  <ObjectInspector
552
339
  name="schema"
553
340
  data={multiplayerStateManager.schema}
554
341
  theme={theme}
555
342
  />
556
- </InspectorRow>
343
+ </StateInspectorRow>
557
344
  )}
558
- </div>
559
- </DisclosureSection>
560
- <DisclosureSection
561
- open={showHistory}
562
- setOpen={setShowHistory}
563
- title="History"
345
+ </StateInspectorDisclosureRowInner>
346
+ </StateInspectorDisclosureSection>
347
+ <HistorySection
348
+ showHistory={showHistory}
349
+ setShowHistory={setShowHistory}
564
350
  colorScheme={colorScheme}
565
- right={
566
- <span
567
- style={{
568
- display: "flex",
569
- gap: "4px",
570
- }}
571
- >
572
- <SmallButton
573
- disabled={!historySnapshot.canUndo}
574
- theme={theme}
575
- onClick={() => {
576
- multiplayerStateManager.undo();
577
- }}
578
- >
579
- <span style={{ width: "12px", height: "12px" }}>
580
- <svg
581
- xmlns="http://www.w3.org/2000/svg"
582
- width="1em"
583
- height="1em"
584
- viewBox="0 0 20 20"
585
- >
586
- <path
587
- fill="currentColor"
588
- d="M12 5H7V2L1 6l6 4V7h5c2.2 0 4 1.8 4 4s-1.8 4-4 4H7v2h5c3.3 0 6-2.7 6-6s-2.7-6-6-6"
589
- />
590
- </svg>
591
- </span>
592
- </SmallButton>
593
- <SmallButton
594
- disabled={!historySnapshot.canRedo}
595
- theme={theme}
596
- onClick={() => {
597
- multiplayerStateManager.redo();
598
- }}
599
- >
600
- <span style={{ width: "12px", height: "12px" }}>
601
- <svg
602
- xmlns="http://www.w3.org/2000/svg"
603
- width="1em"
604
- height="1em"
605
- viewBox="0 0 20 20"
606
- >
607
- <path
608
- fill="currentColor"
609
- d="M8 5h5V2l6 4l-6 4V7H8c-2.2 0-4 1.8-4 4s1.8 4 4 4h5v2H8c-3.3 0-6-2.7-6-6s2.7-6 6-6"
610
- />
611
- </svg>
612
- </span>
613
- </SmallButton>
614
- </span>
615
- }
616
- >
617
- <div ref={historyContainerRef} style={styles.sectionInner}>
618
- <div
619
- id={`${HISTORY_ELEMENT_PREFIX}0`}
620
- style={{
621
- borderBottom: `1px solid ${solidBorderColor}`,
622
- fontSize: "12px",
623
- fontFamily: "Menlo, monospace",
624
- padding: "2px 12px 1px",
625
- display: "flex",
626
- alignItems: "center",
627
- background:
628
- historySnapshot.historyIndex === 0
629
- ? "rgb(59 130 246 / 15%)"
630
- : undefined,
631
- }}
632
- >
633
- <span
634
- style={{
635
- fontFamily: "Menlo, monospace",
636
- fontSize: "11px",
637
- borderRadius: 4,
638
- display: "inline-block",
639
- }}
640
- >
641
- Initial state
642
- </span>
643
- </div>
644
- {historySnapshot.history.map((entry, index) => (
645
- <div
646
- id={`${HISTORY_ELEMENT_PREFIX}${index + 1}`}
647
- key={index}
648
- style={{
649
- padding: "0px 12px 1px",
650
- borderBottom: `1px solid ${solidBorderColor}`,
651
- background:
652
- index + 1 === historySnapshot.historyIndex
653
- ? "rgb(59 130 246 / 15%)"
654
- : undefined,
655
- }}
656
- >
657
- {"name" in entry.metadata &&
658
- typeof entry.metadata.name === "string" && (
659
- <pre
660
- style={{
661
- fontSize: "11px",
662
- display: "flex",
663
- flexWrap: "wrap",
664
- margin: 0,
665
- }}
666
- >
667
- {entry.metadata.name}
668
- </pre>
669
- )}
670
- {entry.redoPatches?.map((patch, j) => (
671
- <pre
672
- key={j}
673
- style={{
674
- fontSize: "11px",
675
- display: "flex",
676
- flexWrap: "wrap",
677
- margin: 0,
678
- }}
679
- >
680
- {patch.op === "add" && (
681
- <>
682
- <span style={{ fontStyle: "italic" }}>
683
- {pathToString(patch.path)}
684
- </span>
685
- {" = "}
686
- <ObjectInspector data={patch.value} theme={theme} />
687
- </>
688
- )}
689
- {patch.op === "replace" && (
690
- <>
691
- <span style={{ fontStyle: "italic" }}>
692
- {pathToString(patch.path)}
693
- </span>
694
- {" = "}
695
- <ObjectInspector data={patch.value} theme={theme} />
696
- </>
697
- )}
698
- {patch.op === "remove" && (
699
- <>
700
- <span style={{ color: theme.OBJECT_VALUE_STRING_COLOR }}>
701
- delete{" "}
702
- </span>
703
- <span style={{ fontStyle: "italic" }}>
704
- {pathToString(patch.path)}
705
- </span>
706
- </>
707
- )}
708
- {patch.op === "move" && (
709
- <>
710
- <span style={{ fontStyle: "italic" }}>
711
- {pathToString(patch.from!)}
712
- </span>
713
- {" -> "}
714
- <span style={{ fontStyle: "italic" }}>
715
- {pathToString(patch.path)}
716
- </span>
717
- </>
718
- )}
719
- </pre>
720
- ))}
721
- </div>
722
- ))}
723
- </div>
724
- </DisclosureSection>
725
- <DisclosureSection
351
+ historySnapshot={historySnapshot}
352
+ multiplayerStateManager={multiplayerStateManager}
353
+ />
354
+ <StateInspectorDisclosureSection
726
355
  open={showAssets}
727
356
  setOpen={setShowAssets}
728
357
  title={
729
358
  <TitleLabel>
730
- Assets
359
+ Assets ({assets.length})
731
360
  <ColoredDot type={assetsInitialized ? "success" : "error"} />
732
361
  </TitleLabel>
733
362
  }
734
363
  colorScheme={colorScheme}
735
364
  right={
736
365
  <span style={{ display: "flex", gap: "10px" }}>
737
- <SmallButton
366
+ <StateInspectorButton
738
367
  theme={theme}
739
368
  onClick={() => {
740
369
  console.info(
@@ -745,8 +374,8 @@ export const StateInspector = memoGeneric(function StateInspector<
745
374
  }}
746
375
  >
747
376
  Reload
748
- </SmallButton>
749
- <SmallButton
377
+ </StateInspectorButton>
378
+ <StateInspectorButton
750
379
  theme={theme}
751
380
  onClick={() => {
752
381
  // get file input
@@ -767,25 +396,25 @@ export const StateInspector = memoGeneric(function StateInspector<
767
396
  }}
768
397
  >
769
398
  Upload
770
- </SmallButton>
399
+ </StateInspectorButton>
771
400
  </span>
772
401
  }
773
402
  >
774
- <div style={styles.sectionInner}>
403
+ <StateInspectorDisclosureRowInner>
775
404
  {assets.map((asset) => (
776
- <InspectorRow key={asset.id} colorScheme={colorScheme}>
405
+ <StateInspectorRow key={asset.id} colorScheme={colorScheme}>
777
406
  <ObjectInspector name={asset.id} data={asset} theme={theme} />
778
- <SmallButton
407
+ <StateInspectorButton
779
408
  theme={theme}
780
409
  onClick={() => assetManager.delete(asset.id)}
781
410
  >
782
411
  Delete
783
- </SmallButton>
784
- </InspectorRow>
412
+ </StateInspectorButton>
413
+ </StateInspectorRow>
785
414
  ))}
786
- </div>
787
- </DisclosureSection>
788
- <DisclosureSection
415
+ </StateInspectorDisclosureRowInner>
416
+ </StateInspectorDisclosureSection>
417
+ <StateInspectorDisclosureSection
789
418
  title={
790
419
  <TitleLabel>
791
420
  Secrets
@@ -796,7 +425,7 @@ export const StateInspector = memoGeneric(function StateInspector<
796
425
  open={showSecrets}
797
426
  setOpen={setShowSecrets}
798
427
  right={
799
- <SmallButton
428
+ <StateInspectorButton
800
429
  theme={theme}
801
430
  onClick={() => {
802
431
  const name = prompt("Enter secret name");
@@ -807,24 +436,24 @@ export const StateInspector = memoGeneric(function StateInspector<
807
436
  }}
808
437
  >
809
438
  Create
810
- </SmallButton>
439
+ </StateInspectorButton>
811
440
  }
812
441
  >
813
- <div style={styles.sectionInner}>
442
+ <StateInspectorDisclosureRowInner>
814
443
  {secrets.map((secret) => (
815
- <InspectorRow key={secret.id} colorScheme={colorScheme}>
444
+ <StateInspectorRow key={secret.id} colorScheme={colorScheme}>
816
445
  <ObjectInspector data={secret} theme={theme} />
817
- <SmallButton
446
+ <StateInspectorButton
818
447
  theme={theme}
819
448
  onClick={() => secretManager.deleteSecret(secret.id)}
820
449
  >
821
450
  Delete
822
- </SmallButton>
823
- </InspectorRow>
451
+ </StateInspectorButton>
452
+ </StateInspectorRow>
824
453
  ))}
825
- </div>
826
- </DisclosureSection>{" "}
827
- <DisclosureSection
454
+ </StateInspectorDisclosureRowInner>
455
+ </StateInspectorDisclosureSection>{" "}
456
+ <StateInspectorDisclosureSection
828
457
  open={showInputs}
829
458
  setOpen={setShowInputs}
830
459
  title={
@@ -835,11 +464,11 @@ export const StateInspector = memoGeneric(function StateInspector<
835
464
  }
836
465
  colorScheme={colorScheme}
837
466
  >
838
- <div style={styles.sectionInner}>
467
+ <StateInspectorDisclosureRowInner>
839
468
  {inputs?.map((input: Input) => (
840
- <InspectorRow key={input.id} colorScheme={colorScheme}>
469
+ <StateInspectorRow key={input.id} colorScheme={colorScheme}>
841
470
  <ObjectInspector data={input} theme={theme} />
842
- </InspectorRow>
471
+ </StateInspectorRow>
843
472
  ))}
844
473
  {!inputs?.length && (
845
474
  <div
@@ -854,9 +483,9 @@ export const StateInspector = memoGeneric(function StateInspector<
854
483
  <span>No inputs</span>
855
484
  </div>
856
485
  )}
857
- </div>
858
- </DisclosureSection>
859
- <DisclosureSection
486
+ </StateInspectorDisclosureRowInner>
487
+ </StateInspectorDisclosureSection>
488
+ <StateInspectorDisclosureSection
860
489
  open={showOutputTransforms}
861
490
  setOpen={setShowOutputTransforms}
862
491
  title={
@@ -869,11 +498,11 @@ export const StateInspector = memoGeneric(function StateInspector<
869
498
  }
870
499
  colorScheme={colorScheme}
871
500
  >
872
- <div style={styles.sectionInner}>
501
+ <StateInspectorDisclosureRowInner>
873
502
  {outputTransforms?.map((transform: OutputTransform) => (
874
- <InspectorRow key={transform.id} colorScheme={colorScheme}>
503
+ <StateInspectorRow key={transform.id} colorScheme={colorScheme}>
875
504
  <ObjectInspector data={transform} theme={theme} />
876
- </InspectorRow>
505
+ </StateInspectorRow>
877
506
  ))}
878
507
  {!outputTransforms?.length && (
879
508
  <div
@@ -888,17 +517,17 @@ export const StateInspector = memoGeneric(function StateInspector<
888
517
  <span>No output transforms</span>
889
518
  </div>
890
519
  )}
891
- </div>
892
- </DisclosureSection>
893
- <DisclosureSection
520
+ </StateInspectorDisclosureRowInner>
521
+ </StateInspectorDisclosureSection>
522
+ <StateInspectorDisclosureSection
894
523
  title="Tasks"
895
524
  colorScheme={colorScheme}
896
525
  open={showTasks}
897
526
  setOpen={setShowTasks}
898
527
  >
899
- <div style={styles.sectionInner}>
528
+ <StateInspectorDisclosureRowInner>
900
529
  {tasks?.map((task) => (
901
- <InspectorRow
530
+ <StateInspectorRow
902
531
  key={task.id}
903
532
  colorScheme={colorScheme}
904
533
  style={{
@@ -915,265 +544,40 @@ export const StateInspector = memoGeneric(function StateInspector<
915
544
  data={task.payload}
916
545
  theme={theme}
917
546
  />
918
- </InspectorRow>
547
+ </StateInspectorRow>
919
548
  ))}
920
- </div>
921
- </DisclosureSection>
922
- <DisclosureSection
549
+ </StateInspectorDisclosureRowInner>
550
+ </StateInspectorDisclosureSection>
551
+ <StateInspectorDisclosureSection
923
552
  open={showEphemeral}
924
553
  setOpen={setShowEphemeral}
925
554
  title="Ephemeral User Data"
926
555
  colorScheme={colorScheme}
927
556
  >
928
- <div style={styles.sectionInner}>
557
+ <StateInspectorDisclosureRowInner>
929
558
  {Object.entries(ephemeral).map(([key, value]) => (
930
- <InspectorRow key={key} colorScheme={colorScheme}>
559
+ <StateInspectorRow key={key} colorScheme={colorScheme}>
931
560
  <ObjectInspector
932
561
  name={key}
933
562
  data={value}
934
563
  theme={theme}
935
564
  expandLevel={10}
936
565
  />
937
- </InspectorRow>
566
+ </StateInspectorRow>
938
567
  ))}
939
- </div>
940
- </DisclosureSection>
941
- <DisclosureSection
942
- open={showEvents}
943
- setOpen={setShowEvents}
944
- title="Events"
568
+ </StateInspectorDisclosureRowInner>
569
+ </StateInspectorDisclosureSection>
570
+ <EventsSection<S>
571
+ connectionEvents={connectionEvents}
945
572
  colorScheme={colorScheme}
946
- >
947
- <div ref={eventsContainerRef} style={styles.sectionInner}>
948
- {connectionEvents?.map((event, index) =>
949
- event.type === "stateChange" ? (
950
- <InspectorRow key={index} colorScheme={colorScheme}>
951
- connection:{" "}
952
- <span style={{ fontStyle: "italic" }}>
953
- {event.state.toLowerCase()}
954
- </span>
955
- </InspectorRow>
956
- ) : (
957
- <InspectorRow
958
- key={index}
959
- colorScheme={colorScheme}
960
- variant={event.type === "send" ? "up" : "down"}
961
- style={{
962
- padding: "0px 12px 1px",
963
- }}
964
- >
965
- <ObjectInspector
966
- data={event.type === "error" ? event.error : event.message}
967
- theme={theme}
968
- nodeRenderer={({
969
- depth,
970
- name,
971
- data,
972
- isNonenumerable,
973
- }: any) => {
974
- const direction = event.type === "send" ? "up" : "down";
975
-
976
- return depth === 0 ? (
977
- <ObjectRootLabel direction={direction} data={data} />
978
- ) : (
979
- <ObjectLabel
980
- direction={direction}
981
- name={name}
982
- data={data}
983
- isNonenumerable={isNonenumerable}
984
- />
985
- );
986
- }}
987
- />
988
- </InspectorRow>
989
- )
990
- )}
991
- {!connectionEvents && (
992
- <div
993
- style={{
994
- padding: "12px",
995
- fontSize: "12px",
996
- display: "flex",
997
- flexDirection: "column",
998
- gap: "4px",
999
- }}
1000
- >
1001
- <span>No recorded events</span>
1002
- </div>
1003
- )}
1004
- </div>
1005
- </DisclosureSection>
573
+ eventsContainerRef={eventsContainerRef}
574
+ showEvents={showEvents}
575
+ setShowEvents={setShowEvents}
576
+ />
1006
577
  </div>
1007
578
  );
1008
579
  });
1009
580
 
1010
- type MessageDirection = "up" | "down";
1011
-
1012
- const ObjectRootLabel: FC<{
1013
- name?: string;
1014
- data: any;
1015
- direction?: MessageDirection;
1016
- }> = ({ name, data, direction }) => {
1017
- if (typeof name === "string") {
1018
- return (
1019
- <span>
1020
- <ObjectName name={name} />
1021
- <span>: </span>
1022
- <ObjectPreview data={data} />
1023
- </span>
1024
- );
1025
- }
1026
- if (direction === "up" || direction === "down") {
1027
- const arrow = direction === "up" ? "↑" : "↓";
1028
-
1029
- return (
1030
- <span>
1031
- <span
1032
- style={{
1033
- background:
1034
- direction === "up" ? "rgba(0,255,0,0.2)" : "rgba(255,0,0,0.2)",
1035
- // color: "white",
1036
- width: 12,
1037
- height: 12,
1038
- borderRadius: 2,
1039
- display: "inline-flex",
1040
- justifyContent: "center",
1041
- alignItems: "center",
1042
- marginRight: 4,
1043
- lineHeight: "12px",
1044
- }}
1045
- >
1046
- {arrow}
1047
- </span>
1048
- <ObjectPreview data={data} />
1049
- </span>
1050
- );
1051
- } else {
1052
- return <ObjectPreview data={data} />;
1053
- }
1054
- };
1055
-
1056
- // Rotates ▶️ or ▼ based on expanded state
1057
- function Arrow({
1058
- expanded,
1059
- onClick,
1060
- style,
1061
- }: {
1062
- expanded: boolean;
1063
- onClick?: () => void;
1064
- style?: CSSProperties;
1065
- }) {
1066
- return (
1067
- <span
1068
- role="button"
1069
- onClick={onClick}
1070
- style={{
1071
- display: "inline-block",
1072
- textAlign: "center",
1073
- transform: expanded ? "rotate(0deg)" : "rotate(-90deg)",
1074
- fontFamily: "Menlo, monospace",
1075
- ...style,
1076
- }}
1077
- >
1078
- {"▼"}
1079
- </span>
1080
- );
1081
- }
1082
-
1083
- function pathToString(extendedPath: ExtendedPathKey[]) {
1084
- return extendedPath
1085
- .map((key) =>
1086
- typeof key === "object"
1087
- ? `:${ellipsis(key.id.toString(), 8, "middle")}`
1088
- : key
1089
- )
1090
- .map((key, index) =>
1091
- index === 0 || (typeof key === "string" && key.startsWith(":"))
1092
- ? key
1093
- : `.${key}`
1094
- )
1095
- .join("");
1096
- }
1097
-
1098
- type EllipsisPosition = "start" | "middle" | "end";
1099
-
1100
- function ellipsis(
1101
- str: string,
1102
- maxLength: number,
1103
- position: EllipsisPosition = "end"
1104
- ) {
1105
- if (str.length <= maxLength) return str;
1106
-
1107
- switch (position) {
1108
- case "start":
1109
- return `…${str.slice(str.length - maxLength)}`;
1110
- case "middle":
1111
- const halfLength = Math.floor(maxLength / 2);
1112
- return `${str.slice(0, halfLength)}…${str.slice(str.length - halfLength)}`;
1113
- case "end":
1114
- return `${str.slice(0, maxLength)}…`;
1115
- }
1116
- }
1117
-
1118
- function SmallButton({
1119
- children,
1120
- onClick,
1121
- style,
1122
- theme,
1123
- disabled,
1124
- }: {
1125
- children: React.ReactNode;
1126
- onClick: () => void;
1127
- style?: CSSProperties;
1128
- theme: typeof chromeLight;
1129
- disabled?: boolean;
1130
- }) {
1131
- return (
1132
- <button
1133
- type="button"
1134
- disabled={disabled}
1135
- onClick={(event) => {
1136
- event.stopPropagation();
1137
- onClick();
1138
- }}
1139
- style={{
1140
- flex: "0",
1141
- appearance: "none",
1142
- background: "none",
1143
- color: theme.BASE_COLOR,
1144
- opacity: disabled ? 0.25 : 1,
1145
- border: "none",
1146
- fontSize: "12px",
1147
- whiteSpace: "nowrap",
1148
- display: "inline-flex",
1149
- alignItems: "center",
1150
- justifyContent: "center",
1151
- padding: "0",
1152
- cursor: "pointer",
1153
- ...style,
1154
- }}
1155
- >
1156
- {children}
1157
- </button>
1158
- );
1159
- }
1160
-
1161
- function ColoredDot({ type }: { type: "success" | "error" }) {
1162
- return (
1163
- <span
1164
- style={{
1165
- display: "inline-block",
1166
- width: 6,
1167
- height: 6,
1168
- background:
1169
- type === "success" ? "rgba(0,200,0,0.8)" : "rgba(200,0,0,0.8)",
1170
- borderRadius: "50%",
1171
- verticalAlign: "middle",
1172
- }}
1173
- />
1174
- );
1175
- }
1176
-
1177
581
  function TitleLabel({ children }: { children: React.ReactNode }) {
1178
582
  return (
1179
583
  <span style={{ display: "flex", alignItems: "center", gap: "4px" }}>