jazz-tools 0.20.1 → 0.20.3

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 (115) hide show
  1. package/.turbo/turbo-build.log +49 -49
  2. package/CHANGELOG.md +19 -6
  3. package/dist/{chunk-2OPP7KWV.js → chunk-Q5RNSSUM.js} +121 -22
  4. package/dist/chunk-Q5RNSSUM.js.map +1 -0
  5. package/dist/index.js +1 -1
  6. package/dist/inspector/{chunk-MCTB5ZJC.js → chunk-6JPVMI3V.js} +302 -182
  7. package/dist/inspector/chunk-6JPVMI3V.js.map +1 -0
  8. package/dist/inspector/{custom-element-5YWVZBWA.js → custom-element-WOQY2M4W.js} +1337 -206
  9. package/dist/inspector/custom-element-WOQY2M4W.js.map +1 -0
  10. package/dist/inspector/in-app.d.ts +1 -0
  11. package/dist/inspector/in-app.d.ts.map +1 -1
  12. package/dist/inspector/index.d.ts +1 -0
  13. package/dist/inspector/index.d.ts.map +1 -1
  14. package/dist/inspector/index.js +1044 -17
  15. package/dist/inspector/index.js.map +1 -1
  16. package/dist/inspector/pages/home.d.ts +4 -1
  17. package/dist/inspector/pages/home.d.ts.map +1 -1
  18. package/dist/inspector/pages/performance/PerformancePage.d.ts +7 -0
  19. package/dist/inspector/pages/performance/PerformancePage.d.ts.map +1 -0
  20. package/dist/inspector/pages/performance/SubscriptionDetailPanel.d.ts +8 -0
  21. package/dist/inspector/pages/performance/SubscriptionDetailPanel.d.ts.map +1 -0
  22. package/dist/inspector/pages/performance/SubscriptionRow.d.ts +11 -0
  23. package/dist/inspector/pages/performance/SubscriptionRow.d.ts.map +1 -0
  24. package/dist/inspector/pages/performance/Timeline.d.ts +12 -0
  25. package/dist/inspector/pages/performance/Timeline.d.ts.map +1 -0
  26. package/dist/inspector/pages/performance/helpers.d.ts +5 -0
  27. package/dist/inspector/pages/performance/helpers.d.ts.map +1 -0
  28. package/dist/inspector/pages/performance/index.d.ts +3 -0
  29. package/dist/inspector/pages/performance/index.d.ts.map +1 -0
  30. package/dist/inspector/pages/performance/types.d.ts +13 -0
  31. package/dist/inspector/pages/performance/types.d.ts.map +1 -0
  32. package/dist/inspector/pages/performance/usePerformanceEntries.d.ts +3 -0
  33. package/dist/inspector/pages/performance/usePerformanceEntries.d.ts.map +1 -0
  34. package/dist/inspector/register-custom-element.js +3 -1
  35. package/dist/inspector/register-custom-element.js.map +1 -1
  36. package/dist/inspector/standalone.js +1 -1
  37. package/dist/inspector/tests/pages/performance/PerformancePage.test.d.ts +2 -0
  38. package/dist/inspector/tests/pages/performance/PerformancePage.test.d.ts.map +1 -0
  39. package/dist/inspector/tests/pages/performance/SubscriptionDetailPanel.test.d.ts +2 -0
  40. package/dist/inspector/tests/pages/performance/SubscriptionDetailPanel.test.d.ts.map +1 -0
  41. package/dist/inspector/tests/pages/performance/SubscriptionRow.test.d.ts +2 -0
  42. package/dist/inspector/tests/pages/performance/SubscriptionRow.test.d.ts.map +1 -0
  43. package/dist/inspector/tests/pages/performance/Timeline.test.d.ts +2 -0
  44. package/dist/inspector/tests/pages/performance/Timeline.test.d.ts.map +1 -0
  45. package/dist/inspector/tests/pages/performance/helpers.test.d.ts +2 -0
  46. package/dist/inspector/tests/pages/performance/helpers.test.d.ts.map +1 -0
  47. package/dist/inspector/viewer/delete-local-data.d.ts.map +1 -1
  48. package/dist/inspector/viewer/header.d.ts +4 -2
  49. package/dist/inspector/viewer/header.d.ts.map +1 -1
  50. package/dist/inspector/viewer/page-stack.d.ts +3 -1
  51. package/dist/inspector/viewer/page-stack.d.ts.map +1 -1
  52. package/dist/react-core/hooks.d.ts +2 -2
  53. package/dist/react-core/hooks.d.ts.map +1 -1
  54. package/dist/react-core/index.js +50 -18
  55. package/dist/react-core/index.js.map +1 -1
  56. package/dist/react-core/subscription-provider.d.ts.map +1 -1
  57. package/dist/react-native-core/media/image.d.ts +1 -1
  58. package/dist/svelte/jazz.class.svelte.d.ts.map +1 -1
  59. package/dist/svelte/jazz.class.svelte.js +27 -22
  60. package/dist/testing.js +1 -1
  61. package/dist/tools/coValues/interfaces.d.ts.map +1 -1
  62. package/dist/tools/exports.d.ts +1 -1
  63. package/dist/tools/exports.d.ts.map +1 -1
  64. package/dist/tools/implementation/zodSchema/runtimeConverters/coValueSchemaTransformation.d.ts.map +1 -1
  65. package/dist/tools/subscribe/SubscriptionCache.d.ts +2 -2
  66. package/dist/tools/subscribe/SubscriptionCache.d.ts.map +1 -1
  67. package/dist/tools/subscribe/SubscriptionScope.d.ts +19 -12
  68. package/dist/tools/subscribe/SubscriptionScope.d.ts.map +1 -1
  69. package/dist/tools/subscribe/errorReporting.d.ts +6 -0
  70. package/dist/tools/subscribe/errorReporting.d.ts.map +1 -1
  71. package/dist/tools/subscribe/index.d.ts +4 -4
  72. package/dist/tools/subscribe/index.d.ts.map +1 -1
  73. package/dist/tools/subscribe/types.d.ts +48 -3
  74. package/dist/tools/subscribe/types.d.ts.map +1 -1
  75. package/dist/tools/subscribe/utils.d.ts +1 -1
  76. package/dist/tools/subscribe/utils.d.ts.map +1 -1
  77. package/dist/tools/tests/SubscriptionScope.performance.test.d.ts +2 -0
  78. package/dist/tools/tests/SubscriptionScope.performance.test.d.ts.map +1 -0
  79. package/package.json +4 -4
  80. package/src/inspector/in-app.tsx +41 -3
  81. package/src/inspector/index.tsx +5 -1
  82. package/src/inspector/pages/home.tsx +26 -3
  83. package/src/inspector/pages/performance/PerformancePage.tsx +215 -0
  84. package/src/inspector/pages/performance/SubscriptionDetailPanel.tsx +182 -0
  85. package/src/inspector/pages/performance/SubscriptionRow.tsx +242 -0
  86. package/src/inspector/pages/performance/Timeline.tsx +513 -0
  87. package/src/inspector/pages/performance/helpers.ts +70 -0
  88. package/src/inspector/pages/performance/index.ts +2 -0
  89. package/src/inspector/pages/performance/types.ts +12 -0
  90. package/src/inspector/pages/performance/usePerformanceEntries.ts +53 -0
  91. package/src/inspector/register-custom-element.ts +3 -0
  92. package/src/inspector/tests/pages/performance/PerformancePage.test.tsx +83 -0
  93. package/src/inspector/tests/pages/performance/SubscriptionDetailPanel.test.tsx +68 -0
  94. package/src/inspector/tests/pages/performance/SubscriptionRow.test.tsx +93 -0
  95. package/src/inspector/tests/pages/performance/Timeline.test.tsx +57 -0
  96. package/src/inspector/tests/pages/performance/helpers.test.ts +91 -0
  97. package/src/inspector/viewer/delete-local-data.tsx +24 -5
  98. package/src/inspector/viewer/header.tsx +96 -17
  99. package/src/inspector/viewer/page-stack.tsx +22 -18
  100. package/src/react-core/hooks.ts +34 -4
  101. package/src/react-core/subscription-provider.tsx +17 -8
  102. package/src/svelte/jazz.class.svelte.ts +51 -33
  103. package/src/tools/coValues/interfaces.ts +3 -0
  104. package/src/tools/exports.ts +1 -0
  105. package/src/tools/implementation/zodSchema/runtimeConverters/coValueSchemaTransformation.ts +13 -0
  106. package/src/tools/subscribe/SubscriptionCache.ts +6 -4
  107. package/src/tools/subscribe/SubscriptionScope.ts +141 -23
  108. package/src/tools/subscribe/errorReporting.ts +1 -1
  109. package/src/tools/subscribe/index.ts +1 -1
  110. package/src/tools/subscribe/types.ts +62 -9
  111. package/src/tools/subscribe/utils.ts +2 -2
  112. package/src/tools/tests/SubscriptionScope.performance.test.ts +149 -0
  113. package/dist/chunk-2OPP7KWV.js.map +0 -1
  114. package/dist/inspector/chunk-MCTB5ZJC.js.map +0 -1
  115. package/dist/inspector/custom-element-5YWVZBWA.js.map +0 -1
