create-ncblock 0.0.1
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.
- package/README.md +49 -0
- package/bin/cli.js +33 -0
- package/package.json +25 -0
- package/scripts/init.ts +527 -0
- package/scripts/scaffold-assets/AGENTS.md +65 -0
- package/scripts/utils/templates.ts +293 -0
- package/sdk-version.json +1 -0
- package/templates/debug/README.md +36 -0
- package/templates/debug/_gitignore +2 -0
- package/templates/debug/custom_blocks.json +9 -0
- package/templates/debug/dist/assets/index-Cet2SsjS.css +2 -0
- package/templates/debug/dist/assets/index-DAzv_fuh.js +9 -0
- package/templates/debug/dist/custom_blocks.json +9 -0
- package/templates/debug/dist/index.html +16 -0
- package/templates/debug/index.html +15 -0
- package/templates/debug/node_modules/.bin/browserslist +21 -0
- package/templates/debug/node_modules/.bin/esbuild +21 -0
- package/templates/debug/node_modules/.bin/jiti +21 -0
- package/templates/debug/node_modules/.bin/rollup +21 -0
- package/templates/debug/node_modules/.bin/tsc +21 -0
- package/templates/debug/node_modules/.bin/tsserver +21 -0
- package/templates/debug/node_modules/.bin/tsx +21 -0
- package/templates/debug/node_modules/.bin/vite +21 -0
- package/templates/debug/node_modules/.vite/deps/_metadata.json +50 -0
- package/templates/debug/node_modules/.vite/deps/package.json +3 -0
- package/templates/debug/node_modules/.vite/deps/react-CsV5wVHy.js +770 -0
- package/templates/debug/node_modules/.vite/deps/react-CsV5wVHy.js.map +1 -0
- package/templates/debug/node_modules/.vite/deps/react-dom.js +185 -0
- package/templates/debug/node_modules/.vite/deps/react-dom.js.map +1 -0
- package/templates/debug/node_modules/.vite/deps/react-dom_client.js +14384 -0
- package/templates/debug/node_modules/.vite/deps/react-dom_client.js.map +1 -0
- package/templates/debug/node_modules/.vite/deps/react.js +2 -0
- package/templates/debug/node_modules/.vite/deps/react_jsx-dev-runtime.js +204 -0
- package/templates/debug/node_modules/.vite/deps/react_jsx-dev-runtime.js.map +1 -0
- package/templates/debug/node_modules/.vite/deps/react_jsx-runtime.js +208 -0
- package/templates/debug/node_modules/.vite/deps/react_jsx-runtime.js.map +1 -0
- package/templates/debug/node_modules/.vite/deps/valibot.js +6623 -0
- package/templates/debug/node_modules/.vite/deps/valibot.js.map +1 -0
- package/templates/debug/node_modules/.vite-temp/vite.config.ts.timestamp-1778623720803-0bcf523a67aa8.mjs +15 -0
- package/templates/debug/package.json +30 -0
- package/templates/debug/src/index.css +62 -0
- package/templates/debug/src/index.tsx +1963 -0
- package/templates/debug/tsconfig.json +17 -0
- package/templates/debug/vite.config.ts +8 -0
- package/templates/empty/README.md +10 -0
- package/templates/empty/_gitignore +2 -0
- package/templates/empty/custom_blocks.json +12 -0
- package/templates/empty/dist/assets/index-CodJADav.js +9 -0
- package/templates/empty/dist/custom_blocks.json +12 -0
- package/templates/empty/dist/index.html +15 -0
- package/templates/empty/index.html +15 -0
- package/templates/empty/node_modules/.bin/esbuild +21 -0
- package/templates/empty/node_modules/.bin/jiti +21 -0
- package/templates/empty/node_modules/.bin/tsc +21 -0
- package/templates/empty/node_modules/.bin/tsserver +21 -0
- package/templates/empty/node_modules/.bin/tsx +21 -0
- package/templates/empty/node_modules/.bin/vite +21 -0
- package/templates/empty/node_modules/.vite/deps/_metadata.json +50 -0
- package/templates/empty/node_modules/.vite/deps/package.json +3 -0
- package/templates/empty/node_modules/.vite/deps/react-CsV5wVHy.js +770 -0
- package/templates/empty/node_modules/.vite/deps/react-CsV5wVHy.js.map +1 -0
- package/templates/empty/node_modules/.vite/deps/react-dom.js +185 -0
- package/templates/empty/node_modules/.vite/deps/react-dom.js.map +1 -0
- package/templates/empty/node_modules/.vite/deps/react-dom_client.js +14384 -0
- package/templates/empty/node_modules/.vite/deps/react-dom_client.js.map +1 -0
- package/templates/empty/node_modules/.vite/deps/react.js +2 -0
- package/templates/empty/node_modules/.vite/deps/react_jsx-dev-runtime.js +204 -0
- package/templates/empty/node_modules/.vite/deps/react_jsx-dev-runtime.js.map +1 -0
- package/templates/empty/node_modules/.vite/deps/react_jsx-runtime.js +208 -0
- package/templates/empty/node_modules/.vite/deps/react_jsx-runtime.js.map +1 -0
- package/templates/empty/node_modules/.vite/deps/valibot.js +6623 -0
- package/templates/empty/node_modules/.vite/deps/valibot.js.map +1 -0
- package/templates/empty/package.json +28 -0
- package/templates/empty/src/index.tsx +12 -0
- package/templates/empty/tsconfig.json +17 -0
- package/templates/empty/vite.config.ts +7 -0
- package/templates/gantt-chart/node_modules/.bin/tsc +21 -0
- package/templates/gantt-chart/node_modules/.bin/tsserver +21 -0
- package/templates/gantt-chart/node_modules/.bin/vite +21 -0
- package/templates/gantt-chart/node_modules/.vite/deps/_metadata.json +50 -0
- package/templates/gantt-chart/node_modules/.vite/deps/package.json +3 -0
- package/templates/gantt-chart/node_modules/.vite/deps/react-CsV5wVHy.js +770 -0
- package/templates/gantt-chart/node_modules/.vite/deps/react-CsV5wVHy.js.map +1 -0
- package/templates/gantt-chart/node_modules/.vite/deps/react-dom.js +185 -0
- package/templates/gantt-chart/node_modules/.vite/deps/react-dom.js.map +1 -0
- package/templates/gantt-chart/node_modules/.vite/deps/react-dom_client.js +14384 -0
- package/templates/gantt-chart/node_modules/.vite/deps/react-dom_client.js.map +1 -0
- package/templates/gantt-chart/node_modules/.vite/deps/react.js +2 -0
- package/templates/gantt-chart/node_modules/.vite/deps/react_jsx-dev-runtime.js +204 -0
- package/templates/gantt-chart/node_modules/.vite/deps/react_jsx-dev-runtime.js.map +1 -0
- package/templates/gantt-chart/node_modules/.vite/deps/react_jsx-runtime.js +208 -0
- package/templates/gantt-chart/node_modules/.vite/deps/react_jsx-runtime.js.map +1 -0
- package/templates/gantt-chart/node_modules/.vite/deps/valibot.js +6623 -0
- package/templates/gantt-chart/node_modules/.vite/deps/valibot.js.map +1 -0
- package/templates/hello-world/node_modules/.bin/tsc +21 -0
- package/templates/hello-world/node_modules/.bin/tsserver +21 -0
- package/templates/hello-world/node_modules/.bin/vite +21 -0
- package/templates/hello-world/node_modules/.vite/deps/_metadata.json +50 -0
- package/templates/hello-world/node_modules/.vite/deps/package.json +3 -0
- package/templates/hello-world/node_modules/.vite/deps/react-CsV5wVHy.js +770 -0
- package/templates/hello-world/node_modules/.vite/deps/react-CsV5wVHy.js.map +1 -0
- package/templates/hello-world/node_modules/.vite/deps/react-dom.js +185 -0
- package/templates/hello-world/node_modules/.vite/deps/react-dom.js.map +1 -0
- package/templates/hello-world/node_modules/.vite/deps/react-dom_client.js +14384 -0
- package/templates/hello-world/node_modules/.vite/deps/react-dom_client.js.map +1 -0
- package/templates/hello-world/node_modules/.vite/deps/react.js +2 -0
- package/templates/hello-world/node_modules/.vite/deps/react_jsx-dev-runtime.js +204 -0
- package/templates/hello-world/node_modules/.vite/deps/react_jsx-dev-runtime.js.map +1 -0
- package/templates/hello-world/node_modules/.vite/deps/react_jsx-runtime.js +208 -0
- package/templates/hello-world/node_modules/.vite/deps/react_jsx-runtime.js.map +1 -0
- package/templates/hello-world/node_modules/.vite/deps/valibot.js +6623 -0
- package/templates/hello-world/node_modules/.vite/deps/valibot.js.map +1 -0
- package/templates/interactive-resize/node_modules/.bin/tsc +21 -0
- package/templates/interactive-resize/node_modules/.bin/tsserver +21 -0
- package/templates/interactive-resize/node_modules/.bin/vite +21 -0
- package/templates/interactive-resize/node_modules/.vite/deps/_metadata.json +50 -0
- package/templates/interactive-resize/node_modules/.vite/deps/package.json +3 -0
- package/templates/interactive-resize/node_modules/.vite/deps/react-CsV5wVHy.js +770 -0
- package/templates/interactive-resize/node_modules/.vite/deps/react-CsV5wVHy.js.map +1 -0
- package/templates/interactive-resize/node_modules/.vite/deps/react-dom.js +185 -0
- package/templates/interactive-resize/node_modules/.vite/deps/react-dom.js.map +1 -0
- package/templates/interactive-resize/node_modules/.vite/deps/react-dom_client.js +14384 -0
- package/templates/interactive-resize/node_modules/.vite/deps/react-dom_client.js.map +1 -0
- package/templates/interactive-resize/node_modules/.vite/deps/react.js +2 -0
- package/templates/interactive-resize/node_modules/.vite/deps/react_jsx-dev-runtime.js +204 -0
- package/templates/interactive-resize/node_modules/.vite/deps/react_jsx-dev-runtime.js.map +1 -0
- package/templates/interactive-resize/node_modules/.vite/deps/react_jsx-runtime.js +208 -0
- package/templates/interactive-resize/node_modules/.vite/deps/react_jsx-runtime.js.map +1 -0
- package/templates/interactive-resize/node_modules/.vite/deps/valibot.js +6623 -0
- package/templates/interactive-resize/node_modules/.vite/deps/valibot.js.map +1 -0
- package/templates/org-chart/node_modules/.bin/tsc +21 -0
- package/templates/org-chart/node_modules/.bin/tsserver +21 -0
- package/templates/org-chart/node_modules/.bin/vite +21 -0
- package/templates/org-chart/node_modules/.vite/deps/_metadata.json +50 -0
- package/templates/org-chart/node_modules/.vite/deps/package.json +3 -0
- package/templates/org-chart/node_modules/.vite/deps/react-CsV5wVHy.js +770 -0
- package/templates/org-chart/node_modules/.vite/deps/react-CsV5wVHy.js.map +1 -0
- package/templates/org-chart/node_modules/.vite/deps/react-dom.js +185 -0
- package/templates/org-chart/node_modules/.vite/deps/react-dom.js.map +1 -0
- package/templates/org-chart/node_modules/.vite/deps/react-dom_client.js +14384 -0
- package/templates/org-chart/node_modules/.vite/deps/react-dom_client.js.map +1 -0
- package/templates/org-chart/node_modules/.vite/deps/react.js +2 -0
- package/templates/org-chart/node_modules/.vite/deps/react_jsx-dev-runtime.js +204 -0
- package/templates/org-chart/node_modules/.vite/deps/react_jsx-dev-runtime.js.map +1 -0
- package/templates/org-chart/node_modules/.vite/deps/react_jsx-runtime.js +208 -0
- package/templates/org-chart/node_modules/.vite/deps/react_jsx-runtime.js.map +1 -0
- package/templates/org-chart/node_modules/.vite/deps/valibot.js +6623 -0
- package/templates/org-chart/node_modules/.vite/deps/valibot.js.map +1 -0
- package/templates/radar-chart/README.md +55 -0
- package/templates/radar-chart/_gitignore +2 -0
- package/templates/radar-chart/custom_blocks.json +34 -0
- package/templates/radar-chart/dist/assets/index-DOf05oXg.css +2 -0
- package/templates/radar-chart/dist/assets/index-DWpNd1qt.js +9 -0
- package/templates/radar-chart/dist/custom_blocks.json +34 -0
- package/templates/radar-chart/dist/index.html +16 -0
- package/templates/radar-chart/index.html +15 -0
- package/templates/radar-chart/node_modules/.bin/esbuild +21 -0
- package/templates/radar-chart/node_modules/.bin/jiti +21 -0
- package/templates/radar-chart/node_modules/.bin/tsc +21 -0
- package/templates/radar-chart/node_modules/.bin/tsserver +21 -0
- package/templates/radar-chart/node_modules/.bin/tsx +21 -0
- package/templates/radar-chart/node_modules/.bin/vite +21 -0
- package/templates/radar-chart/node_modules/.vite/deps/_metadata.json +50 -0
- package/templates/radar-chart/node_modules/.vite/deps/package.json +3 -0
- package/templates/radar-chart/node_modules/.vite/deps/react-CsV5wVHy.js +770 -0
- package/templates/radar-chart/node_modules/.vite/deps/react-CsV5wVHy.js.map +1 -0
- package/templates/radar-chart/node_modules/.vite/deps/react-dom.js +185 -0
- package/templates/radar-chart/node_modules/.vite/deps/react-dom.js.map +1 -0
- package/templates/radar-chart/node_modules/.vite/deps/react-dom_client.js +14384 -0
- package/templates/radar-chart/node_modules/.vite/deps/react-dom_client.js.map +1 -0
- package/templates/radar-chart/node_modules/.vite/deps/react.js +2 -0
- package/templates/radar-chart/node_modules/.vite/deps/react_jsx-dev-runtime.js +204 -0
- package/templates/radar-chart/node_modules/.vite/deps/react_jsx-dev-runtime.js.map +1 -0
- package/templates/radar-chart/node_modules/.vite/deps/react_jsx-runtime.js +208 -0
- package/templates/radar-chart/node_modules/.vite/deps/react_jsx-runtime.js.map +1 -0
- package/templates/radar-chart/node_modules/.vite/deps/valibot.js +6623 -0
- package/templates/radar-chart/node_modules/.vite/deps/valibot.js.map +1 -0
- package/templates/radar-chart/package.json +30 -0
- package/templates/radar-chart/src/index.css +44 -0
- package/templates/radar-chart/src/index.tsx +531 -0
- package/templates/radar-chart/tsconfig.json +17 -0
- package/templates/radar-chart/vite.config.ts +8 -0
- package/templates/table-view/README.md +43 -0
- package/templates/table-view/_gitignore +2 -0
- package/templates/table-view/custom_blocks.json +9 -0
- package/templates/table-view/dist/assets/index-Bd8u_e4X.js +12 -0
- package/templates/table-view/dist/assets/index-BkZn3aQZ.css +1 -0
- package/templates/table-view/dist/custom_blocks.json +9 -0
- package/templates/table-view/dist/index.html +16 -0
- package/templates/table-view/index.html +15 -0
- package/templates/table-view/node_modules/.bin/esbuild +21 -0
- package/templates/table-view/node_modules/.bin/jiti +21 -0
- package/templates/table-view/node_modules/.bin/rollup +21 -0
- package/templates/table-view/node_modules/.bin/tsc +21 -0
- package/templates/table-view/node_modules/.bin/tsserver +21 -0
- package/templates/table-view/node_modules/.bin/tsx +21 -0
- package/templates/table-view/node_modules/.bin/vite +21 -0
- package/templates/table-view/node_modules/.vite/deps/@tanstack_react-table.js +2809 -0
- package/templates/table-view/node_modules/.vite/deps/@tanstack_react-table.js.map +1 -0
- package/templates/table-view/node_modules/.vite/deps/_metadata.json +56 -0
- package/templates/table-view/node_modules/.vite/deps/package.json +3 -0
- package/templates/table-view/node_modules/.vite/deps/react-D5jdVkJj.js +790 -0
- package/templates/table-view/node_modules/.vite/deps/react-D5jdVkJj.js.map +1 -0
- package/templates/table-view/node_modules/.vite/deps/react-dom.js +185 -0
- package/templates/table-view/node_modules/.vite/deps/react-dom.js.map +1 -0
- package/templates/table-view/node_modules/.vite/deps/react-dom_client.js +14384 -0
- package/templates/table-view/node_modules/.vite/deps/react-dom_client.js.map +1 -0
- package/templates/table-view/node_modules/.vite/deps/react.js +2 -0
- package/templates/table-view/node_modules/.vite/deps/react_jsx-dev-runtime.js +204 -0
- package/templates/table-view/node_modules/.vite/deps/react_jsx-dev-runtime.js.map +1 -0
- package/templates/table-view/node_modules/.vite/deps/react_jsx-runtime.js +208 -0
- package/templates/table-view/node_modules/.vite/deps/react_jsx-runtime.js.map +1 -0
- package/templates/table-view/node_modules/.vite/deps/valibot.js +6623 -0
- package/templates/table-view/node_modules/.vite/deps/valibot.js.map +1 -0
- package/templates/table-view/package.json +31 -0
- package/templates/table-view/src/index.css +256 -0
- package/templates/table-view/src/index.tsx +1814 -0
- package/templates/table-view/src/table-model.ts +663 -0
- package/templates/table-view/tsconfig.json +17 -0
- package/templates/table-view/vite.config.ts +8 -0
- package/templates/us-heatmap/node_modules/.bin/tsc +21 -0
- package/templates/us-heatmap/node_modules/.bin/tsserver +21 -0
- package/templates/us-heatmap/node_modules/.bin/vite +21 -0
- package/templates/us-heatmap/node_modules/.vite/deps/_metadata.json +50 -0
- package/templates/us-heatmap/node_modules/.vite/deps/package.json +3 -0
- package/templates/us-heatmap/node_modules/.vite/deps/react-CsV5wVHy.js +770 -0
- package/templates/us-heatmap/node_modules/.vite/deps/react-CsV5wVHy.js.map +1 -0
- package/templates/us-heatmap/node_modules/.vite/deps/react-dom.js +185 -0
- package/templates/us-heatmap/node_modules/.vite/deps/react-dom.js.map +1 -0
- package/templates/us-heatmap/node_modules/.vite/deps/react-dom_client.js +14384 -0
- package/templates/us-heatmap/node_modules/.vite/deps/react-dom_client.js.map +1 -0
- package/templates/us-heatmap/node_modules/.vite/deps/react.js +2 -0
- package/templates/us-heatmap/node_modules/.vite/deps/react_jsx-dev-runtime.js +204 -0
- package/templates/us-heatmap/node_modules/.vite/deps/react_jsx-dev-runtime.js.map +1 -0
- package/templates/us-heatmap/node_modules/.vite/deps/react_jsx-runtime.js +208 -0
- package/templates/us-heatmap/node_modules/.vite/deps/react_jsx-runtime.js.map +1 -0
- package/templates/us-heatmap/node_modules/.vite/deps/valibot.js +6623 -0
- package/templates/us-heatmap/node_modules/.vite/deps/valibot.js.map +1 -0
|
@@ -0,0 +1,1963 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type NotionCreatePagePosition,
|
|
3
|
+
NotionCustomBlock,
|
|
4
|
+
type NotionCustomBlockContext,
|
|
5
|
+
type NotionDataSource,
|
|
6
|
+
type NotionDataSourceId,
|
|
7
|
+
type NotionPage,
|
|
8
|
+
type NotionPageId,
|
|
9
|
+
pages,
|
|
10
|
+
useCustomBlockContext,
|
|
11
|
+
useDataSourceDefinitions,
|
|
12
|
+
useTheme,
|
|
13
|
+
} from "ncblock"
|
|
14
|
+
import React, {
|
|
15
|
+
Component,
|
|
16
|
+
useCallback,
|
|
17
|
+
useEffect,
|
|
18
|
+
useMemo,
|
|
19
|
+
useRef,
|
|
20
|
+
useState,
|
|
21
|
+
} from "react"
|
|
22
|
+
import ReactDOM from "react-dom/client"
|
|
23
|
+
|
|
24
|
+
import "./index.css"
|
|
25
|
+
|
|
26
|
+
type LogEntry = {
|
|
27
|
+
id: number
|
|
28
|
+
time: string
|
|
29
|
+
direction: "sent" | "received"
|
|
30
|
+
type: string
|
|
31
|
+
data: unknown
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type ThemeOverride = "host" | "light" | "dark"
|
|
35
|
+
type TrackedPage = {
|
|
36
|
+
page: NotionPage
|
|
37
|
+
draftTitle: string
|
|
38
|
+
draftIcon: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const cardClass =
|
|
42
|
+
"mb-3 rounded-lg border border-(--border) bg-(--card-bg) px-4 py-3"
|
|
43
|
+
const labelClass =
|
|
44
|
+
"text-[10px] font-semibold uppercase tracking-[0.06em] text-(--muted)"
|
|
45
|
+
const metaTextClass =
|
|
46
|
+
"font-mono text-[11px] font-normal normal-case tracking-normal text-(--muted)"
|
|
47
|
+
|
|
48
|
+
function App() {
|
|
49
|
+
const ctx = useCustomBlockContext()
|
|
50
|
+
const hostThemeValue = useTheme()
|
|
51
|
+
const dataSources = useDataSourceDefinitions()
|
|
52
|
+
const [isPaused, setIsPaused] = useState(false)
|
|
53
|
+
const { log, initData, send, clearLog } = useMessageLog(isPaused)
|
|
54
|
+
const [themeOverride, setThemeOverride] = useState<ThemeOverride>("host")
|
|
55
|
+
const inIframe = window.parent !== window
|
|
56
|
+
const hostTheme = hostThemeValue === "dark" ? "dark" : "light"
|
|
57
|
+
const theme = themeOverride === "host" ? hostTheme : themeOverride
|
|
58
|
+
const { toast, copy } = useCopyToast()
|
|
59
|
+
const viewport = useViewportInfo()
|
|
60
|
+
const readyAt = useReadyTimestamp(true)
|
|
61
|
+
const elapsedSeconds = useElapsedSeconds(readyAt)
|
|
62
|
+
|
|
63
|
+
const status = !inIframe
|
|
64
|
+
? { label: "Standalone", color: "var(--status-standalone)" }
|
|
65
|
+
: {
|
|
66
|
+
label: `Connected${
|
|
67
|
+
elapsedSeconds !== null ? ` · ${formatElapsed(elapsedSeconds)}` : ""
|
|
68
|
+
}`,
|
|
69
|
+
color: "var(--status-connected)",
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const themeVars = {
|
|
73
|
+
"--status-color": status.color,
|
|
74
|
+
} as React.CSSProperties
|
|
75
|
+
|
|
76
|
+
const contextValue = {
|
|
77
|
+
theme: hostThemeValue,
|
|
78
|
+
customBlockId: ctx?.customBlockId,
|
|
79
|
+
parent: ctx?.parent,
|
|
80
|
+
page: ctx?.page,
|
|
81
|
+
dataSources,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const metadataRows: Array<[string, string]> = [
|
|
85
|
+
["origin", window.location.origin],
|
|
86
|
+
["url", window.location.href],
|
|
87
|
+
["sandbox", inIframe ? "iframe" : "standalone"],
|
|
88
|
+
[
|
|
89
|
+
"theme",
|
|
90
|
+
`${hostThemeValue ?? "unknown"}${themeOverride !== "host" ? ` (override: ${themeOverride})` : ""}`,
|
|
91
|
+
],
|
|
92
|
+
["bridgeProtocolVersion", getBridgeProtocolVersion(initData) ?? "\u2014"],
|
|
93
|
+
["viewport", `${viewport.innerWidth} \u00d7 ${viewport.innerHeight}`],
|
|
94
|
+
["contentHeight", `${viewport.scrollHeight}px`],
|
|
95
|
+
["devicePixelRatio", String(viewport.devicePixelRatio)],
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<div
|
|
100
|
+
data-theme={theme}
|
|
101
|
+
className="bg-(--app-bg) p-8 text-(--foreground)"
|
|
102
|
+
style={themeVars}
|
|
103
|
+
>
|
|
104
|
+
<div className="mb-6 flex flex-wrap items-center justify-between gap-3">
|
|
105
|
+
<h1 className="text-[32px] font-bold leading-tight tracking-[-0.03em]">
|
|
106
|
+
Debug
|
|
107
|
+
</h1>
|
|
108
|
+
<div className="flex items-center gap-3">
|
|
109
|
+
<div className="flex items-center gap-1.5 leading-none">
|
|
110
|
+
<span
|
|
111
|
+
className="inline-block h-2 w-2 rounded-full bg-(--status-color)"
|
|
112
|
+
aria-hidden="true"
|
|
113
|
+
/>
|
|
114
|
+
<span className="text-[13px] font-medium leading-none text-(--status-color)">
|
|
115
|
+
{status.label}
|
|
116
|
+
</span>
|
|
117
|
+
</div>
|
|
118
|
+
<ThemeToggle value={themeOverride} onChange={setThemeOverride} />
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
{/* Connection metadata */}
|
|
123
|
+
<CollapsibleCard
|
|
124
|
+
label="Metadata"
|
|
125
|
+
headerExtra={
|
|
126
|
+
<span className={metaTextClass}>{metadataRows.length} fields</span>
|
|
127
|
+
}
|
|
128
|
+
headerActions={
|
|
129
|
+
<IconButton
|
|
130
|
+
label="Copy metadata JSON"
|
|
131
|
+
onClick={() =>
|
|
132
|
+
copy(serializeForCopy(Object.fromEntries(metadataRows)))
|
|
133
|
+
}
|
|
134
|
+
>
|
|
135
|
+
<CopyIcon />
|
|
136
|
+
</IconButton>
|
|
137
|
+
}
|
|
138
|
+
>
|
|
139
|
+
<div className="rounded-lg border border-(--border) bg-(--app-bg) p-1">
|
|
140
|
+
{metadataRows.map(([key, val]) => (
|
|
141
|
+
<MetadataRow key={key} label={key} value={val} onCopy={copy} />
|
|
142
|
+
))}
|
|
143
|
+
</div>
|
|
144
|
+
</CollapsibleCard>
|
|
145
|
+
|
|
146
|
+
{/* Context state */}
|
|
147
|
+
<CollapsibleCard
|
|
148
|
+
label="Init payload"
|
|
149
|
+
headerExtra={
|
|
150
|
+
<span className={metaTextClass}>
|
|
151
|
+
{Object.keys(contextValue).length} keys
|
|
152
|
+
</span>
|
|
153
|
+
}
|
|
154
|
+
headerActions={
|
|
155
|
+
<IconButton
|
|
156
|
+
label="Copy context JSON"
|
|
157
|
+
onClick={() => copy(serializeForCopy(contextValue))}
|
|
158
|
+
>
|
|
159
|
+
<CopyIcon />
|
|
160
|
+
</IconButton>
|
|
161
|
+
}
|
|
162
|
+
>
|
|
163
|
+
<JsonPanel
|
|
164
|
+
value={contextValue}
|
|
165
|
+
onCopy={copy}
|
|
166
|
+
defaultExpandedDepth={2}
|
|
167
|
+
/>
|
|
168
|
+
</CollapsibleCard>
|
|
169
|
+
|
|
170
|
+
{/* Create page */}
|
|
171
|
+
<CreatePageCard context={ctx} dataSources={dataSources} />
|
|
172
|
+
|
|
173
|
+
{/* Send message */}
|
|
174
|
+
<SendMessageCard onSend={send} />
|
|
175
|
+
|
|
176
|
+
{/* Events log */}
|
|
177
|
+
<EventsLog
|
|
178
|
+
log={log}
|
|
179
|
+
onCopy={copy}
|
|
180
|
+
onClear={clearLog}
|
|
181
|
+
isPaused={isPaused}
|
|
182
|
+
onPauseChange={setIsPaused}
|
|
183
|
+
/>
|
|
184
|
+
|
|
185
|
+
{/* Copy toast */}
|
|
186
|
+
{toast !== null && (
|
|
187
|
+
<div className="fixed bottom-5 left-1/2 z-1000 max-w-[80%] -translate-x-1/2 overflow-hidden text-ellipsis whitespace-nowrap rounded-lg border border-(--border) bg-(--toast-bg) px-3.5 py-1.5 font-mono text-xs text-(--toast-fg) shadow-(--toast-shadow)">
|
|
188
|
+
Copied: {toast}
|
|
189
|
+
</div>
|
|
190
|
+
)}
|
|
191
|
+
</div>
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// --- Hooks ---
|
|
196
|
+
|
|
197
|
+
function useMessageLog(isPaused: boolean) {
|
|
198
|
+
const [log, setLog] = useState<LogEntry[]>([])
|
|
199
|
+
const [initData, setInitData] = useState<unknown>(null)
|
|
200
|
+
const idRef = useRef(0)
|
|
201
|
+
const pausedRef = useRef(isPaused)
|
|
202
|
+
pausedRef.current = isPaused
|
|
203
|
+
const initSeenRef = useRef(false)
|
|
204
|
+
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
const inIframe = window.parent !== window
|
|
207
|
+
const handler = (event: MessageEvent) => {
|
|
208
|
+
if (!inIframe || event.source !== window.parent) {
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
const d = event.data
|
|
212
|
+
if (
|
|
213
|
+
!d ||
|
|
214
|
+
typeof d !== "object" ||
|
|
215
|
+
typeof (d as { type?: unknown }).type !== "string"
|
|
216
|
+
) {
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
const type = (d as { type: string }).type
|
|
220
|
+
if (type === "init" && !initSeenRef.current) {
|
|
221
|
+
initSeenRef.current = true
|
|
222
|
+
setInitData(d)
|
|
223
|
+
}
|
|
224
|
+
if (pausedRef.current) {
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
setLog(prev => [
|
|
228
|
+
...prev,
|
|
229
|
+
{
|
|
230
|
+
id: idRef.current++,
|
|
231
|
+
time: formatTime(new Date()),
|
|
232
|
+
direction: "received",
|
|
233
|
+
type,
|
|
234
|
+
data: d,
|
|
235
|
+
},
|
|
236
|
+
])
|
|
237
|
+
}
|
|
238
|
+
window.addEventListener("message", handler)
|
|
239
|
+
return () => {
|
|
240
|
+
window.removeEventListener("message", handler)
|
|
241
|
+
}
|
|
242
|
+
}, [])
|
|
243
|
+
|
|
244
|
+
const send = useCallback((message: Record<string, unknown>) => {
|
|
245
|
+
window.parent.postMessage(message, "*")
|
|
246
|
+
const type = typeof message.type === "string" ? message.type : "unknown"
|
|
247
|
+
setLog(prev => [
|
|
248
|
+
...prev,
|
|
249
|
+
{
|
|
250
|
+
id: idRef.current++,
|
|
251
|
+
time: formatTime(new Date()),
|
|
252
|
+
direction: "sent",
|
|
253
|
+
type,
|
|
254
|
+
data: message,
|
|
255
|
+
},
|
|
256
|
+
])
|
|
257
|
+
}, [])
|
|
258
|
+
|
|
259
|
+
const clearLog = useCallback(() => setLog([]), [])
|
|
260
|
+
|
|
261
|
+
return { log, initData, send, clearLog }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function useReadyTimestamp(isReady: boolean): number | null {
|
|
265
|
+
const [readyAt, setReadyAt] = useState<number | null>(null)
|
|
266
|
+
useEffect(() => {
|
|
267
|
+
if (isReady && readyAt === null) {
|
|
268
|
+
setReadyAt(Date.now())
|
|
269
|
+
}
|
|
270
|
+
}, [isReady, readyAt])
|
|
271
|
+
return readyAt
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function useCopyToast() {
|
|
275
|
+
const [toast, setToast] = useState<string | null>(null)
|
|
276
|
+
|
|
277
|
+
useEffect(() => {
|
|
278
|
+
if (toast === null) {
|
|
279
|
+
return
|
|
280
|
+
}
|
|
281
|
+
const t = setTimeout(() => setToast(null), 1200)
|
|
282
|
+
return () => clearTimeout(t)
|
|
283
|
+
}, [toast])
|
|
284
|
+
|
|
285
|
+
const copy = (text: string) => {
|
|
286
|
+
// navigator.clipboard.writeText is blocked inside sandboxed iframes,
|
|
287
|
+
// so fall back to execCommand("copy") with a temporary textarea.
|
|
288
|
+
try {
|
|
289
|
+
const textarea = document.createElement("textarea")
|
|
290
|
+
textarea.value = text
|
|
291
|
+
textarea.style.position = "fixed"
|
|
292
|
+
textarea.style.opacity = "0"
|
|
293
|
+
document.body.appendChild(textarea)
|
|
294
|
+
textarea.select()
|
|
295
|
+
document.execCommand("copy")
|
|
296
|
+
document.body.removeChild(textarea)
|
|
297
|
+
} catch {
|
|
298
|
+
// Silently fail if even execCommand is blocked
|
|
299
|
+
}
|
|
300
|
+
setToast(text.length > 40 ? text.slice(0, 40) + "\u2026" : text)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return { toast, copy }
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function useElapsedSeconds(from: number | null) {
|
|
307
|
+
const [now, setNow] = useState(() => Date.now())
|
|
308
|
+
useEffect(() => {
|
|
309
|
+
if (from === null) {
|
|
310
|
+
return
|
|
311
|
+
}
|
|
312
|
+
setNow(Date.now())
|
|
313
|
+
const id = setInterval(() => setNow(Date.now()), 1000)
|
|
314
|
+
return () => clearInterval(id)
|
|
315
|
+
}, [from])
|
|
316
|
+
if (from === null) {
|
|
317
|
+
return null
|
|
318
|
+
}
|
|
319
|
+
return Math.max(0, Math.floor((now - from) / 1000))
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function useViewportInfo() {
|
|
323
|
+
const [info, setInfo] = useState(() => captureViewportInfo())
|
|
324
|
+
useEffect(() => {
|
|
325
|
+
const onChange = () => setInfo(captureViewportInfo())
|
|
326
|
+
window.addEventListener("resize", onChange)
|
|
327
|
+
const root = document.getElementById("root")
|
|
328
|
+
let ro: ResizeObserver | null = null
|
|
329
|
+
if (root && typeof ResizeObserver !== "undefined") {
|
|
330
|
+
ro = new ResizeObserver(onChange)
|
|
331
|
+
ro.observe(root)
|
|
332
|
+
}
|
|
333
|
+
return () => {
|
|
334
|
+
window.removeEventListener("resize", onChange)
|
|
335
|
+
ro?.disconnect()
|
|
336
|
+
}
|
|
337
|
+
}, [])
|
|
338
|
+
return info
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function captureViewportInfo() {
|
|
342
|
+
const root = document.getElementById("root")
|
|
343
|
+
return {
|
|
344
|
+
innerWidth: window.innerWidth,
|
|
345
|
+
innerHeight: window.innerHeight,
|
|
346
|
+
scrollHeight: root?.scrollHeight ?? document.documentElement.scrollHeight,
|
|
347
|
+
devicePixelRatio: window.devicePixelRatio,
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function useOutsideClick(
|
|
352
|
+
ref: React.RefObject<HTMLElement | null>,
|
|
353
|
+
open: boolean,
|
|
354
|
+
onClose: () => void,
|
|
355
|
+
) {
|
|
356
|
+
useEffect(() => {
|
|
357
|
+
if (!open) {
|
|
358
|
+
return
|
|
359
|
+
}
|
|
360
|
+
const onDoc = (event: MouseEvent) => {
|
|
361
|
+
if (ref.current && !ref.current.contains(event.target as Node)) {
|
|
362
|
+
onClose()
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
document.addEventListener("mousedown", onDoc)
|
|
366
|
+
return () => document.removeEventListener("mousedown", onDoc)
|
|
367
|
+
}, [open, onClose, ref])
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// --- Components ---
|
|
371
|
+
|
|
372
|
+
function CollapsibleCard({
|
|
373
|
+
label,
|
|
374
|
+
headerActions,
|
|
375
|
+
headerExtra,
|
|
376
|
+
defaultOpen = true,
|
|
377
|
+
children,
|
|
378
|
+
}: {
|
|
379
|
+
label: string
|
|
380
|
+
headerActions?: React.ReactNode
|
|
381
|
+
headerExtra?: React.ReactNode
|
|
382
|
+
defaultOpen?: boolean
|
|
383
|
+
children: React.ReactNode
|
|
384
|
+
}) {
|
|
385
|
+
const [open, setOpen] = useState(defaultOpen)
|
|
386
|
+
return (
|
|
387
|
+
<div className={cardClass}>
|
|
388
|
+
<div className={`-mx-1 flex items-center gap-2 ${open ? "mb-2.5" : ""}`}>
|
|
389
|
+
<button
|
|
390
|
+
type="button"
|
|
391
|
+
aria-expanded={open}
|
|
392
|
+
onClick={() => setOpen(o => !o)}
|
|
393
|
+
className="group/trigger flex min-w-0 flex-1 items-center gap-2 rounded-md px-1.5 py-1 text-left outline-none transition-colors duration-150 hover:bg-(--hover-bg) focus-visible:ring-1 focus-visible:ring-(--button-bg)"
|
|
394
|
+
>
|
|
395
|
+
<ChevronIcon isOpen={open} />
|
|
396
|
+
<span
|
|
397
|
+
className={`${labelClass} transition-opacity duration-150 group-hover/trigger:text-(--foreground)`}
|
|
398
|
+
>
|
|
399
|
+
{label}
|
|
400
|
+
</span>
|
|
401
|
+
{headerExtra && (
|
|
402
|
+
<span className="flex min-w-0 items-center gap-1.5 truncate">
|
|
403
|
+
{headerExtra}
|
|
404
|
+
</span>
|
|
405
|
+
)}
|
|
406
|
+
</button>
|
|
407
|
+
{headerActions && (
|
|
408
|
+
<div
|
|
409
|
+
className="flex shrink-0 items-center gap-0.5"
|
|
410
|
+
onClick={e => e.stopPropagation()}
|
|
411
|
+
>
|
|
412
|
+
{headerActions}
|
|
413
|
+
</div>
|
|
414
|
+
)}
|
|
415
|
+
</div>
|
|
416
|
+
{open && children}
|
|
417
|
+
</div>
|
|
418
|
+
)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function MetadataRow({
|
|
422
|
+
label,
|
|
423
|
+
value,
|
|
424
|
+
onCopy,
|
|
425
|
+
}: {
|
|
426
|
+
label: string
|
|
427
|
+
value: string
|
|
428
|
+
onCopy: (text: string) => void
|
|
429
|
+
}) {
|
|
430
|
+
return (
|
|
431
|
+
<div className="group flex items-center gap-2 rounded-md px-1.5 py-1 hover:bg-(--hover-bg)">
|
|
432
|
+
<span className="w-3.5 shrink-0" />
|
|
433
|
+
<span className="shrink-0 font-mono text-[12px] text-(--muted)">
|
|
434
|
+
{label}
|
|
435
|
+
</span>
|
|
436
|
+
<span className="shrink-0 font-mono text-[12px] opacity-60">:</span>
|
|
437
|
+
<span className="min-w-0 flex-1 break-all font-mono text-[12px] leading-5 text-(--foreground)">
|
|
438
|
+
{value}
|
|
439
|
+
</span>
|
|
440
|
+
<div className="shrink-0 opacity-0 transition-opacity duration-150 group-hover:opacity-100 focus-within:opacity-100">
|
|
441
|
+
<CopyValueButton
|
|
442
|
+
label={`Copy ${label}`}
|
|
443
|
+
onClick={() => onCopy(value)}
|
|
444
|
+
/>
|
|
445
|
+
</div>
|
|
446
|
+
</div>
|
|
447
|
+
)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function ThemeToggle({
|
|
451
|
+
value,
|
|
452
|
+
onChange,
|
|
453
|
+
}: {
|
|
454
|
+
value: ThemeOverride
|
|
455
|
+
onChange: (value: ThemeOverride) => void
|
|
456
|
+
}) {
|
|
457
|
+
const options: { id: ThemeOverride; label: string }[] = [
|
|
458
|
+
{ id: "host", label: "Host" },
|
|
459
|
+
{ id: "light", label: "Light" },
|
|
460
|
+
{ id: "dark", label: "Dark" },
|
|
461
|
+
]
|
|
462
|
+
return (
|
|
463
|
+
<div
|
|
464
|
+
role="group"
|
|
465
|
+
aria-label="Theme override"
|
|
466
|
+
className="inline-flex rounded-md border border-(--border) p-0.5 text-[11px]"
|
|
467
|
+
>
|
|
468
|
+
{options.map(option => {
|
|
469
|
+
const active = option.id === value
|
|
470
|
+
return (
|
|
471
|
+
<button
|
|
472
|
+
key={option.id}
|
|
473
|
+
type="button"
|
|
474
|
+
onClick={() => onChange(option.id)}
|
|
475
|
+
className={`rounded-[4px] px-2 py-0.5 font-medium outline-none transition-colors ${
|
|
476
|
+
active
|
|
477
|
+
? "bg-(--button-bg) text-(--button-fg)"
|
|
478
|
+
: "text-(--muted) hover:bg-(--hover-bg) hover:text-(--foreground)"
|
|
479
|
+
}`}
|
|
480
|
+
>
|
|
481
|
+
{option.label}
|
|
482
|
+
</button>
|
|
483
|
+
)
|
|
484
|
+
})}
|
|
485
|
+
</div>
|
|
486
|
+
)
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function JsonPanel({
|
|
490
|
+
value,
|
|
491
|
+
onCopy,
|
|
492
|
+
defaultExpandedDepth = 2,
|
|
493
|
+
embedded = false,
|
|
494
|
+
}: {
|
|
495
|
+
value: unknown
|
|
496
|
+
onCopy: (text: string) => void
|
|
497
|
+
defaultExpandedDepth?: number
|
|
498
|
+
embedded?: boolean
|
|
499
|
+
}) {
|
|
500
|
+
const expandable = isExpandableJsonValue(value)
|
|
501
|
+
const entries = expandable
|
|
502
|
+
? Array.isArray(value)
|
|
503
|
+
? value.map((item, index) => [String(index), item] as const)
|
|
504
|
+
: Object.entries(value)
|
|
505
|
+
: []
|
|
506
|
+
|
|
507
|
+
const containerClass = embedded
|
|
508
|
+
? ""
|
|
509
|
+
: "max-h-[360px] overflow-auto rounded-lg border border-(--border) bg-(--app-bg) p-1"
|
|
510
|
+
|
|
511
|
+
return (
|
|
512
|
+
<div className={containerClass}>
|
|
513
|
+
{expandable ? (
|
|
514
|
+
entries.length === 0 ? (
|
|
515
|
+
<div className="px-2 py-1.5 font-mono text-[11px] italic text-(--muted)">
|
|
516
|
+
Empty {Array.isArray(value) ? "array" : "object"}
|
|
517
|
+
</div>
|
|
518
|
+
) : (
|
|
519
|
+
<div>
|
|
520
|
+
{entries.map(([childKey, childValue]) => (
|
|
521
|
+
<JsonNode
|
|
522
|
+
key={childKey}
|
|
523
|
+
name={childKey}
|
|
524
|
+
value={childValue}
|
|
525
|
+
path={childKey}
|
|
526
|
+
depth={0}
|
|
527
|
+
onCopy={onCopy}
|
|
528
|
+
defaultExpandedDepth={defaultExpandedDepth}
|
|
529
|
+
/>
|
|
530
|
+
))}
|
|
531
|
+
</div>
|
|
532
|
+
)
|
|
533
|
+
) : (
|
|
534
|
+
<JsonNode
|
|
535
|
+
name="value"
|
|
536
|
+
value={value}
|
|
537
|
+
path="value"
|
|
538
|
+
depth={0}
|
|
539
|
+
onCopy={onCopy}
|
|
540
|
+
defaultExpandedDepth={defaultExpandedDepth}
|
|
541
|
+
/>
|
|
542
|
+
)}
|
|
543
|
+
</div>
|
|
544
|
+
)
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function JsonNode({
|
|
548
|
+
name,
|
|
549
|
+
value,
|
|
550
|
+
path,
|
|
551
|
+
depth,
|
|
552
|
+
onCopy,
|
|
553
|
+
defaultExpandedDepth,
|
|
554
|
+
}: {
|
|
555
|
+
name: string
|
|
556
|
+
value: unknown
|
|
557
|
+
path: string
|
|
558
|
+
depth: number
|
|
559
|
+
onCopy: (text: string) => void
|
|
560
|
+
defaultExpandedDepth: number
|
|
561
|
+
}) {
|
|
562
|
+
const expandable = isExpandableJsonValue(value)
|
|
563
|
+
const [isOpen, setIsOpen] = useState(depth < defaultExpandedDepth)
|
|
564
|
+
|
|
565
|
+
if (!expandable) {
|
|
566
|
+
return (
|
|
567
|
+
<div className="group flex items-center gap-2 rounded-md px-1.5 py-1 hover:bg-(--hover-bg)">
|
|
568
|
+
<span className="w-3.5 shrink-0" />
|
|
569
|
+
<span className="shrink-0 font-mono text-[12px] text-(--muted)">
|
|
570
|
+
{name}
|
|
571
|
+
</span>
|
|
572
|
+
<span className="shrink-0 font-mono text-[12px] opacity-60">:</span>
|
|
573
|
+
<span className="min-w-0 flex-1 break-all font-mono text-[12px] leading-5 text-(--foreground)">
|
|
574
|
+
{formatPrimitiveValue(value)}
|
|
575
|
+
</span>
|
|
576
|
+
<div className="shrink-0 opacity-0 transition-opacity duration-150 group-hover:opacity-100 focus-within:opacity-100">
|
|
577
|
+
<CopyValueButton
|
|
578
|
+
label={`Copy ${name}`}
|
|
579
|
+
onClick={() => onCopy(serializeForCopy(value))}
|
|
580
|
+
/>
|
|
581
|
+
</div>
|
|
582
|
+
</div>
|
|
583
|
+
)
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const entries = Array.isArray(value)
|
|
587
|
+
? value.map((item, index) => [String(index), item] as const)
|
|
588
|
+
: Object.entries(value)
|
|
589
|
+
const showCopyButton = depth > 0
|
|
590
|
+
|
|
591
|
+
return (
|
|
592
|
+
<div>
|
|
593
|
+
<div className="group flex items-stretch gap-1 rounded-md hover:bg-(--hover-bg)">
|
|
594
|
+
<button
|
|
595
|
+
type="button"
|
|
596
|
+
aria-expanded={isOpen}
|
|
597
|
+
onClick={() => setIsOpen(open => !open)}
|
|
598
|
+
className="flex min-h-7 min-w-0 flex-1 items-center gap-2 rounded-md px-1.5 py-1 text-left outline-none focus-visible:ring-1 focus-visible:ring-(--button-bg)"
|
|
599
|
+
>
|
|
600
|
+
<ChevronIcon isOpen={isOpen} />
|
|
601
|
+
<span className="shrink-0 font-mono text-[12px] text-(--muted)">
|
|
602
|
+
{name}
|
|
603
|
+
</span>
|
|
604
|
+
<span className="shrink-0 rounded-full bg-(--chip-bg) px-1.5 py-0.5 font-mono text-[9px] uppercase tracking-[0.04em] text-(--muted)">
|
|
605
|
+
{getJsonTypeLabel(value)}
|
|
606
|
+
</span>
|
|
607
|
+
<span className="min-w-0 truncate font-mono text-[11px] text-(--muted) opacity-70">
|
|
608
|
+
{formatInlinePreview(value)}
|
|
609
|
+
</span>
|
|
610
|
+
</button>
|
|
611
|
+
{showCopyButton && (
|
|
612
|
+
<div className="flex shrink-0 items-center pr-1 opacity-0 transition-opacity duration-150 group-hover:opacity-100 focus-within:opacity-100">
|
|
613
|
+
<CopyValueButton
|
|
614
|
+
label={`Copy ${name}`}
|
|
615
|
+
onClick={() => onCopy(serializeForCopy(value))}
|
|
616
|
+
/>
|
|
617
|
+
</div>
|
|
618
|
+
)}
|
|
619
|
+
</div>
|
|
620
|
+
{isOpen && (
|
|
621
|
+
<div className="ml-[9px] border-l border-(--border-subtle) pl-1">
|
|
622
|
+
{entries.length === 0 ? (
|
|
623
|
+
<div className="px-2.5 py-1 font-mono text-[11px] italic text-(--muted)">
|
|
624
|
+
Empty {Array.isArray(value) ? "array" : "object"}
|
|
625
|
+
</div>
|
|
626
|
+
) : (
|
|
627
|
+
<div>
|
|
628
|
+
{entries.map(([childKey, childValue]) => (
|
|
629
|
+
<JsonNode
|
|
630
|
+
key={`${path}.${childKey}`}
|
|
631
|
+
name={childKey}
|
|
632
|
+
value={childValue}
|
|
633
|
+
path={`${path}.${childKey}`}
|
|
634
|
+
depth={depth + 1}
|
|
635
|
+
onCopy={onCopy}
|
|
636
|
+
defaultExpandedDepth={defaultExpandedDepth}
|
|
637
|
+
/>
|
|
638
|
+
))}
|
|
639
|
+
</div>
|
|
640
|
+
)}
|
|
641
|
+
</div>
|
|
642
|
+
)}
|
|
643
|
+
</div>
|
|
644
|
+
)
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function EventsLog({
|
|
648
|
+
log,
|
|
649
|
+
onCopy,
|
|
650
|
+
onClear,
|
|
651
|
+
isPaused,
|
|
652
|
+
onPauseChange,
|
|
653
|
+
}: {
|
|
654
|
+
log: LogEntry[]
|
|
655
|
+
onCopy: (text: string) => void
|
|
656
|
+
onClear: () => void
|
|
657
|
+
isPaused: boolean
|
|
658
|
+
onPauseChange: (paused: boolean) => void
|
|
659
|
+
}) {
|
|
660
|
+
const [excludedDirections, setExcludedDirections] = useState<
|
|
661
|
+
Set<"sent" | "received">
|
|
662
|
+
>(() => new Set())
|
|
663
|
+
const [excludedTypes, setExcludedTypes] = useState<Set<string>>(
|
|
664
|
+
() => new Set(),
|
|
665
|
+
)
|
|
666
|
+
const [expanded, setExpanded] = useState<Set<number>>(() => new Set())
|
|
667
|
+
const [filterOpen, setFilterOpen] = useState(false)
|
|
668
|
+
|
|
669
|
+
const availableTypes = useMemo(() => {
|
|
670
|
+
const set = new Set<string>()
|
|
671
|
+
for (const entry of log) {
|
|
672
|
+
set.add(entry.type)
|
|
673
|
+
}
|
|
674
|
+
return [...set].sort()
|
|
675
|
+
}, [log])
|
|
676
|
+
|
|
677
|
+
const filtered = useMemo(
|
|
678
|
+
() =>
|
|
679
|
+
log.filter(
|
|
680
|
+
entry =>
|
|
681
|
+
!excludedDirections.has(entry.direction) &&
|
|
682
|
+
!excludedTypes.has(entry.type),
|
|
683
|
+
),
|
|
684
|
+
[log, excludedDirections, excludedTypes],
|
|
685
|
+
)
|
|
686
|
+
|
|
687
|
+
const sentCount = useMemo(
|
|
688
|
+
() => log.filter(entry => entry.direction === "sent").length,
|
|
689
|
+
[log],
|
|
690
|
+
)
|
|
691
|
+
const receivedCount = log.length - sentCount
|
|
692
|
+
const hasFilter = excludedDirections.size > 0 || excludedTypes.size > 0
|
|
693
|
+
|
|
694
|
+
const toggleExpanded = useCallback((id: number) => {
|
|
695
|
+
setExpanded(prev => {
|
|
696
|
+
const next = new Set(prev)
|
|
697
|
+
if (next.has(id)) {
|
|
698
|
+
next.delete(id)
|
|
699
|
+
} else {
|
|
700
|
+
next.add(id)
|
|
701
|
+
}
|
|
702
|
+
return next
|
|
703
|
+
})
|
|
704
|
+
}, [])
|
|
705
|
+
|
|
706
|
+
const clearFilters = useCallback(() => {
|
|
707
|
+
setExcludedDirections(new Set())
|
|
708
|
+
setExcludedTypes(new Set())
|
|
709
|
+
}, [])
|
|
710
|
+
|
|
711
|
+
return (
|
|
712
|
+
<CollapsibleCard
|
|
713
|
+
label="Events"
|
|
714
|
+
headerExtra={
|
|
715
|
+
<>
|
|
716
|
+
<span className={metaTextClass}>
|
|
717
|
+
<span className="text-(--sent-color)">
|
|
718
|
+
{"\u2191"} {sentCount}
|
|
719
|
+
</span>
|
|
720
|
+
<span className="mx-1 opacity-40">·</span>
|
|
721
|
+
<span className="text-(--received-color)">
|
|
722
|
+
{"\u2193"} {receivedCount}
|
|
723
|
+
</span>
|
|
724
|
+
{hasFilter && log.length !== filtered.length && (
|
|
725
|
+
<span className="ml-2 opacity-60">({filtered.length} shown)</span>
|
|
726
|
+
)}
|
|
727
|
+
</span>
|
|
728
|
+
{isPaused && (
|
|
729
|
+
<span className="rounded-full bg-(--chip-bg) px-1.5 py-0.5 font-mono text-[9px] font-semibold uppercase tracking-[0.04em] text-(--status-waiting)">
|
|
730
|
+
Paused
|
|
731
|
+
</span>
|
|
732
|
+
)}
|
|
733
|
+
</>
|
|
734
|
+
}
|
|
735
|
+
headerActions={
|
|
736
|
+
<>
|
|
737
|
+
<FilterMenu
|
|
738
|
+
open={filterOpen}
|
|
739
|
+
onOpenChange={setFilterOpen}
|
|
740
|
+
availableTypes={availableTypes}
|
|
741
|
+
excludedDirections={excludedDirections}
|
|
742
|
+
excludedTypes={excludedTypes}
|
|
743
|
+
onExcludedDirectionsChange={setExcludedDirections}
|
|
744
|
+
onExcludedTypesChange={setExcludedTypes}
|
|
745
|
+
onClearFilters={clearFilters}
|
|
746
|
+
active={hasFilter}
|
|
747
|
+
/>
|
|
748
|
+
<IconButton
|
|
749
|
+
label={isPaused ? "Resume logging" : "Pause logging"}
|
|
750
|
+
onClick={() => onPauseChange(!isPaused)}
|
|
751
|
+
>
|
|
752
|
+
{isPaused ? <PlayIcon /> : <PauseIcon />}
|
|
753
|
+
</IconButton>
|
|
754
|
+
<IconButton label="Clear events" onClick={onClear}>
|
|
755
|
+
<TrashIcon />
|
|
756
|
+
</IconButton>
|
|
757
|
+
</>
|
|
758
|
+
}
|
|
759
|
+
>
|
|
760
|
+
<div className="max-h-[400px] overflow-auto rounded-lg border border-(--border) bg-(--app-bg) p-1 font-mono">
|
|
761
|
+
{filtered.length === 0 && (
|
|
762
|
+
<div className="px-2 py-1.5 text-[11px] italic text-(--muted)">
|
|
763
|
+
{log.length === 0
|
|
764
|
+
? "No events yet"
|
|
765
|
+
: "No events match the current filters"}
|
|
766
|
+
</div>
|
|
767
|
+
)}
|
|
768
|
+
{[...filtered].reverse().map(entry => {
|
|
769
|
+
const isOpen = expanded.has(entry.id)
|
|
770
|
+
return (
|
|
771
|
+
<div key={entry.id} className="group">
|
|
772
|
+
<div
|
|
773
|
+
onClick={() => toggleExpanded(entry.id)}
|
|
774
|
+
className="flex items-center gap-2 rounded-md px-1.5 py-1 hover:bg-(--hover-bg)"
|
|
775
|
+
>
|
|
776
|
+
<ChevronIcon isOpen={isOpen} />
|
|
777
|
+
<span
|
|
778
|
+
className={`shrink-0 text-[13px] font-bold leading-4 ${
|
|
779
|
+
entry.direction === "sent"
|
|
780
|
+
? "text-(--sent-color)"
|
|
781
|
+
: "text-(--received-color)"
|
|
782
|
+
}`}
|
|
783
|
+
>
|
|
784
|
+
{entry.direction === "sent" ? "\u2191" : "\u2193"}
|
|
785
|
+
</span>
|
|
786
|
+
<span className="shrink-0 text-[11px] opacity-35">
|
|
787
|
+
{entry.time}
|
|
788
|
+
</span>
|
|
789
|
+
<span className="shrink-0 rounded-full bg-(--chip-bg) px-1.5 py-0.5 text-[10px] text-(--muted)">
|
|
790
|
+
{entry.type}
|
|
791
|
+
</span>
|
|
792
|
+
<span className="min-w-0 flex-1 truncate text-[11px] text-(--muted) opacity-80">
|
|
793
|
+
{formatInlinePreview(entry.data)}
|
|
794
|
+
</span>
|
|
795
|
+
<div
|
|
796
|
+
className="shrink-0 opacity-0 transition-opacity duration-150 group-hover:opacity-100 focus-within:opacity-100"
|
|
797
|
+
onClick={e => e.stopPropagation()}
|
|
798
|
+
>
|
|
799
|
+
<CopyValueButton
|
|
800
|
+
label={`Copy ${entry.type} event`}
|
|
801
|
+
onClick={() => onCopy(serializeForCopy(entry.data))}
|
|
802
|
+
/>
|
|
803
|
+
</div>
|
|
804
|
+
</div>
|
|
805
|
+
{isOpen && (
|
|
806
|
+
<div className="mb-1 ml-[9px] border-l border-(--border-subtle) pl-1">
|
|
807
|
+
<JsonPanel
|
|
808
|
+
value={entry.data}
|
|
809
|
+
onCopy={onCopy}
|
|
810
|
+
defaultExpandedDepth={3}
|
|
811
|
+
embedded
|
|
812
|
+
/>
|
|
813
|
+
</div>
|
|
814
|
+
)}
|
|
815
|
+
</div>
|
|
816
|
+
)
|
|
817
|
+
})}
|
|
818
|
+
</div>
|
|
819
|
+
</CollapsibleCard>
|
|
820
|
+
)
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function FilterMenu({
|
|
824
|
+
open,
|
|
825
|
+
onOpenChange,
|
|
826
|
+
availableTypes,
|
|
827
|
+
excludedDirections,
|
|
828
|
+
excludedTypes,
|
|
829
|
+
onExcludedDirectionsChange,
|
|
830
|
+
onExcludedTypesChange,
|
|
831
|
+
onClearFilters,
|
|
832
|
+
active,
|
|
833
|
+
}: {
|
|
834
|
+
open: boolean
|
|
835
|
+
onOpenChange: (open: boolean) => void
|
|
836
|
+
availableTypes: string[]
|
|
837
|
+
excludedDirections: Set<"sent" | "received">
|
|
838
|
+
excludedTypes: Set<string>
|
|
839
|
+
onExcludedDirectionsChange: (value: Set<"sent" | "received">) => void
|
|
840
|
+
onExcludedTypesChange: (value: Set<string>) => void
|
|
841
|
+
onClearFilters: () => void
|
|
842
|
+
active: boolean
|
|
843
|
+
}) {
|
|
844
|
+
const ref = useRef<HTMLDivElement>(null)
|
|
845
|
+
useOutsideClick(ref, open, () => onOpenChange(false))
|
|
846
|
+
|
|
847
|
+
const directions: { id: "sent" | "received"; label: string }[] = [
|
|
848
|
+
{ id: "sent", label: "Sent" },
|
|
849
|
+
{ id: "received", label: "Received" },
|
|
850
|
+
]
|
|
851
|
+
|
|
852
|
+
return (
|
|
853
|
+
<div ref={ref} className="relative">
|
|
854
|
+
<IconButton label="Filter events" onClick={() => onOpenChange(!open)}>
|
|
855
|
+
<FilterIcon active={active} />
|
|
856
|
+
</IconButton>
|
|
857
|
+
{open && (
|
|
858
|
+
<div className="absolute right-0 top-full z-10 mt-1 w-56 rounded-lg border border-(--border) bg-(--card-bg) p-2 shadow-(--toast-shadow)">
|
|
859
|
+
<div className="mb-1 px-1 text-[10px] font-semibold uppercase tracking-[0.05em] opacity-40">
|
|
860
|
+
Direction
|
|
861
|
+
</div>
|
|
862
|
+
{directions.map(direction => (
|
|
863
|
+
<FilterCheckbox
|
|
864
|
+
key={direction.id}
|
|
865
|
+
label={direction.label}
|
|
866
|
+
checked={!excludedDirections.has(direction.id)}
|
|
867
|
+
onChange={checked => {
|
|
868
|
+
const next = new Set(excludedDirections)
|
|
869
|
+
if (checked) {
|
|
870
|
+
next.delete(direction.id)
|
|
871
|
+
} else {
|
|
872
|
+
next.add(direction.id)
|
|
873
|
+
}
|
|
874
|
+
onExcludedDirectionsChange(next)
|
|
875
|
+
}}
|
|
876
|
+
/>
|
|
877
|
+
))}
|
|
878
|
+
<div className="mt-2 mb-1 px-1 text-[10px] font-semibold uppercase tracking-[0.05em] opacity-40">
|
|
879
|
+
Types
|
|
880
|
+
</div>
|
|
881
|
+
{availableTypes.length === 0 ? (
|
|
882
|
+
<div className="px-1 py-1 font-mono text-[11px] italic text-(--muted)">
|
|
883
|
+
No types yet
|
|
884
|
+
</div>
|
|
885
|
+
) : (
|
|
886
|
+
availableTypes.map(type => (
|
|
887
|
+
<FilterCheckbox
|
|
888
|
+
key={type}
|
|
889
|
+
label={type}
|
|
890
|
+
checked={!excludedTypes.has(type)}
|
|
891
|
+
onChange={checked => {
|
|
892
|
+
const next = new Set(excludedTypes)
|
|
893
|
+
if (checked) {
|
|
894
|
+
next.delete(type)
|
|
895
|
+
} else {
|
|
896
|
+
next.add(type)
|
|
897
|
+
}
|
|
898
|
+
onExcludedTypesChange(next)
|
|
899
|
+
}}
|
|
900
|
+
/>
|
|
901
|
+
))
|
|
902
|
+
)}
|
|
903
|
+
{active && (
|
|
904
|
+
<button
|
|
905
|
+
type="button"
|
|
906
|
+
onClick={onClearFilters}
|
|
907
|
+
className="mt-1 w-full rounded-md px-2 py-1 text-left font-mono text-[11px] text-(--muted) transition-colors hover:bg-(--hover-bg) hover:text-(--foreground)"
|
|
908
|
+
>
|
|
909
|
+
Clear filters
|
|
910
|
+
</button>
|
|
911
|
+
)}
|
|
912
|
+
</div>
|
|
913
|
+
)}
|
|
914
|
+
</div>
|
|
915
|
+
)
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
function FilterCheckbox({
|
|
919
|
+
label,
|
|
920
|
+
checked,
|
|
921
|
+
onChange,
|
|
922
|
+
}: {
|
|
923
|
+
label: string
|
|
924
|
+
checked: boolean
|
|
925
|
+
onChange: (checked: boolean) => void
|
|
926
|
+
}) {
|
|
927
|
+
return (
|
|
928
|
+
<label className="flex cursor-pointer items-center gap-2 rounded-md px-1 py-1 font-mono text-[12px] hover:bg-(--hover-bg)">
|
|
929
|
+
<input
|
|
930
|
+
type="checkbox"
|
|
931
|
+
checked={checked}
|
|
932
|
+
onChange={event => onChange(event.target.checked)}
|
|
933
|
+
className="h-3.5 w-3.5 cursor-pointer"
|
|
934
|
+
style={{ accentColor: "var(--button-bg)" }}
|
|
935
|
+
/>
|
|
936
|
+
<span className="min-w-0 flex-1 truncate">{label}</span>
|
|
937
|
+
</label>
|
|
938
|
+
)
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function IconButton({
|
|
942
|
+
label,
|
|
943
|
+
onClick,
|
|
944
|
+
children,
|
|
945
|
+
}: {
|
|
946
|
+
label: string
|
|
947
|
+
onClick: () => void
|
|
948
|
+
children: React.ReactNode
|
|
949
|
+
}) {
|
|
950
|
+
return (
|
|
951
|
+
<button
|
|
952
|
+
type="button"
|
|
953
|
+
aria-label={label}
|
|
954
|
+
title={label}
|
|
955
|
+
onClick={onClick}
|
|
956
|
+
className="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-(--foreground) opacity-55 outline-none transition-[opacity,background-color] duration-150 ease-out hover:bg-(--hover-bg) hover:opacity-100 focus-visible:opacity-100 focus-visible:ring-1 focus-visible:ring-(--button-bg)"
|
|
957
|
+
>
|
|
958
|
+
{children}
|
|
959
|
+
</button>
|
|
960
|
+
)
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function CopyValueButton({
|
|
964
|
+
label,
|
|
965
|
+
onClick,
|
|
966
|
+
}: {
|
|
967
|
+
label: string
|
|
968
|
+
onClick: () => void
|
|
969
|
+
}) {
|
|
970
|
+
return (
|
|
971
|
+
<IconButton label={label} onClick={onClick}>
|
|
972
|
+
<CopyIcon />
|
|
973
|
+
</IconButton>
|
|
974
|
+
)
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function ChevronIcon({ isOpen }: { isOpen: boolean }) {
|
|
978
|
+
return (
|
|
979
|
+
<svg
|
|
980
|
+
aria-hidden="true"
|
|
981
|
+
viewBox="0 0 16 16"
|
|
982
|
+
className={`h-3.5 w-3.5 shrink-0 text-(--muted) transition-transform duration-150 ease-out ${
|
|
983
|
+
isOpen ? "rotate-90" : ""
|
|
984
|
+
}`}
|
|
985
|
+
fill="none"
|
|
986
|
+
stroke="currentColor"
|
|
987
|
+
strokeWidth="1.75"
|
|
988
|
+
strokeLinecap="round"
|
|
989
|
+
strokeLinejoin="round"
|
|
990
|
+
>
|
|
991
|
+
<path d="M6 4l4 4-4 4" />
|
|
992
|
+
</svg>
|
|
993
|
+
)
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
function CopyIcon() {
|
|
997
|
+
return (
|
|
998
|
+
<svg
|
|
999
|
+
aria-hidden="true"
|
|
1000
|
+
viewBox="0 0 16 16"
|
|
1001
|
+
className="h-3.5 w-3.5"
|
|
1002
|
+
fill="none"
|
|
1003
|
+
stroke="currentColor"
|
|
1004
|
+
strokeWidth="1.5"
|
|
1005
|
+
strokeLinecap="round"
|
|
1006
|
+
strokeLinejoin="round"
|
|
1007
|
+
>
|
|
1008
|
+
<rect x="6" y="6" width="8" height="8" rx="1.5" />
|
|
1009
|
+
<path d="M3 11V3.5C3 3.22 3.22 3 3.5 3H10" />
|
|
1010
|
+
</svg>
|
|
1011
|
+
)
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function PauseIcon() {
|
|
1015
|
+
return (
|
|
1016
|
+
<svg
|
|
1017
|
+
aria-hidden="true"
|
|
1018
|
+
viewBox="0 0 16 16"
|
|
1019
|
+
className="h-3.5 w-3.5"
|
|
1020
|
+
fill="currentColor"
|
|
1021
|
+
>
|
|
1022
|
+
<rect x="4.5" y="3.5" width="2.5" height="9" rx="0.75" />
|
|
1023
|
+
<rect x="9" y="3.5" width="2.5" height="9" rx="0.75" />
|
|
1024
|
+
</svg>
|
|
1025
|
+
)
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
function PlayIcon() {
|
|
1029
|
+
return (
|
|
1030
|
+
<svg
|
|
1031
|
+
aria-hidden="true"
|
|
1032
|
+
viewBox="0 0 16 16"
|
|
1033
|
+
className="h-3.5 w-3.5"
|
|
1034
|
+
fill="currentColor"
|
|
1035
|
+
>
|
|
1036
|
+
<path d="M5 3.5v9l7-4.5-7-4.5z" />
|
|
1037
|
+
</svg>
|
|
1038
|
+
)
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function TrashIcon() {
|
|
1042
|
+
return (
|
|
1043
|
+
<svg
|
|
1044
|
+
aria-hidden="true"
|
|
1045
|
+
viewBox="0 0 16 16"
|
|
1046
|
+
className="h-3.5 w-3.5"
|
|
1047
|
+
fill="none"
|
|
1048
|
+
stroke="currentColor"
|
|
1049
|
+
strokeWidth="1.5"
|
|
1050
|
+
strokeLinecap="round"
|
|
1051
|
+
strokeLinejoin="round"
|
|
1052
|
+
>
|
|
1053
|
+
<path d="M3 4.5h10" />
|
|
1054
|
+
<path d="M6.5 4.5V3.25a.75.75 0 0 1 .75-.75h1.5a.75.75 0 0 1 .75.75V4.5" />
|
|
1055
|
+
<path d="M5 4.5v8a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1v-8" />
|
|
1056
|
+
<path d="M7 7v4M9 7v4" />
|
|
1057
|
+
</svg>
|
|
1058
|
+
)
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
function FilterIcon({ active }: { active: boolean }) {
|
|
1062
|
+
return (
|
|
1063
|
+
<svg
|
|
1064
|
+
aria-hidden="true"
|
|
1065
|
+
viewBox="0 0 16 16"
|
|
1066
|
+
className="h-3.5 w-3.5"
|
|
1067
|
+
fill="none"
|
|
1068
|
+
stroke="currentColor"
|
|
1069
|
+
strokeWidth="1.5"
|
|
1070
|
+
strokeLinecap="round"
|
|
1071
|
+
strokeLinejoin="round"
|
|
1072
|
+
>
|
|
1073
|
+
<path d="M2.5 3.5h11l-4 5.5v3.5l-3 1V9L2.5 3.5z" />
|
|
1074
|
+
{active && (
|
|
1075
|
+
<circle
|
|
1076
|
+
cx="13"
|
|
1077
|
+
cy="3"
|
|
1078
|
+
r="2"
|
|
1079
|
+
fill="var(--status-waiting)"
|
|
1080
|
+
stroke="var(--card-bg)"
|
|
1081
|
+
strokeWidth="1"
|
|
1082
|
+
/>
|
|
1083
|
+
)}
|
|
1084
|
+
</svg>
|
|
1085
|
+
)
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
function HistoryIcon() {
|
|
1089
|
+
return (
|
|
1090
|
+
<svg
|
|
1091
|
+
aria-hidden="true"
|
|
1092
|
+
viewBox="0 0 16 16"
|
|
1093
|
+
className="h-3.5 w-3.5"
|
|
1094
|
+
fill="none"
|
|
1095
|
+
stroke="currentColor"
|
|
1096
|
+
strokeWidth="1.5"
|
|
1097
|
+
strokeLinecap="round"
|
|
1098
|
+
strokeLinejoin="round"
|
|
1099
|
+
>
|
|
1100
|
+
<path d="M2.5 8a5.5 5.5 0 1 0 1.7-3.97" />
|
|
1101
|
+
<path d="M2.5 3v3h3" />
|
|
1102
|
+
<path d="M8 5v3.5l2 1.25" />
|
|
1103
|
+
</svg>
|
|
1104
|
+
)
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
/** Single-line JSON with spaces after colons and commas, but strings preserved intact. */
|
|
1108
|
+
function prettyJson(value: unknown): string {
|
|
1109
|
+
try {
|
|
1110
|
+
return JSON.stringify(value, null, 2).replace(/\n\s*/g, " ")
|
|
1111
|
+
} catch {
|
|
1112
|
+
return String(value)
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
function serializeForCopy(value: unknown) {
|
|
1117
|
+
try {
|
|
1118
|
+
return JSON.stringify(value, null, 2) ?? String(value)
|
|
1119
|
+
} catch {
|
|
1120
|
+
return String(value)
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
function isExpandableJsonValue(
|
|
1125
|
+
value: unknown,
|
|
1126
|
+
): value is Record<string, unknown> | unknown[] {
|
|
1127
|
+
return value !== null && typeof value === "object"
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
function getJsonTypeLabel(value: unknown) {
|
|
1131
|
+
if (Array.isArray(value)) {
|
|
1132
|
+
return `array ${value.length}`
|
|
1133
|
+
}
|
|
1134
|
+
if (value === null) {
|
|
1135
|
+
return "null"
|
|
1136
|
+
}
|
|
1137
|
+
if (typeof value === "object") {
|
|
1138
|
+
return `object ${Object.keys(value).length}`
|
|
1139
|
+
}
|
|
1140
|
+
return typeof value
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
function formatPrimitiveValue(value: unknown) {
|
|
1144
|
+
if (typeof value === "string") {
|
|
1145
|
+
return JSON.stringify(value)
|
|
1146
|
+
}
|
|
1147
|
+
if (value === undefined) {
|
|
1148
|
+
return "undefined"
|
|
1149
|
+
}
|
|
1150
|
+
return String(value)
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
function formatInlinePreview(value: unknown) {
|
|
1154
|
+
if (Array.isArray(value)) {
|
|
1155
|
+
if (value.length === 0) {
|
|
1156
|
+
return "[]"
|
|
1157
|
+
}
|
|
1158
|
+
const preview = value
|
|
1159
|
+
.slice(0, 3)
|
|
1160
|
+
.map(item => summarizePreviewToken(item))
|
|
1161
|
+
.join(", ")
|
|
1162
|
+
return value.length > 3 ? `${preview}, +${value.length - 3} more` : preview
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
if (value !== null && typeof value === "object") {
|
|
1166
|
+
const keys = Object.keys(value)
|
|
1167
|
+
if (keys.length === 0) {
|
|
1168
|
+
return "{}"
|
|
1169
|
+
}
|
|
1170
|
+
const preview = keys.slice(0, 4).join(", ")
|
|
1171
|
+
return keys.length > 4 ? `${preview}, +${keys.length - 4} more` : preview
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
return formatPrimitiveValue(value)
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
function summarizePreviewToken(value: unknown) {
|
|
1178
|
+
if (Array.isArray(value)) {
|
|
1179
|
+
return `[${value.length}]`
|
|
1180
|
+
}
|
|
1181
|
+
if (value !== null && typeof value === "object") {
|
|
1182
|
+
return `{${Object.keys(value).length}}`
|
|
1183
|
+
}
|
|
1184
|
+
if (typeof value === "string") {
|
|
1185
|
+
return value.length > 18 ? `${value.slice(0, 18)}\u2026` : value
|
|
1186
|
+
}
|
|
1187
|
+
return String(value)
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
function formatElapsed(seconds: number) {
|
|
1191
|
+
if (seconds < 60) {
|
|
1192
|
+
return `${seconds}s ago`
|
|
1193
|
+
}
|
|
1194
|
+
if (seconds < 3600) {
|
|
1195
|
+
const minutes = Math.floor(seconds / 60)
|
|
1196
|
+
return `${minutes}m ago`
|
|
1197
|
+
}
|
|
1198
|
+
const hours = Math.floor(seconds / 3600)
|
|
1199
|
+
return `${hours}h ago`
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
function getBridgeProtocolVersion(initData: unknown): string | null {
|
|
1203
|
+
if (!initData || typeof initData !== "object") {
|
|
1204
|
+
return null
|
|
1205
|
+
}
|
|
1206
|
+
const v = (initData as { bridgeProtocolVersion?: unknown })
|
|
1207
|
+
.bridgeProtocolVersion
|
|
1208
|
+
if (typeof v === "number") {
|
|
1209
|
+
return String(v)
|
|
1210
|
+
}
|
|
1211
|
+
if (typeof v === "string") {
|
|
1212
|
+
return v
|
|
1213
|
+
}
|
|
1214
|
+
return null
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
function formatTime(date: Date) {
|
|
1218
|
+
return date.toISOString().slice(11, 23)
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
function getContentHeight() {
|
|
1222
|
+
const root = document.getElementById("root")
|
|
1223
|
+
return Math.ceil(root?.scrollHeight ?? document.body.scrollHeight)
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
const PRESETS: {
|
|
1227
|
+
label: string
|
|
1228
|
+
createMessage: () => Record<string, unknown>
|
|
1229
|
+
}[] = [
|
|
1230
|
+
{
|
|
1231
|
+
label: "ready",
|
|
1232
|
+
createMessage: () => ({
|
|
1233
|
+
type: "ready",
|
|
1234
|
+
bridgeProtocolVersion: 1,
|
|
1235
|
+
manifest: null,
|
|
1236
|
+
}),
|
|
1237
|
+
},
|
|
1238
|
+
{
|
|
1239
|
+
label: "resize 900",
|
|
1240
|
+
createMessage: () => ({ type: "resize", height: 900 }),
|
|
1241
|
+
},
|
|
1242
|
+
{
|
|
1243
|
+
label: "resize: contentHeight",
|
|
1244
|
+
createMessage: () => ({ type: "resize", height: getContentHeight() }),
|
|
1245
|
+
},
|
|
1246
|
+
{ label: "ping", createMessage: () => ({ type: "ping" }) },
|
|
1247
|
+
{
|
|
1248
|
+
label: "queryDataSource",
|
|
1249
|
+
createMessage: () => ({
|
|
1250
|
+
type: "queryDataSource",
|
|
1251
|
+
requestId: "debug-query",
|
|
1252
|
+
key: "default",
|
|
1253
|
+
limit: 20,
|
|
1254
|
+
}),
|
|
1255
|
+
},
|
|
1256
|
+
]
|
|
1257
|
+
|
|
1258
|
+
type SendMessageCardProps = {
|
|
1259
|
+
onSend: (message: Record<string, unknown>) => void
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
function SendMessageCard({ onSend }: SendMessageCardProps) {
|
|
1263
|
+
const [input, setInput] = useState('{ "type": "ready" }')
|
|
1264
|
+
const [history, setHistory] = useState<string[]>([])
|
|
1265
|
+
const [historyOpen, setHistoryOpen] = useState(false)
|
|
1266
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
|
1267
|
+
const historyRef = useRef<HTMLDivElement>(null)
|
|
1268
|
+
|
|
1269
|
+
const parsed = useMemo<
|
|
1270
|
+
| { ok: true; value: Record<string, unknown> }
|
|
1271
|
+
| { ok: false; error: string | null }
|
|
1272
|
+
>(() => {
|
|
1273
|
+
const trimmed = input.trim()
|
|
1274
|
+
if (!trimmed) {
|
|
1275
|
+
return { ok: false, error: null }
|
|
1276
|
+
}
|
|
1277
|
+
try {
|
|
1278
|
+
const value = JSON.parse(trimmed)
|
|
1279
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1280
|
+
return { ok: false, error: "Must be a JSON object" }
|
|
1281
|
+
}
|
|
1282
|
+
return { ok: true, value: value as Record<string, unknown> }
|
|
1283
|
+
} catch {
|
|
1284
|
+
return { ok: false, error: "Invalid JSON" }
|
|
1285
|
+
}
|
|
1286
|
+
}, [input])
|
|
1287
|
+
|
|
1288
|
+
useEffect(() => {
|
|
1289
|
+
const textarea = textareaRef.current
|
|
1290
|
+
if (!textarea) {
|
|
1291
|
+
return
|
|
1292
|
+
}
|
|
1293
|
+
textarea.style.height = "auto"
|
|
1294
|
+
textarea.style.height = `${Math.min(Math.max(textarea.scrollHeight, 32), 200)}px`
|
|
1295
|
+
}, [input])
|
|
1296
|
+
|
|
1297
|
+
useOutsideClick(historyRef, historyOpen, () => setHistoryOpen(false))
|
|
1298
|
+
|
|
1299
|
+
const handleSend = () => {
|
|
1300
|
+
if (!parsed.ok) {
|
|
1301
|
+
return
|
|
1302
|
+
}
|
|
1303
|
+
onSend(parsed.value)
|
|
1304
|
+
setHistory(prev => {
|
|
1305
|
+
if (prev[0] === input) {
|
|
1306
|
+
return prev
|
|
1307
|
+
}
|
|
1308
|
+
return [input, ...prev.filter(entry => entry !== input)].slice(0, 10)
|
|
1309
|
+
})
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
return (
|
|
1313
|
+
<CollapsibleCard label="Send message">
|
|
1314
|
+
<div className="mb-2 flex flex-wrap gap-1.5">
|
|
1315
|
+
{PRESETS.map(preset => (
|
|
1316
|
+
<button
|
|
1317
|
+
key={preset.label}
|
|
1318
|
+
type="button"
|
|
1319
|
+
className="rounded-md border border-(--border) bg-transparent px-2 py-1 font-mono text-[11px] text-(--muted) transition-colors hover:border-(--foreground)/20 hover:bg-(--hover-bg) hover:text-(--foreground)"
|
|
1320
|
+
onClick={() => {
|
|
1321
|
+
const message = preset.createMessage()
|
|
1322
|
+
setInput(prettyJson(message))
|
|
1323
|
+
onSend(message)
|
|
1324
|
+
}}
|
|
1325
|
+
>
|
|
1326
|
+
{preset.label}
|
|
1327
|
+
</button>
|
|
1328
|
+
))}
|
|
1329
|
+
</div>
|
|
1330
|
+
<textarea
|
|
1331
|
+
ref={textareaRef}
|
|
1332
|
+
value={input}
|
|
1333
|
+
onChange={event => setInput(event.target.value)}
|
|
1334
|
+
onKeyDown={event => {
|
|
1335
|
+
if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) {
|
|
1336
|
+
event.preventDefault()
|
|
1337
|
+
handleSend()
|
|
1338
|
+
}
|
|
1339
|
+
}}
|
|
1340
|
+
rows={1}
|
|
1341
|
+
spellCheck={false}
|
|
1342
|
+
style={{ scrollbarWidth: "none" }}
|
|
1343
|
+
className={`block w-full resize-none rounded-md border bg-(--app-bg) px-2.5 py-1.5 font-mono text-[11px] leading-5 outline-none transition-colors [&::-webkit-scrollbar]:hidden focus:border-(--foreground) ${
|
|
1344
|
+
parsed.ok || parsed.error === null
|
|
1345
|
+
? "border-(--border)"
|
|
1346
|
+
: "border-(--error-color)"
|
|
1347
|
+
}`}
|
|
1348
|
+
placeholder='{ "type": "..." }'
|
|
1349
|
+
/>
|
|
1350
|
+
<div className="mt-2 flex items-center justify-between gap-2">
|
|
1351
|
+
<div className="flex min-w-0 items-center gap-2 font-mono text-[10px]">
|
|
1352
|
+
<span className="shrink-0 text-(--muted) opacity-70">
|
|
1353
|
+
{"\u2318\u23ce to send"}
|
|
1354
|
+
</span>
|
|
1355
|
+
{parsed.ok === false && parsed.error !== null && (
|
|
1356
|
+
<span className="min-w-0 truncate text-(--error-color)">
|
|
1357
|
+
{parsed.error}
|
|
1358
|
+
</span>
|
|
1359
|
+
)}
|
|
1360
|
+
</div>
|
|
1361
|
+
<div className="flex shrink-0 items-center gap-1">
|
|
1362
|
+
<div ref={historyRef} className="relative">
|
|
1363
|
+
<IconButton
|
|
1364
|
+
label="Recent sent messages"
|
|
1365
|
+
onClick={() => setHistoryOpen(open => !open)}
|
|
1366
|
+
>
|
|
1367
|
+
<HistoryIcon />
|
|
1368
|
+
</IconButton>
|
|
1369
|
+
{historyOpen && (
|
|
1370
|
+
<div className="absolute right-0 top-full z-10 mt-1 w-72 rounded-lg border border-(--border) bg-(--card-bg) p-1 shadow-(--toast-shadow)">
|
|
1371
|
+
{history.length === 0 ? (
|
|
1372
|
+
<div className="px-2 py-1.5 font-mono text-[11px] italic text-(--muted)">
|
|
1373
|
+
No sent messages yet
|
|
1374
|
+
</div>
|
|
1375
|
+
) : (
|
|
1376
|
+
history.map((item, index) => (
|
|
1377
|
+
<button
|
|
1378
|
+
key={`${index}-${item}`}
|
|
1379
|
+
type="button"
|
|
1380
|
+
onClick={() => {
|
|
1381
|
+
setInput(item)
|
|
1382
|
+
setHistoryOpen(false)
|
|
1383
|
+
textareaRef.current?.focus()
|
|
1384
|
+
}}
|
|
1385
|
+
className="block w-full truncate rounded-md px-2 py-1 text-left font-mono text-[11px] text-(--foreground) hover:bg-(--hover-bg)"
|
|
1386
|
+
>
|
|
1387
|
+
{item}
|
|
1388
|
+
</button>
|
|
1389
|
+
))
|
|
1390
|
+
)}
|
|
1391
|
+
</div>
|
|
1392
|
+
)}
|
|
1393
|
+
</div>
|
|
1394
|
+
<button
|
|
1395
|
+
type="button"
|
|
1396
|
+
disabled={!parsed.ok}
|
|
1397
|
+
onClick={handleSend}
|
|
1398
|
+
className="rounded-md border-0 bg-(--button-bg) px-3 py-1.5 font-mono text-[11px] font-semibold text-(--button-fg) outline-none transition-opacity hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-40"
|
|
1399
|
+
>
|
|
1400
|
+
Send
|
|
1401
|
+
</button>
|
|
1402
|
+
</div>
|
|
1403
|
+
</div>
|
|
1404
|
+
</CollapsibleCard>
|
|
1405
|
+
)
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
type ErrorBoundaryProps = {
|
|
1409
|
+
children: React.ReactNode
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
type ErrorBoundaryState = {
|
|
1413
|
+
error: Error | null
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|
1417
|
+
state: ErrorBoundaryState = { error: null }
|
|
1418
|
+
|
|
1419
|
+
static getDerivedStateFromError(error: Error) {
|
|
1420
|
+
return { error }
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
render() {
|
|
1424
|
+
if (this.state.error) {
|
|
1425
|
+
return (
|
|
1426
|
+
<div className="px-10 py-8 text-(--error-color)">
|
|
1427
|
+
<div className="mb-3 text-[28px] font-bold tracking-[-0.02em]">
|
|
1428
|
+
Error
|
|
1429
|
+
</div>
|
|
1430
|
+
<div className="break-all whitespace-pre-wrap rounded-lg border border-(--error-border) bg-(--error-surface) px-5 py-3 font-mono text-xs">
|
|
1431
|
+
{this.state.error.message}
|
|
1432
|
+
{this.state.error.stack && (
|
|
1433
|
+
<div className="mt-2 opacity-60">{this.state.error.stack}</div>
|
|
1434
|
+
)}
|
|
1435
|
+
</div>
|
|
1436
|
+
</div>
|
|
1437
|
+
)
|
|
1438
|
+
}
|
|
1439
|
+
return this.props.children
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
type CreatePageCardProps = {
|
|
1444
|
+
context: NotionCustomBlockContext | undefined
|
|
1445
|
+
dataSources: NotionDataSource[]
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
type CreatePagePreset = {
|
|
1449
|
+
label: string
|
|
1450
|
+
description: string
|
|
1451
|
+
position?: NotionCreatePagePosition
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
function presetPositions(
|
|
1455
|
+
customBlockId: string | undefined,
|
|
1456
|
+
): CreatePagePreset[] {
|
|
1457
|
+
const presets: CreatePagePreset[] = [
|
|
1458
|
+
{
|
|
1459
|
+
label: "Top of page",
|
|
1460
|
+
description: "Append to the top of the page",
|
|
1461
|
+
position: { type: "start" },
|
|
1462
|
+
},
|
|
1463
|
+
{
|
|
1464
|
+
label: "Bottom of page",
|
|
1465
|
+
description: "Append to the bottom of the page",
|
|
1466
|
+
position: { type: "end" },
|
|
1467
|
+
},
|
|
1468
|
+
]
|
|
1469
|
+
if (customBlockId !== undefined) {
|
|
1470
|
+
presets.push(
|
|
1471
|
+
{
|
|
1472
|
+
label: "Above this block",
|
|
1473
|
+
description: "Directly above the custom block",
|
|
1474
|
+
position: { type: "before", blockId: customBlockId },
|
|
1475
|
+
},
|
|
1476
|
+
{
|
|
1477
|
+
label: "Below this block",
|
|
1478
|
+
description: "Directly below the custom block",
|
|
1479
|
+
position: { type: "after", blockId: customBlockId },
|
|
1480
|
+
},
|
|
1481
|
+
)
|
|
1482
|
+
}
|
|
1483
|
+
return presets
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
function getPageTitle(page: NotionPage): string {
|
|
1487
|
+
const titleValue = page.properties.title
|
|
1488
|
+
if (titleValue === undefined || !("title" in titleValue)) {
|
|
1489
|
+
return ""
|
|
1490
|
+
}
|
|
1491
|
+
const richText = titleValue.title
|
|
1492
|
+
if (!Array.isArray(richText)) {
|
|
1493
|
+
return ""
|
|
1494
|
+
}
|
|
1495
|
+
return richText
|
|
1496
|
+
.map(part => {
|
|
1497
|
+
if (typeof part.plain_text === "string") {
|
|
1498
|
+
return part.plain_text
|
|
1499
|
+
}
|
|
1500
|
+
if (
|
|
1501
|
+
part.type !== "text" ||
|
|
1502
|
+
part.text === null ||
|
|
1503
|
+
typeof part.text !== "object" ||
|
|
1504
|
+
!("content" in part.text)
|
|
1505
|
+
) {
|
|
1506
|
+
return ""
|
|
1507
|
+
}
|
|
1508
|
+
return typeof part.text.content === "string" ? part.text.content : ""
|
|
1509
|
+
})
|
|
1510
|
+
.join("")
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
function getPageIconValue(page: NotionPage): string {
|
|
1514
|
+
const icon = page.icon
|
|
1515
|
+
if (!icon) {
|
|
1516
|
+
return ""
|
|
1517
|
+
}
|
|
1518
|
+
switch (icon.type) {
|
|
1519
|
+
case "emoji":
|
|
1520
|
+
return icon.emoji
|
|
1521
|
+
case "external":
|
|
1522
|
+
return icon.external.url
|
|
1523
|
+
case "file":
|
|
1524
|
+
return icon.file.url
|
|
1525
|
+
case "custom_emoji":
|
|
1526
|
+
return icon.custom_emoji.url ?? icon.custom_emoji.id
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
function trackPage(prev: TrackedPage[], page: NotionPage): TrackedPage[] {
|
|
1531
|
+
const nextEntry: TrackedPage = {
|
|
1532
|
+
page,
|
|
1533
|
+
draftTitle: getPageTitle(page),
|
|
1534
|
+
draftIcon: getPageIconValue(page),
|
|
1535
|
+
}
|
|
1536
|
+
return [nextEntry, ...prev.filter(entry => entry.page.id !== page.id)]
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
function pageIconFromInput(raw: string): NotionPage["icon"] | undefined {
|
|
1540
|
+
const value = raw.trim()
|
|
1541
|
+
if (value === "") {
|
|
1542
|
+
return undefined
|
|
1543
|
+
}
|
|
1544
|
+
if (
|
|
1545
|
+
value.startsWith("http://") ||
|
|
1546
|
+
value.startsWith("https://") ||
|
|
1547
|
+
value.startsWith("/") ||
|
|
1548
|
+
value.includes("://")
|
|
1549
|
+
) {
|
|
1550
|
+
return {
|
|
1551
|
+
type: "external",
|
|
1552
|
+
external: { url: value },
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
return {
|
|
1556
|
+
type: "emoji",
|
|
1557
|
+
emoji: value,
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
/**
|
|
1562
|
+
* Buttons that exercise `sdk.pages.create` at each of the supported positions. Every preset creates
|
|
1563
|
+
* a page whose `parent.page_id` is the nearest page ancestor. `position` demonstrates append,
|
|
1564
|
+
* prepend, and inserting directly above or below the custom block via `context.customBlockId`.
|
|
1565
|
+
*
|
|
1566
|
+
* Two extra affordances exercise the data-source-backed parents:
|
|
1567
|
+
* - A free-form input accepts a data source ID and uses `parent: { type: "data_source_id", ... }`
|
|
1568
|
+
* directly.
|
|
1569
|
+
* - A button per known data-source key uses `parent: { type: "data_source_key", key }`, letting the
|
|
1570
|
+
* SDK resolve the key to the configured data source ID locally before sending the request to the
|
|
1571
|
+
* Notion host.
|
|
1572
|
+
*/
|
|
1573
|
+
function CreatePageCard({ context, dataSources }: CreatePageCardProps) {
|
|
1574
|
+
const [status, setStatus] = useState<string | null>(null)
|
|
1575
|
+
const [busy, setBusy] = useState(false)
|
|
1576
|
+
const [dataSourceId, setDataSourceId] = useState("")
|
|
1577
|
+
const [pageId, setPageId] = useState("")
|
|
1578
|
+
const [trackedPages, setTrackedPages] = useState<TrackedPage[]>([])
|
|
1579
|
+
const presets = presetPositions(context?.customBlockId)
|
|
1580
|
+
const disabled = context === undefined || busy
|
|
1581
|
+
const trimmedDataSourceId = dataSourceId.trim()
|
|
1582
|
+
const dataSourceDisabled = disabled || trimmedDataSourceId === ""
|
|
1583
|
+
const trimmedPageId = pageId.trim()
|
|
1584
|
+
const fetchDisabled = busy || trimmedPageId === ""
|
|
1585
|
+
|
|
1586
|
+
useEffect(() => {
|
|
1587
|
+
if (context?.page.id && pageId === "") {
|
|
1588
|
+
setPageId(context.page.id)
|
|
1589
|
+
}
|
|
1590
|
+
}, [context?.page.id, pageId])
|
|
1591
|
+
|
|
1592
|
+
const handleCreate = async (preset: CreatePagePreset) => {
|
|
1593
|
+
if (context === undefined || busy) {
|
|
1594
|
+
return
|
|
1595
|
+
}
|
|
1596
|
+
setBusy(true)
|
|
1597
|
+
setStatus(`Creating (${preset.label})\u2026`)
|
|
1598
|
+
const title = `New page (${preset.label.toLowerCase()}) ${new Date().toLocaleTimeString()}`
|
|
1599
|
+
const result = await pages.create({
|
|
1600
|
+
parent: { type: "page_id", page_id: context.page.id },
|
|
1601
|
+
properties: {
|
|
1602
|
+
title: {
|
|
1603
|
+
id: "title",
|
|
1604
|
+
type: "title",
|
|
1605
|
+
title: [{ type: "text", text: { content: title } }],
|
|
1606
|
+
},
|
|
1607
|
+
},
|
|
1608
|
+
...(preset.position !== undefined ? { position: preset.position } : {}),
|
|
1609
|
+
})
|
|
1610
|
+
if (result.status === "success") {
|
|
1611
|
+
setTrackedPages(prev => trackPage(prev, result.page))
|
|
1612
|
+
setStatus(`\u2713 ${preset.label}: ${result.page.id}`)
|
|
1613
|
+
} else {
|
|
1614
|
+
setStatus(`\u2717 ${preset.label}: ${result.error}`)
|
|
1615
|
+
}
|
|
1616
|
+
setBusy(false)
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
const handleCreateInDataSource = async () => {
|
|
1620
|
+
if (context === undefined || busy || trimmedDataSourceId === "") {
|
|
1621
|
+
return
|
|
1622
|
+
}
|
|
1623
|
+
setBusy(true)
|
|
1624
|
+
setStatus("Creating (data source)\u2026")
|
|
1625
|
+
const title = `New row ${new Date().toLocaleTimeString()}`
|
|
1626
|
+
const result = await pages.create({
|
|
1627
|
+
parent: {
|
|
1628
|
+
type: "data_source_id",
|
|
1629
|
+
data_source_id: trimmedDataSourceId as NotionDataSourceId,
|
|
1630
|
+
},
|
|
1631
|
+
properties: {
|
|
1632
|
+
title: {
|
|
1633
|
+
id: "title",
|
|
1634
|
+
type: "title",
|
|
1635
|
+
title: [{ type: "text", text: { content: title } }],
|
|
1636
|
+
},
|
|
1637
|
+
},
|
|
1638
|
+
})
|
|
1639
|
+
if (result.status === "success") {
|
|
1640
|
+
setTrackedPages(prev => trackPage(prev, result.page))
|
|
1641
|
+
setStatus(`\u2713 Data source: ${result.page.id}`)
|
|
1642
|
+
} else {
|
|
1643
|
+
setStatus(`\u2717 Data source: ${result.error}`)
|
|
1644
|
+
}
|
|
1645
|
+
setBusy(false)
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
const handleCreateForKey = async (key: string) => {
|
|
1649
|
+
if (context === undefined || busy) {
|
|
1650
|
+
return
|
|
1651
|
+
}
|
|
1652
|
+
setBusy(true)
|
|
1653
|
+
setStatus(`Creating (key: ${key})\u2026`)
|
|
1654
|
+
const title = `New row (${key}) ${new Date().toLocaleTimeString()}`
|
|
1655
|
+
const result = await pages.create({
|
|
1656
|
+
parent: { type: "data_source_key", key },
|
|
1657
|
+
properties: {
|
|
1658
|
+
title: {
|
|
1659
|
+
id: "title",
|
|
1660
|
+
type: "title",
|
|
1661
|
+
title: [{ type: "text", text: { content: title } }],
|
|
1662
|
+
},
|
|
1663
|
+
},
|
|
1664
|
+
})
|
|
1665
|
+
if (result.status === "success") {
|
|
1666
|
+
setTrackedPages(prev => trackPage(prev, result.page))
|
|
1667
|
+
setStatus(`\u2713 Key "${key}": ${result.page.id}`)
|
|
1668
|
+
} else {
|
|
1669
|
+
setStatus(`\u2717 Key "${key}": ${result.error}`)
|
|
1670
|
+
}
|
|
1671
|
+
setBusy(false)
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
const handleFetchPage = async () => {
|
|
1675
|
+
if (busy || trimmedPageId === "") {
|
|
1676
|
+
return
|
|
1677
|
+
}
|
|
1678
|
+
setBusy(true)
|
|
1679
|
+
setStatus(`Fetching ${trimmedPageId}\u2026`)
|
|
1680
|
+
const result = await pages.get(trimmedPageId as NotionPageId)
|
|
1681
|
+
if (result.status === "success") {
|
|
1682
|
+
setTrackedPages(prev => trackPage(prev, result.page))
|
|
1683
|
+
setStatus(`\u2713 Loaded ${result.page.id}`)
|
|
1684
|
+
} else {
|
|
1685
|
+
setStatus(`\u2717 Fetch failed: ${result.error}`)
|
|
1686
|
+
}
|
|
1687
|
+
setBusy(false)
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
const handleRefreshPage = async (trackedPageId: string) => {
|
|
1691
|
+
if (busy) {
|
|
1692
|
+
return
|
|
1693
|
+
}
|
|
1694
|
+
setBusy(true)
|
|
1695
|
+
setStatus(`Refreshing ${trackedPageId}\u2026`)
|
|
1696
|
+
const result = await pages.get(trackedPageId as NotionPageId)
|
|
1697
|
+
if (result.status === "success") {
|
|
1698
|
+
setTrackedPages(prev => trackPage(prev, result.page))
|
|
1699
|
+
setStatus(`\u2713 Refreshed ${result.page.id}`)
|
|
1700
|
+
} else {
|
|
1701
|
+
setStatus(`\u2717 Refresh failed: ${result.error}`)
|
|
1702
|
+
}
|
|
1703
|
+
setBusy(false)
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
const handleSavePage = async (trackedPage: TrackedPage) => {
|
|
1707
|
+
if (busy) {
|
|
1708
|
+
return
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
const nextTitle = trackedPage.draftTitle
|
|
1712
|
+
const nextIcon = trackedPage.draftIcon.trim()
|
|
1713
|
+
const currentTitle = getPageTitle(trackedPage.page)
|
|
1714
|
+
const currentIcon = getPageIconValue(trackedPage.page)
|
|
1715
|
+
const titleChanged = nextTitle !== currentTitle
|
|
1716
|
+
const iconChanged = nextIcon !== currentIcon
|
|
1717
|
+
const iconPatch =
|
|
1718
|
+
iconChanged && nextIcon !== "" ? pageIconFromInput(nextIcon) : undefined
|
|
1719
|
+
|
|
1720
|
+
if (!titleChanged && !iconChanged) {
|
|
1721
|
+
setStatus(`No changes for ${trackedPage.page.id}`)
|
|
1722
|
+
return
|
|
1723
|
+
}
|
|
1724
|
+
if (iconChanged && nextIcon === "" && !titleChanged) {
|
|
1725
|
+
setStatus("Clearing icons is not supported yet")
|
|
1726
|
+
return
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
setBusy(true)
|
|
1730
|
+
setStatus(`Saving ${trackedPage.page.id}\u2026`)
|
|
1731
|
+
const result = await pages.update({
|
|
1732
|
+
pageId: trackedPage.page.id,
|
|
1733
|
+
...(titleChanged
|
|
1734
|
+
? {
|
|
1735
|
+
properties: {
|
|
1736
|
+
title: {
|
|
1737
|
+
id: "title",
|
|
1738
|
+
type: "title",
|
|
1739
|
+
title: [{ type: "text", text: { content: nextTitle } }],
|
|
1740
|
+
},
|
|
1741
|
+
},
|
|
1742
|
+
}
|
|
1743
|
+
: {}),
|
|
1744
|
+
...(iconPatch !== undefined ? { icon: iconPatch } : {}),
|
|
1745
|
+
})
|
|
1746
|
+
if (result.status === "success") {
|
|
1747
|
+
setTrackedPages(prev => trackPage(prev, result.page))
|
|
1748
|
+
setStatus(`\u2713 Saved ${result.page.id}`)
|
|
1749
|
+
} else {
|
|
1750
|
+
setStatus(`\u2717 Save failed: ${result.error}`)
|
|
1751
|
+
}
|
|
1752
|
+
setBusy(false)
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
const handleDeletePage = async (trackedPageId: string) => {
|
|
1756
|
+
if (busy) {
|
|
1757
|
+
return
|
|
1758
|
+
}
|
|
1759
|
+
setBusy(true)
|
|
1760
|
+
setStatus(`Deleting ${trackedPageId}\u2026`)
|
|
1761
|
+
const result = await pages.delete(trackedPageId as NotionPageId)
|
|
1762
|
+
if (result.status === "success") {
|
|
1763
|
+
setTrackedPages(prev => trackPage(prev, result.page))
|
|
1764
|
+
setStatus(`\u2713 Deleted ${result.page.id}`)
|
|
1765
|
+
} else {
|
|
1766
|
+
setStatus(`\u2717 Delete failed: ${result.error}`)
|
|
1767
|
+
}
|
|
1768
|
+
setBusy(false)
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
return (
|
|
1772
|
+
<CollapsibleCard label="Page API debugger">
|
|
1773
|
+
<div className="mb-2 flex flex-wrap gap-1.5">
|
|
1774
|
+
{presets.map(preset => (
|
|
1775
|
+
<button
|
|
1776
|
+
key={preset.label}
|
|
1777
|
+
type="button"
|
|
1778
|
+
disabled={disabled}
|
|
1779
|
+
title={preset.description}
|
|
1780
|
+
className="rounded-md border border-(--border) bg-transparent px-2 py-1 font-mono text-[11px] text-(--muted) transition-colors hover:border-(--foreground)/20 hover:bg-(--hover-bg) hover:text-(--foreground) disabled:cursor-not-allowed disabled:opacity-50"
|
|
1781
|
+
onClick={() => {
|
|
1782
|
+
void handleCreate(preset)
|
|
1783
|
+
}}
|
|
1784
|
+
>
|
|
1785
|
+
{preset.label}
|
|
1786
|
+
</button>
|
|
1787
|
+
))}
|
|
1788
|
+
</div>
|
|
1789
|
+
<div className="mb-2 flex flex-wrap items-center gap-1.5">
|
|
1790
|
+
<input
|
|
1791
|
+
type="text"
|
|
1792
|
+
value={dataSourceId}
|
|
1793
|
+
onChange={event => setDataSourceId(event.target.value)}
|
|
1794
|
+
placeholder="data_source_id"
|
|
1795
|
+
spellCheck={false}
|
|
1796
|
+
className="min-w-0 flex-1 rounded-md border border-(--border) bg-(--app-bg) px-2 py-1 font-mono text-[11px] outline-none transition-colors focus:border-(--foreground)"
|
|
1797
|
+
/>
|
|
1798
|
+
<button
|
|
1799
|
+
type="button"
|
|
1800
|
+
disabled={dataSourceDisabled}
|
|
1801
|
+
title="Create a page as a row of the data source with the given ID"
|
|
1802
|
+
className="rounded-md border border-(--border) bg-transparent px-2 py-1 font-mono text-[11px] text-(--muted) transition-colors hover:border-(--foreground)/20 hover:bg-(--hover-bg) hover:text-(--foreground) disabled:cursor-not-allowed disabled:opacity-50"
|
|
1803
|
+
onClick={() => {
|
|
1804
|
+
void handleCreateInDataSource()
|
|
1805
|
+
}}
|
|
1806
|
+
>
|
|
1807
|
+
Create in data source
|
|
1808
|
+
</button>
|
|
1809
|
+
</div>
|
|
1810
|
+
{dataSources.length > 0 && (
|
|
1811
|
+
<div className="mb-2 flex flex-wrap items-center gap-1.5">
|
|
1812
|
+
<span className="font-mono text-[11px] text-(--muted)">
|
|
1813
|
+
by data source key:
|
|
1814
|
+
</span>
|
|
1815
|
+
{dataSources.map(source => (
|
|
1816
|
+
<button
|
|
1817
|
+
key={source.key}
|
|
1818
|
+
type="button"
|
|
1819
|
+
disabled={disabled}
|
|
1820
|
+
title={
|
|
1821
|
+
source.collectionPointer !== undefined
|
|
1822
|
+
? `Create a page in data source ${source.collectionPointer.id} via key "${source.key}"`
|
|
1823
|
+
: `Key "${source.key}" has no collection pointer yet; request will resolve with an error`
|
|
1824
|
+
}
|
|
1825
|
+
className="rounded-md border border-(--border) bg-transparent px-2 py-1 font-mono text-[11px] text-(--muted) transition-colors hover:border-(--foreground)/20 hover:bg-(--hover-bg) hover:text-(--foreground) disabled:cursor-not-allowed disabled:opacity-50"
|
|
1826
|
+
onClick={() => {
|
|
1827
|
+
void handleCreateForKey(source.key)
|
|
1828
|
+
}}
|
|
1829
|
+
>
|
|
1830
|
+
{source.key}
|
|
1831
|
+
</button>
|
|
1832
|
+
))}
|
|
1833
|
+
</div>
|
|
1834
|
+
)}
|
|
1835
|
+
<div className="mb-3 flex flex-wrap items-center gap-1.5 border-t border-(--border) pt-3">
|
|
1836
|
+
<input
|
|
1837
|
+
type="text"
|
|
1838
|
+
value={pageId}
|
|
1839
|
+
onChange={event => setPageId(event.target.value)}
|
|
1840
|
+
placeholder={context?.page.id ?? "page_id"}
|
|
1841
|
+
spellCheck={false}
|
|
1842
|
+
className="min-w-0 flex-1 rounded-md border border-(--border) bg-(--app-bg) px-2 py-1 font-mono text-[11px] outline-none transition-colors focus:border-(--foreground)"
|
|
1843
|
+
/>
|
|
1844
|
+
<button
|
|
1845
|
+
type="button"
|
|
1846
|
+
disabled={fetchDisabled}
|
|
1847
|
+
className="rounded-md border border-(--border) bg-transparent px-2 py-1 font-mono text-[11px] text-(--muted) transition-colors hover:border-(--foreground)/20 hover:bg-(--hover-bg) hover:text-(--foreground) disabled:cursor-not-allowed disabled:opacity-50"
|
|
1848
|
+
onClick={() => {
|
|
1849
|
+
void handleFetchPage()
|
|
1850
|
+
}}
|
|
1851
|
+
>
|
|
1852
|
+
Get page
|
|
1853
|
+
</button>
|
|
1854
|
+
</div>
|
|
1855
|
+
{trackedPages.length > 0 && (
|
|
1856
|
+
<div className="mb-2 space-y-2">
|
|
1857
|
+
{trackedPages.map(trackedPage => {
|
|
1858
|
+
const archived = trackedPage.page.in_trash === true
|
|
1859
|
+
return (
|
|
1860
|
+
<div
|
|
1861
|
+
key={trackedPage.page.id}
|
|
1862
|
+
className={`rounded-lg border border-(--border) px-3 py-2 ${
|
|
1863
|
+
archived ? "bg-(--hover-bg)" : "bg-(--app-bg)"
|
|
1864
|
+
}`}
|
|
1865
|
+
>
|
|
1866
|
+
<div className="mb-2 flex items-center justify-between gap-2">
|
|
1867
|
+
<div className="min-w-0">
|
|
1868
|
+
<div className="truncate font-mono text-[11px] text-(--foreground)">
|
|
1869
|
+
{trackedPage.page.id}
|
|
1870
|
+
</div>
|
|
1871
|
+
<div className="font-mono text-[10px] text-(--muted)">
|
|
1872
|
+
{trackedPage.page.parent.type}
|
|
1873
|
+
{archived ? " · archived" : ""}
|
|
1874
|
+
</div>
|
|
1875
|
+
</div>
|
|
1876
|
+
<div className="flex items-center gap-1.5">
|
|
1877
|
+
<button
|
|
1878
|
+
type="button"
|
|
1879
|
+
disabled={busy}
|
|
1880
|
+
className="rounded-md border border-(--border) bg-transparent px-2 py-1 font-mono text-[11px] text-(--muted) transition-colors hover:border-(--foreground)/20 hover:bg-(--hover-bg) hover:text-(--foreground) disabled:cursor-not-allowed disabled:opacity-50"
|
|
1881
|
+
onClick={() => {
|
|
1882
|
+
void handleRefreshPage(trackedPage.page.id)
|
|
1883
|
+
}}
|
|
1884
|
+
>
|
|
1885
|
+
Refresh
|
|
1886
|
+
</button>
|
|
1887
|
+
<button
|
|
1888
|
+
type="button"
|
|
1889
|
+
disabled={busy}
|
|
1890
|
+
className="rounded-md border border-(--border) bg-transparent px-2 py-1 font-mono text-[11px] text-(--muted) transition-colors hover:border-(--foreground)/20 hover:bg-(--hover-bg) hover:text-(--foreground) disabled:cursor-not-allowed disabled:opacity-50"
|
|
1891
|
+
onClick={() => {
|
|
1892
|
+
void handleSavePage(trackedPage)
|
|
1893
|
+
}}
|
|
1894
|
+
>
|
|
1895
|
+
Save
|
|
1896
|
+
</button>
|
|
1897
|
+
<button
|
|
1898
|
+
type="button"
|
|
1899
|
+
disabled={busy || archived}
|
|
1900
|
+
className="rounded-md border border-(--border) bg-transparent px-2 py-1 font-mono text-[11px] text-(--muted) transition-colors hover:border-(--foreground)/20 hover:bg-(--hover-bg) hover:text-(--foreground) disabled:cursor-not-allowed disabled:opacity-50"
|
|
1901
|
+
onClick={() => {
|
|
1902
|
+
void handleDeletePage(trackedPage.page.id)
|
|
1903
|
+
}}
|
|
1904
|
+
>
|
|
1905
|
+
Delete
|
|
1906
|
+
</button>
|
|
1907
|
+
</div>
|
|
1908
|
+
</div>
|
|
1909
|
+
<div className="grid gap-2 md:grid-cols-2">
|
|
1910
|
+
<input
|
|
1911
|
+
type="text"
|
|
1912
|
+
value={trackedPage.draftTitle}
|
|
1913
|
+
onChange={event => {
|
|
1914
|
+
const nextTitle = event.target.value
|
|
1915
|
+
setTrackedPages(prev =>
|
|
1916
|
+
prev.map(entry =>
|
|
1917
|
+
entry.page.id === trackedPage.page.id
|
|
1918
|
+
? { ...entry, draftTitle: nextTitle }
|
|
1919
|
+
: entry,
|
|
1920
|
+
),
|
|
1921
|
+
)
|
|
1922
|
+
}}
|
|
1923
|
+
placeholder="Page title"
|
|
1924
|
+
className="rounded-md border border-(--border) bg-(--app-bg) px-2 py-1 font-mono text-[11px] outline-none transition-colors focus:border-(--foreground)"
|
|
1925
|
+
/>
|
|
1926
|
+
<input
|
|
1927
|
+
type="text"
|
|
1928
|
+
value={trackedPage.draftIcon}
|
|
1929
|
+
onChange={event => {
|
|
1930
|
+
const nextIcon = event.target.value
|
|
1931
|
+
setTrackedPages(prev =>
|
|
1932
|
+
prev.map(entry =>
|
|
1933
|
+
entry.page.id === trackedPage.page.id
|
|
1934
|
+
? { ...entry, draftIcon: nextIcon }
|
|
1935
|
+
: entry,
|
|
1936
|
+
),
|
|
1937
|
+
)
|
|
1938
|
+
}}
|
|
1939
|
+
placeholder="Emoji or URL icon"
|
|
1940
|
+
className="rounded-md border border-(--border) bg-(--app-bg) px-2 py-1 font-mono text-[11px] outline-none transition-colors focus:border-(--foreground)"
|
|
1941
|
+
/>
|
|
1942
|
+
</div>
|
|
1943
|
+
</div>
|
|
1944
|
+
)
|
|
1945
|
+
})}
|
|
1946
|
+
</div>
|
|
1947
|
+
)}
|
|
1948
|
+
{status !== null && (
|
|
1949
|
+
<div className="font-mono text-[11px] text-(--muted)">{status}</div>
|
|
1950
|
+
)}
|
|
1951
|
+
</CollapsibleCard>
|
|
1952
|
+
)
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
// --- Mount ---
|
|
1956
|
+
|
|
1957
|
+
ReactDOM.createRoot(document.getElementById("root")!).render(
|
|
1958
|
+
<ErrorBoundary>
|
|
1959
|
+
<NotionCustomBlock>
|
|
1960
|
+
<App />
|
|
1961
|
+
</NotionCustomBlock>
|
|
1962
|
+
</ErrorBoundary>,
|
|
1963
|
+
)
|