@@ -0,0 +1,83 @@
1
+ // @vitest-environment happy-dom
2
+ import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
3
+ import { cleanup, render, screen } from "@testing-library/react";
4
+ import { setup } from "goober";
5
+ import React from "react";
6
+ import { PerformancePage } from "../../../pages/performance/index.js";
7
+ import { RouterContext } from "../../../router/context.js";
8
+ import type { Router } from "../../../router/context.js";
9
+ import { SubscriptionScope } from "jazz-tools";
10
+
11
+ beforeAll(() => {
12
+ SubscriptionScope.enableProfiling();
13
+ setup(React.createElement);
14
+ });
15
+
16
+ afterEach(() => {
17
+ cleanup();
18
+ vi.restoreAllMocks();
19
+ });
20
+
21
+ const mockRouter: Router = {
22
+ path: [],
23
+ setPage: vi.fn(),
24
+ addPages: vi.fn(),
25
+ goToIndex: vi.fn(),
26
+ goBack: vi.fn(),
27
+ };
28
+
29
+ function RouterProvider({ children }: { children: React.ReactNode }) {
30
+ return (
31
+ <RouterContext.Provider value={mockRouter}>
32
+ {children}
33
+ </RouterContext.Provider>
34
+ );
35
+ }
36
+
37
+ describe("PerformancePage", () => {
38
+ it("shows empty state when no entries", () => {
39
+ vi.spyOn(performance, "getEntriesByType").mockReturnValue([]);
40
+
41
+ render(
42
+ <RouterProvider>
43
+ <PerformancePage onNavigate={() => {}} />
44
+ </RouterProvider>,
45
+ );
46
+
47
+ expect(screen.getByText(/No subscriptions recorded yet/)).toBeDefined();
48
+ });
49
+
50
+ it("renders a row when performance entries are provided", () => {
51
+ const mockMeasure = {
52
+ entryType: "measure",
53
+ name: "jazz.subscription:test-uuid",
54
+ startTime: 100,
55
+ duration: 50,
56
+ detail: {
57
+ type: "jazz-subscription",
58
+ uuid: "test-uuid",
59
+ id: "co_z123abc",
60
+ source: "useCoState",
61
+ resolve: { depth: 1 },
62
+ status: "loaded",
63
+ startTime: 100,
64
+ endTime: 150,
65
+ duration: 50,
66
+ },
67
+ } as unknown as PerformanceEntry;
68
+
69
+ vi.spyOn(performance, "getEntriesByType").mockImplementation((type) => {
70
+ if (type === "measure") return [mockMeasure];
71
+ return [];
72
+ });
73
+
74
+ render(
75
+ <RouterProvider>
76
+ <PerformancePage onNavigate={() => {}} />
77
+ </RouterProvider>,
78
+ );
79
+
80
+ expect(screen.getByText("co_z123abc")).toBeDefined();
81
+ expect(screen.getByText("useCoState")).toBeDefined();
82
+ });
83
+ });
@@ -0,0 +1,68 @@
1
+ // @vitest-environment happy-dom
2
+ import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
3
+ import { cleanup, render, screen, fireEvent } from "@testing-library/react";
4
+ import { setup } from "goober";
5
+ import React from "react";
6
+ import { SubscriptionDetailPanel } from "../../../pages/performance/SubscriptionDetailPanel.js";
7
+
8
+ beforeAll(() => {
9
+ setup(React.createElement);
10
+ });
11
+
12
+ afterEach(() => {
13
+ cleanup();
14
+ });
15
+
16
+ describe("SubscriptionDetailPanel", () => {
17
+ const mockEntry = {
18
+ uuid: "test-uuid",
19
+ id: "co_z123",
20
+ source: "useCoState",
21
+ resolve: "{}",
22
+ status: "loaded" as const,
23
+ startTime: 0,
24
+ endTime: 100,
25
+ duration: 100,
26
+ };
27
+
28
+ it("calls onClose when close button is clicked", () => {
29
+ const onClose = vi.fn();
30
+ render(
31
+ <SubscriptionDetailPanel
32
+ entry={mockEntry}
33
+ onNavigate={() => {}}
34
+ onClose={onClose}
35
+ />,
36
+ );
37
+
38
+ fireEvent.click(screen.getByLabelText("Close detail panel"));
39
+ expect(onClose).toHaveBeenCalled();
40
+ });
41
+
42
+ it("calls onNavigate when CoValue link is clicked", () => {
43
+ const onNavigate = vi.fn();
44
+ render(
45
+ <SubscriptionDetailPanel
46
+ entry={mockEntry}
47
+ onNavigate={onNavigate}
48
+ onClose={() => {}}
49
+ />,
50
+ );
51
+
52
+ fireEvent.click(screen.getByText("co_z123"));
53
+ expect(onNavigate).toHaveBeenCalledWith("co_z123");
54
+ });
55
+
56
+ it("displays entry details", () => {
57
+ render(
58
+ <SubscriptionDetailPanel
59
+ entry={mockEntry}
60
+ onNavigate={() => {}}
61
+ onClose={() => {}}
62
+ />,
63
+ );
64
+
65
+ expect(screen.getByText("useCoState")).toBeDefined();
66
+ expect(screen.getByText("co_z123")).toBeDefined();
67
+ });
68
+ });
@@ -0,0 +1,93 @@
1
+ // @vitest-environment happy-dom
2
+ import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
3
+ import { cleanup, render, screen, fireEvent } from "@testing-library/react";
4
+ import { setup } from "goober";
5
+ import React from "react";
6
+ import { SubscriptionRow } from "../../../pages/performance/SubscriptionRow.js";
7
+
8
+ beforeAll(() => {
9
+ setup(React.createElement);
10
+ });
11
+
12
+ afterEach(() => {
13
+ cleanup();
14
+ });
15
+
16
+ describe("SubscriptionRow", () => {
17
+ const mockEntry = {
18
+ uuid: "test-uuid",
19
+ id: "co_z123",
20
+ source: "useCoState",
21
+ resolve: "{}",
22
+ status: "loaded" as const,
23
+ startTime: 0,
24
+ endTime: 100,
25
+ duration: 100,
26
+ };
27
+
28
+ it("triggers selection on Enter key", () => {
29
+ const onSelect = vi.fn();
30
+ render(
31
+ <SubscriptionRow
32
+ entry={mockEntry}
33
+ isSelected={false}
34
+ onSelect={onSelect}
35
+ barLeft="0%"
36
+ barWidth="10%"
37
+ barColor="green"
38
+ />,
39
+ );
40
+
41
+ fireEvent.keyDown(screen.getByRole("button"), { key: "Enter" });
42
+ expect(onSelect).toHaveBeenCalled();
43
+ });
44
+
45
+ it("triggers selection on Space key", () => {
46
+ const onSelect = vi.fn();
47
+ render(
48
+ <SubscriptionRow
49
+ entry={mockEntry}
50
+ isSelected={false}
51
+ onSelect={onSelect}
52
+ barLeft="0%"
53
+ barWidth="10%"
54
+ barColor="green"
55
+ />,
56
+ );
57
+
58
+ fireEvent.keyDown(screen.getByRole("button"), { key: " " });
59
+ expect(onSelect).toHaveBeenCalled();
60
+ });
61
+
62
+ it("is focusable with tabIndex", () => {
63
+ render(
64
+ <SubscriptionRow
65
+ entry={mockEntry}
66
+ isSelected={false}
67
+ onSelect={() => {}}
68
+ barLeft="0%"
69
+ barWidth="10%"
70
+ barColor="green"
71
+ />,
72
+ );
73
+
74
+ expect(screen.getByRole("button").getAttribute("tabindex")).toBe("0");
75
+ });
76
+
77
+ it("triggers selection on click", () => {
78
+ const onSelect = vi.fn();
79
+ render(
80
+ <SubscriptionRow
81
+ entry={mockEntry}
82
+ isSelected={false}
83
+ onSelect={onSelect}
84
+ barLeft="0%"
85
+ barWidth="10%"
86
+ barColor="green"
87
+ />,
88
+ );
89
+
90
+ fireEvent.click(screen.getByRole("button"));
91
+ expect(onSelect).toHaveBeenCalled();
92
+ });
93
+ });
@@ -0,0 +1,57 @@
1
+ // @vitest-environment happy-dom
2
+ import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
3
+ import { cleanup, render, screen, fireEvent } from "@testing-library/react";
4
+ import { setup } from "goober";
5
+ import React from "react";
6
+ import { Timeline } from "../../../pages/performance/Timeline.js";
7
+
8
+ beforeAll(() => {
9
+ setup(React.createElement);
10
+ });
11
+
12
+ afterEach(() => {
13
+ cleanup();
14
+ });
15
+
16
+ describe("Timeline", () => {
17
+ it("shows clear selection button when selection exists", () => {
18
+ render(
19
+ <Timeline
20
+ entries={[]}
21
+ timeRange={{ min: 0, max: 1000 }}
22
+ selection={[100, 500]}
23
+ onSelectionChange={() => {}}
24
+ />,
25
+ );
26
+
27
+ expect(screen.getByText("Clear selection")).toBeDefined();
28
+ });
29
+
30
+ it("calls onSelectionChange(null) when clear button clicked", () => {
31
+ const onSelectionChange = vi.fn();
32
+ render(
33
+ <Timeline
34
+ entries={[]}
35
+ timeRange={{ min: 0, max: 1000 }}
36
+ selection={[100, 500]}
37
+ onSelectionChange={onSelectionChange}
38
+ />,
39
+ );
40
+
41
+ fireEvent.click(screen.getByText("Clear selection"));
42
+ expect(onSelectionChange).toHaveBeenCalledWith(null);
43
+ });
44
+
45
+ it("does not show clear selection button when no selection", () => {
46
+ render(
47
+ <Timeline
48
+ entries={[]}
49
+ timeRange={{ min: 0, max: 1000 }}
50
+ selection={null}
51
+ onSelectionChange={() => {}}
52
+ />,
53
+ );
54
+
55
+ expect(screen.queryByText("Clear selection")).toBeNull();
56
+ });
57
+ });
@@ -0,0 +1,91 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ formatDuration,
4
+ getCallerLocation,
5
+ getCallerStack,
6
+ } from "../../../pages/performance/helpers.js";
7
+
8
+ describe("formatDuration", () => {
9
+ it("returns microseconds for duration < 1ms", () => {
10
+ expect(formatDuration(0.5)).toBe("500μs");
11
+ expect(formatDuration(0.001)).toBe("1μs");
12
+ expect(formatDuration(0.999)).toBe("999μs");
13
+ });
14
+
15
+ it("returns milliseconds for duration < 1000ms", () => {
16
+ expect(formatDuration(1)).toBe("1.00ms");
17
+ expect(formatDuration(123.456)).toBe("123.46ms");
18
+ expect(formatDuration(999.99)).toBe("999.99ms");
19
+ });
20
+
21
+ it("returns seconds for duration >= 1000ms", () => {
22
+ expect(formatDuration(1000)).toBe("1.00s");
23
+ expect(formatDuration(1500)).toBe("1.50s");
24
+ expect(formatDuration(60000)).toBe("60.00s");
25
+ });
26
+ });
27
+
28
+ describe("getCallerLocation", () => {
29
+ it("returns undefined for undefined stack", () => {
30
+ expect(getCallerLocation(undefined)).toBeUndefined();
31
+ });
32
+
33
+ it("returns undefined for empty stack", () => {
34
+ expect(getCallerLocation("")).toBeUndefined();
35
+ });
36
+
37
+ it("extracts user frame location filtering out internals", () => {
38
+ const stack = `Error
39
+ at trackLoadingPerformance (jazz-tools/src/subscribe.js:100:10)
40
+ at useCoState (jazz-tools/src/hooks.js:50:5)
41
+ at MyComponent (src/components/MyComponent.tsx:25:10)
42
+ at renderWithHooks (react-dom.js:100:5)`;
43
+
44
+ const result = getCallerLocation(stack);
45
+ expect(result).toContain("MyComponent.tsx:25:10");
46
+ });
47
+
48
+ it("filters out node_modules frames", () => {
49
+ const stack = `Error
50
+ at someFunction (node_modules/some-lib/index.js:10:5)
51
+ at MyComponent (src/App.tsx:15:3)`;
52
+
53
+ const result = getCallerLocation(stack);
54
+ expect(result).toContain("App.tsx:15:3");
55
+ });
56
+ });
57
+
58
+ describe("getCallerStack", () => {
59
+ it("returns undefined for undefined stack", () => {
60
+ expect(getCallerStack(undefined)).toBeUndefined();
61
+ });
62
+
63
+ it("filters out Error: and React internals", () => {
64
+ // Stack trace format: first two lines are skipped by slice(2, 15)
65
+ const stack = `Error: test
66
+ at internalFunction (jazz-tools/src/internal.js:10:5)
67
+ at Component (src/App.tsx:10:5)
68
+ at renderWithHooks (react-dom.js:100:5)
69
+ at react-stack-bottom-frame (react.js:50:3)
70
+ at Parent (src/Parent.tsx:20:10)`;
71
+
72
+ const result = getCallerStack(stack);
73
+ expect(result).not.toContain("Error:");
74
+ expect(result).not.toContain("renderWithHooks");
75
+ expect(result).not.toContain("react-stack-bottom-frame");
76
+ expect(result).toContain("App.tsx");
77
+ expect(result).toContain("Parent.tsx");
78
+ });
79
+
80
+ it("reverses the stack order", () => {
81
+ // Stack trace format: first two lines are skipped, so we need padding lines
82
+ const stack = `Error
83
+ at internalFunction (jazz-tools/src/internal.js:10:5)
84
+ at First (src/First.tsx:10:5)
85
+ at Second (src/Second.tsx:20:5)`;
86
+
87
+ const result = getCallerStack(stack);
88
+ // Second should come before First after reversing
89
+ expect(result!.indexOf("Second")).toBeLessThan(result!.indexOf("First"));
90
+ });
91
+ });
@@ -2,17 +2,37 @@ import { Button } from "../ui/button.js";
2
2
  import { Modal } from "../ui/modal.js";
3
3
  import { Input } from "../ui/input.js";
4
4
  import { useState } from "react";
5
+ import { useJazzContext } from "jazz-tools/react-core";
5
6
 
6
7
  const DELETE_LOCAL_DATA_STRING = "delete my local data";
7
8
 
8
9
  export function DeleteLocalData() {
9
10
  const [showDeleteModal, setShowDeleteModal] = useState(false);
10
11
  const [confirmDeleteString, setConfirmDeleteString] = useState("");
12
+ const jazzContext = useJazzContext();
11
13
 
12
14
  return (
13
15
  <>
14
- <Button variant="destructive" onClick={() => setShowDeleteModal(true)}>
15
- Delete my local data
16
+ <Button
17
+ variant="destructive"
18
+ onClick={() => setShowDeleteModal(true)}
19
+ title="Delete my local data"
20
+ >
21
+ <svg
22
+ width="16"
23
+ height="16"
24
+ viewBox="0 0 16 16"
25
+ fill="none"
26
+ xmlns="http://www.w3.org/2000/svg"
27
+ >
28
+ <path
29
+ d="M2 4h12M5.333 4V2.667a1.333 1.333 0 011.334-1.334h2.666a1.333 1.333 0 011.334 1.334V4m2 0v9.333a1.333 1.333 0 01-1.334 1.334H4.667a1.333 1.333 0 01-1.334-1.334V4h9.334z"
30
+ stroke="currentColor"
31
+ strokeWidth="1.5"
32
+ strokeLinecap="round"
33
+ strokeLinejoin="round"
34
+ />
35
+ </svg>
16
36
  </Button>
17
37
  <Modal
18
38
  isOpen={showDeleteModal}
@@ -83,10 +103,9 @@ export function DeleteLocalData() {
83
103
  variant="destructive"
84
104
  disabled={confirmDeleteString !== DELETE_LOCAL_DATA_STRING}
85
105
  onClick={() => {
86
- const jazzKeys = Object.keys(localStorage).filter(
87
- (key) => key.startsWith("jazz-") || key.startsWith("co_z"),
106
+ localStorage.removeItem(
107
+ jazzContext.getAuthSecretStorage().getStorageKey(),
88
108
  );
89
- jazzKeys.forEach((key) => localStorage.removeItem(key));
90
109
  indexedDB.deleteDatabase("jazz-storage");
91
110
  window.location.reload();
92
111
  setShowDeleteModal(false);
@@ -4,18 +4,20 @@ import React, { type PropsWithChildren, useState } from "react";
4
4
  import { Button } from "../ui/button.js";
5
5
  import { Input } from "../ui/input.js";
6
6
  import { Breadcrumbs } from "./breadcrumbs.js";
7
- import { DeleteLocalData } from "./delete-local-data.js";
8
7
  import { useRouter } from "../router/context.js";
8
+ import type { InspectorTab } from "../in-app.js";
9
9
 
10
10
  export function Header({
11
- showDeleteLocalData = false,
12
11
  showClose = false,
13
12
  onClose,
13
+ activeTab,
14
+ onTabChange,
14
15
  children,
15
16
  }: PropsWithChildren<{
16
- showDeleteLocalData?: boolean;
17
17
  showClose?: boolean;
18
18
  onClose?: () => void;
19
+ activeTab?: InspectorTab;
20
+ onTabChange?: (tab: InspectorTab) => void;
19
21
  }>) {
20
22
  const [coValueId, setCoValueId] = useState<CoID<RawCoValue> | "">("");
21
23
  const { path, setPage } = useRouter();
@@ -30,24 +32,59 @@ export function Header({
30
32
 
31
33
  return (
32
34
  <HeaderContainer>
33
- <Breadcrumbs />
34
- {path.length !== 0 && (
35
- <Form onSubmit={handleCoValueIdSubmit}>
36
- <Input
37
- label="CoValue ID"
38
- style={{ fontFamily: "monospace" }}
39
- hideLabel
40
- placeholder="co_z1234567890abcdef123456789"
41
- value={coValueId}
42
- onChange={(e) => setCoValueId(e.target.value as CoID<RawCoValue>)}
43
- />
44
- </Form>
35
+ {activeTab && onTabChange && (
36
+ <TabBar>
37
+ <Tab
38
+ active={activeTab === "inspector"}
39
+ onClick={() => onTabChange("inspector")}
40
+ >
41
+ Inspector
42
+ </Tab>
43
+ <Tab
44
+ active={activeTab === "performance"}
45
+ onClick={() => onTabChange("performance")}
46
+ >
47
+ Performance
48
+ </Tab>
49
+ </TabBar>
50
+ )}
51
+ {(activeTab === "inspector" || !activeTab) && (
52
+ <>
53
+ <Breadcrumbs />
54
+ {path.length !== 0 && (
55
+ <Form onSubmit={handleCoValueIdSubmit}>
56
+ <Input
57
+ label="CoValue ID"
58
+ style={{ fontFamily: "monospace" }}
59
+ hideLabel
60
+ placeholder="co_z1234567890abcdef123456789"
61
+ value={coValueId}
62
+ onChange={(e) =>
63
+ setCoValueId(e.target.value as CoID<RawCoValue>)
64
+ }
65
+ />
66
+ </Form>
67
+ )}
68
+ </>
45
69
  )}
46
70
  {children}
47
- {showDeleteLocalData && <DeleteLocalData />}
71
+ <Spacer />
48
72
  {showClose && (
49
73
  <Button variant="plain" type="button" onClick={onClose}>
50
- Close
74
+ <svg
75
+ width="14"
76
+ height="14"
77
+ viewBox="0 0 14 14"
78
+ fill="none"
79
+ xmlns="http://www.w3.org/2000/svg"
80
+ >
81
+ <path
82
+ d="M1 1L13 13M1 13L13 1"
83
+ stroke="currentColor"
84
+ strokeWidth="2"
85
+ strokeLinecap="round"
86
+ />
87
+ </svg>
51
88
  </Button>
52
89
  )}
53
90
  </HeaderContainer>
@@ -65,3 +102,45 @@ const HeaderContainer = styled("div")`
65
102
  const Form = styled("form")`
66
103
  width: 24rem;
67
104
  `;
105
+
106
+ const TabBar = styled("div")`
107
+ display: flex;
108
+ gap: 0.25rem;
109
+ background-color: var(--j-foreground);
110
+ border-radius: var(--j-radius-lg);
111
+ padding: 0.25rem;
112
+ `;
113
+
114
+ const Tab = styled("button")<{ active?: boolean }>`
115
+ padding: 0.375rem 0.75rem;
116
+ border: none;
117
+ border-radius: var(--j-radius-md);
118
+ font-size: 0.875rem;
119
+ font-weight: 500;
120
+ cursor: pointer;
121
+ transition: all 0.15s ease;
122
+
123
+ ${(props) =>
124
+ props.active
125
+ ? `
126
+ background-color: white;
127
+ color: var(--j-text-color-strong);
128
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
129
+
130
+ @media (prefers-color-scheme: dark) {
131
+ background-color: var(--j-neutral-800);
132
+ }
133
+ `
134
+ : `
135
+ background-color: transparent;
136
+ color: var(--j-neutral-500);
137
+
138
+ &:hover {
139
+ color: var(--j-text-color-strong);
140
+ }
141
+ `}
142
+ `;
143
+
144
+ const Spacer = styled("div")`
145
+ flex: 1;
146
+ `;
@@ -1,5 +1,6 @@
1
1
  import { CoID, LocalNode, RawCoValue } from "cojson";
2
2
  import { styled } from "goober";
3
+ import type { CSSProperties } from "react";
3
4
  import { Page } from "./page.js";
4
5
  import { ErrorBoundary } from "../ui/error-boundary.js";
5
6
  import { useRouter } from "../router/context.js";
@@ -17,9 +18,10 @@ const PageStackContainer = styled("article")`
17
18
 
18
19
  type PageStackProps = {
19
20
  homePage?: React.ReactNode;
21
+ style?: CSSProperties;
20
22
  };
21
23
 
22
- export function PageStack({ homePage }: PageStackProps) {
24
+ export function PageStack({ homePage, style }: PageStackProps) {
23
25
  const { path, addPages, goBack } = useRouter();
24
26
  const { localNode } = useNode();
25
27
 
@@ -27,25 +29,27 @@ export function PageStack({ homePage }: PageStackProps) {
27
29
  const index = path.length - 1;
28
30
 
29
31
  if (path.length <= 0) {
30
- return <PageStackContainer>{homePage ?? <HomePage />}</PageStackContainer>;
32
+ return (
33
+ <PageStackContainer style={style}>
34
+ {homePage ?? <HomePage />}
35
+ </PageStackContainer>
36
+ );
31
37
  }
32
38
 
33
39
  return (
34
- <>
35
- <PageStackContainer>
36
- {localNode && page && (
37
- <ErrorBoundary title="An error occurred while rendering this CoValue">
38
- <Page
39
- coId={page.coId}
40
- node={localNode}
41
- name={page.name || page.coId}
42
- onHeaderClick={goBack}
43
- onNavigate={addPages}
44
- isTopLevel={index === path.length - 1}
45
- />
46
- </ErrorBoundary>
47
- )}
48
- </PageStackContainer>
49
- </>
40
+ <PageStackContainer style={style}>
41
+ {localNode && page && (
42
+ <ErrorBoundary title="An error occurred while rendering this CoValue">
43
+ <Page
44
+ coId={page.coId}
45
+ node={localNode}
46
+ name={page.name || page.coId}
47
+ onHeaderClick={goBack}
48
+ onNavigate={addPages}
49
+ isTopLevel={index === path.length - 1}
50
+ />
51
+ </ErrorBoundary>
52
+ )}
53
+ </PageStackContainer>
50
54
  );
51
55
  }