lynx-console 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/assets/src/components/BottomSheet.css.ts.vanilla-D-1A77Ik.css +83 -0
  2. package/dist/assets/src/components/ConsolePanel.css.ts.vanilla-B3avfSlI.css +246 -0
  3. package/dist/assets/src/components/FloatingButton.css.ts.vanilla-rPj35oLW.css +55 -0
  4. package/dist/assets/src/components/NetworkPanel.css.ts.vanilla-DFMduT0T.css +247 -0
  5. package/dist/assets/src/components/PerformancePanel.css.ts.vanilla-D35LuXlW.css +216 -0
  6. package/dist/assets/src/components/Tabs.css.ts.vanilla-DD7L2oXt.css +50 -0
  7. package/dist/index.cjs +1058 -0
  8. package/dist/index.css +466 -0
  9. package/dist/index.css.map +1 -0
  10. package/dist/index.d.cts +17 -0
  11. package/dist/index.d.cts.map +1 -0
  12. package/dist/index.d.mts +17 -0
  13. package/dist/index.d.mts.map +1 -0
  14. package/dist/index.mjs +1059 -0
  15. package/dist/index.mjs.map +1 -0
  16. package/dist/setup.cjs +377 -0
  17. package/dist/setup.d.cts +15 -0
  18. package/dist/setup.d.cts.map +1 -0
  19. package/dist/setup.d.mts +15 -0
  20. package/dist/setup.d.mts.map +1 -0
  21. package/dist/setup.mjs +374 -0
  22. package/dist/setup.mjs.map +1 -0
  23. package/package.json +51 -0
  24. package/src/components/BottomSheet.css.ts +93 -0
  25. package/src/components/BottomSheet.tsx +142 -0
  26. package/src/components/ConsolePanel.css.ts +261 -0
  27. package/src/components/ConsolePanel.tsx +41 -0
  28. package/src/components/FloatingButton.css.ts +62 -0
  29. package/src/components/FloatingButton.tsx +37 -0
  30. package/src/components/LogPanel.tsx +241 -0
  31. package/src/components/NetworkDetailSection.tsx +42 -0
  32. package/src/components/NetworkPanel.css.ts +280 -0
  33. package/src/components/NetworkPanel.tsx +222 -0
  34. package/src/components/PerformancePanel.css.ts +224 -0
  35. package/src/components/PerformancePanel.tsx +209 -0
  36. package/src/components/Tabs.css.ts +66 -0
  37. package/src/components/Tabs.tsx +81 -0
  38. package/src/globals.d.ts +9 -0
  39. package/src/hooks/index.ts +3 -0
  40. package/src/hooks/useConsole.ts +35 -0
  41. package/src/hooks/useNetwork.ts +36 -0
  42. package/src/hooks/usePerformance.ts +39 -0
  43. package/src/index.tsx +110 -0
  44. package/src/setup/_setupMainThreadConsole.ts +80 -0
  45. package/src/setup/index.ts +4 -0
  46. package/src/setup/setupLogMonitor.ts +78 -0
  47. package/src/setup/setupMainThreadConsole.ts +34 -0
  48. package/src/setup/setupNetworkMonitor.ts +247 -0
  49. package/src/setup/setupPerformanceMonitor.ts +70 -0
  50. package/src/shared/ensureConsoleStructure.ts +20 -0
  51. package/src/styles/getDimensionValue.ts +7 -0
  52. package/src/styles/global.css.ts +10 -0
  53. package/src/styles/tokens.json +80 -0
  54. package/src/styles/typography.ts +25 -0
  55. package/src/styles/vars/color.ts +228 -0
  56. package/src/styles/vars/dimension.ts +79 -0
  57. package/src/styles/vars/index.css +463 -0
  58. package/src/styles/vars/index.ts +22 -0
  59. package/src/styles/vars/radius.ts +12 -0
  60. package/src/types.ts +96 -0
@@ -0,0 +1,222 @@
1
+ import { useState } from "@lynx-js/react";
2
+ import type { NetworkEntry } from "../types";
3
+ import { NetworkDetailSection } from "./NetworkDetailSection";
4
+ import * as css from "./NetworkPanel.css";
5
+
6
+ interface NetworkPanelProps {
7
+ networks: NetworkEntry[];
8
+ clearNetworks: () => void;
9
+ }
10
+
11
+ type TabType = "general" | "request" | "response";
12
+
13
+ export const NetworkPanel = ({
14
+ networks,
15
+ clearNetworks,
16
+ }: NetworkPanelProps) => {
17
+ const [selectedId, setSelectedId] = useState<string | null>(null);
18
+ const [activeTab, setActiveTab] = useState<TabType>("general");
19
+
20
+ const formatDuration = (duration?: number): string => {
21
+ if (!duration) return "-";
22
+ if (duration < 1000) return `${duration}ms`;
23
+ return `${(duration / 1000).toFixed(2)}s`;
24
+ };
25
+
26
+ const extractPath = (url: string): string => {
27
+ const pathMatch = url.match(/^https?:\/\/[^/]+(.*)$/);
28
+ if (pathMatch?.[1]) {
29
+ return pathMatch[1].startsWith("/")
30
+ ? pathMatch[1].slice(1)
31
+ : pathMatch[1];
32
+ }
33
+ return url;
34
+ };
35
+
36
+ const getGeneralInfo = (network: NetworkEntry) => {
37
+ return [
38
+ { key: "URL", value: network.url },
39
+ { key: "Method", value: network.method },
40
+ network.statusCode
41
+ ? { key: "Status", value: String(network.statusCode) }
42
+ : null,
43
+ {
44
+ key: "Request Time",
45
+ value: new Date(network.startTime).toISOString(),
46
+ },
47
+ network.endTime
48
+ ? {
49
+ key: "Response Time",
50
+ value: new Date(network.endTime).toISOString(),
51
+ }
52
+ : null,
53
+ network.duration
54
+ ? { key: "Duration", value: formatDuration(network.duration) }
55
+ : null,
56
+ ].filter((item) => item !== null);
57
+ };
58
+
59
+ const getStatusCodeVariant = (
60
+ status: string,
61
+ statusCode?: number,
62
+ ): "success" | "error" | "pending" => {
63
+ if (status === "pending") return "pending";
64
+ if (status === "error") return "error";
65
+ if (statusCode && statusCode >= 200 && statusCode < 300) return "success";
66
+ return "error";
67
+ };
68
+
69
+ return (
70
+ <view className={css.container}>
71
+ <view className={css.header}>
72
+ <text className={css.count}>Total: {networks.length} requests</text>
73
+ <view className={css.clearButton} bindtap={clearNetworks}>
74
+ <text className={css.clearButtonText}>Clear</text>
75
+ </view>
76
+ </view>
77
+
78
+ {networks.length === 0 ? (
79
+ <view className={css.placeholder}>
80
+ <text className={css.placeholderText}>No network requests yet</text>
81
+ </view>
82
+ ) : (
83
+ <list className={css.list}>
84
+ {networks.map((network) => (
85
+ <list-item key={network.id} item-key={network.id}>
86
+ <view className={css.item({ status: network.status })}>
87
+ <view
88
+ className={css.itemHeader}
89
+ bindtap={() =>
90
+ setSelectedId(selectedId === network.id ? null : network.id)
91
+ }
92
+ >
93
+ <text
94
+ className={css.method({
95
+ type: network.method as
96
+ | "GET"
97
+ | "POST"
98
+ | "PUT"
99
+ | "PATCH"
100
+ | "DELETE",
101
+ })}
102
+ >
103
+ {network.method}
104
+ </text>
105
+ {network.statusCode && (
106
+ <text
107
+ className={css.statusCode({
108
+ type: getStatusCodeVariant(
109
+ network.status,
110
+ network.statusCode,
111
+ ),
112
+ })}
113
+ >
114
+ {network.statusCode}
115
+ </text>
116
+ )}
117
+ {network.status === "pending" && (
118
+ <text className={css.statusCode({ type: "pending" })}>
119
+ Pending...
120
+ </text>
121
+ )}
122
+ <text className={css.time}>
123
+ {formatDuration(network.duration)}
124
+ </text>
125
+ <text className={css.time}>
126
+ {new Date(network.startTime).toISOString()}
127
+ </text>
128
+ </view>
129
+
130
+ <text
131
+ className={css.path}
132
+ bindtap={() =>
133
+ setSelectedId(selectedId === network.id ? null : network.id)
134
+ }
135
+ >
136
+ {extractPath(network.url)}
137
+ </text>
138
+
139
+ {selectedId === network.id && (
140
+ <view className={css.detailsContainer}>
141
+ {/* Tabs */}
142
+ <view className={css.tabs}>
143
+ <view
144
+ className={css.tab({ active: activeTab === "general" })}
145
+ bindtap={() => setActiveTab("general")}
146
+ >
147
+ <text
148
+ className={css.tabText({
149
+ active: activeTab === "general",
150
+ })}
151
+ >
152
+ General
153
+ </text>
154
+ </view>
155
+ <view
156
+ className={css.tab({ active: activeTab === "request" })}
157
+ bindtap={() => setActiveTab("request")}
158
+ >
159
+ <text
160
+ className={css.tabText({
161
+ active: activeTab === "request",
162
+ })}
163
+ >
164
+ Request
165
+ </text>
166
+ </view>
167
+ <view
168
+ className={css.tab({
169
+ active: activeTab === "response",
170
+ })}
171
+ bindtap={() => setActiveTab("response")}
172
+ >
173
+ <text
174
+ className={css.tabText({
175
+ active: activeTab === "response",
176
+ })}
177
+ >
178
+ Response
179
+ </text>
180
+ </view>
181
+ </view>
182
+
183
+ {/* Tab Content */}
184
+ <view className={css.tabContent}>
185
+ {activeTab === "general" && (
186
+ <view className={css.table}>
187
+ {getGeneralInfo(network).map((item) => (
188
+ <view key={item.key} className={css.tableRow}>
189
+ <text className={css.tableKey}>{item.key}</text>
190
+ <text className={css.tableValue}>
191
+ {item.value}
192
+ </text>
193
+ </view>
194
+ ))}
195
+ </view>
196
+ )}
197
+
198
+ {activeTab === "request" && (
199
+ <NetworkDetailSection
200
+ headers={network.requestHeaders}
201
+ body={network.requestBody}
202
+ />
203
+ )}
204
+
205
+ {activeTab === "response" && (
206
+ <NetworkDetailSection
207
+ headers={network.responseHeaders}
208
+ body={network.responseBody}
209
+ error={network.error}
210
+ />
211
+ )}
212
+ </view>
213
+ </view>
214
+ )}
215
+ </view>
216
+ </list-item>
217
+ ))}
218
+ </list>
219
+ )}
220
+ </view>
221
+ );
222
+ };
@@ -0,0 +1,224 @@
1
+ import { style } from "@vanilla-extract/css";
2
+ import { recipe } from "@vanilla-extract/recipes";
3
+ import { typography } from "../styles/typography";
4
+ import { vars } from "../styles/vars";
5
+
6
+ export const container = style({
7
+ display: "flex",
8
+ flexDirection: "column",
9
+ flex: 1,
10
+ paddingTop: 4,
11
+ });
12
+
13
+ export const header = style({
14
+ display: "flex",
15
+ flexDirection: "row",
16
+ alignItems: "center",
17
+ justifyContent: "space-between",
18
+ marginBottom: 8,
19
+ paddingBottom: 4,
20
+ borderBottomWidth: 1,
21
+ borderBottomColor: vars.$color.stroke.neutralSubtle,
22
+ borderBottomStyle: "solid",
23
+ });
24
+
25
+ export const count = style({
26
+ ...typography("t3", "regular"),
27
+ color: vars.$color.fg.neutralSubtle,
28
+ });
29
+
30
+ export const clearButton = style({
31
+ padding: "6px 12px",
32
+ backgroundColor: vars.$color.bg.neutralWeak,
33
+ borderRadius: 4,
34
+ });
35
+
36
+ export const clearButtonText = style({
37
+ ...typography("t3", "medium"),
38
+ color: vars.$color.fg.neutral,
39
+ });
40
+
41
+ export const list = style({
42
+ flex: 1,
43
+ });
44
+
45
+ export const placeholder = style({
46
+ display: "flex",
47
+ alignItems: "center",
48
+ justifyContent: "center",
49
+ height: "100%",
50
+ });
51
+
52
+ export const placeholderText = style({
53
+ ...typography("t4", "regular"),
54
+ color: vars.$color.fg.disabled,
55
+ });
56
+
57
+ export const item = style({
58
+ padding: "8px",
59
+ borderBottomWidth: 1,
60
+ borderBottomColor: vars.$color.stroke.neutralWeak,
61
+ borderBottomStyle: "solid",
62
+ });
63
+
64
+ export const itemHeader = style({
65
+ display: "flex",
66
+ flexDirection: "row",
67
+ alignItems: "center",
68
+ marginBottom: 4,
69
+ gap: 8,
70
+ });
71
+
72
+ export const entryType = recipe({
73
+ base: {
74
+ ...typography("t2", "bold"),
75
+ padding: "0 6px",
76
+ borderRadius: 2,
77
+ color: vars.$color.fg.neutral,
78
+ backgroundColor: vars.$color.bg.neutralWeak,
79
+ },
80
+ variants: {
81
+ type: {
82
+ init: {
83
+ color: vars.$color.palette.blue600,
84
+ backgroundColor: vars.$color.palette.blue100,
85
+ },
86
+ metric: {
87
+ color: vars.$color.palette.green600,
88
+ backgroundColor: vars.$color.palette.green100,
89
+ },
90
+ pipeline: {
91
+ color: vars.$color.palette.purple600,
92
+ backgroundColor: vars.$color.palette.purple100,
93
+ },
94
+ resource: {
95
+ color: vars.$color.palette.yellow600,
96
+ backgroundColor: vars.$color.palette.yellow100,
97
+ },
98
+ },
99
+ },
100
+ });
101
+
102
+ export const entryName = style({
103
+ ...typography("t2", "medium"),
104
+ color: vars.$color.fg.neutral,
105
+ });
106
+
107
+ export const timestamp = style({
108
+ ...typography("t2", "regular"),
109
+ color: vars.$color.fg.neutralSubtle,
110
+ });
111
+
112
+ export const fcpMetricHeader = style({
113
+ display: "flex",
114
+ flexDirection: "row",
115
+ alignItems: "center",
116
+ gap: 8,
117
+ });
118
+
119
+ export const fcpHighlight = style({
120
+ ...typography("t3", "bold"),
121
+ color: vars.$color.palette.blue600,
122
+ backgroundColor: vars.$color.palette.blue100,
123
+ padding: "4px 8px",
124
+ borderRadius: 4,
125
+ marginTop: 4,
126
+ });
127
+
128
+ export const metrics = style({
129
+ marginTop: 8,
130
+ display: "flex",
131
+ flexDirection: "column",
132
+ gap: 4,
133
+ });
134
+
135
+ export const metric = style({
136
+ display: "flex",
137
+ flexDirection: "row",
138
+ alignItems: "center",
139
+ gap: 8,
140
+ padding: "4px 8px",
141
+ backgroundColor: vars.$color.bg.neutralWeak,
142
+ borderRadius: 2,
143
+ });
144
+
145
+ export const metricName = style({
146
+ ...typography("t3", "medium"),
147
+ color: vars.$color.fg.neutralSubtle,
148
+ minWidth: 100,
149
+ flexShrink: 0,
150
+ });
151
+
152
+ export const metricValue = style({
153
+ ...typography("t3", "bold"),
154
+ color: vars.$color.palette.green600,
155
+ flex: 1,
156
+ });
157
+
158
+ export const detailsContainer = style({
159
+ marginTop: 12,
160
+ display: "flex",
161
+ flexDirection: "column",
162
+ gap: 12,
163
+ });
164
+
165
+ export const fcpSection = style({
166
+ display: "flex",
167
+ flexDirection: "column",
168
+ gap: 8,
169
+ });
170
+
171
+ export const fcpSectionDescription = style({
172
+ ...typography("t3", "regular"),
173
+ color: vars.$color.fg.neutralSubtle,
174
+ marginBottom: 4,
175
+ });
176
+
177
+ export const fcpMetric = style({
178
+ backgroundColor: vars.$color.bg.layerDefault,
179
+ borderRadius: 4,
180
+ display: "flex",
181
+ flexDirection: "column",
182
+ gap: 4,
183
+ });
184
+
185
+ export const fcpMetricName = style({
186
+ ...typography("t2", "bold"),
187
+ color: vars.$color.fg.neutral,
188
+ });
189
+
190
+ export const fcpMetricValue = style({
191
+ ...typography("t1", "bold"),
192
+ color: vars.$color.palette.blue600,
193
+ });
194
+
195
+ export const fcpMetricDescription = style({
196
+ ...typography("t3", "regular"),
197
+ color: vars.$color.fg.neutralSubtle,
198
+ });
199
+
200
+ export const fcpMetricFormula = style({
201
+ ...typography("t4", "regular"),
202
+ color: vars.$color.fg.disabled,
203
+ fontFamily: "monospace",
204
+ });
205
+
206
+ export const rawEntrySection = style({
207
+ padding: 12,
208
+ backgroundColor: vars.$color.bg.neutralWeak,
209
+ borderRadius: 4,
210
+ });
211
+
212
+ export const detailTitle = style({
213
+ ...typography("t3", "bold"),
214
+ color: vars.$color.fg.neutral,
215
+ marginBottom: 8,
216
+ });
217
+
218
+ export const rawEntry = style({
219
+ ...typography("t3", "regular"),
220
+ color: vars.$color.fg.neutralSubtle,
221
+ fontFamily: "monospace",
222
+ whiteSpace: "pre-wrap",
223
+ wordBreak: "break-all",
224
+ });
@@ -0,0 +1,209 @@
1
+ import { useState } from "@lynx-js/react";
2
+ import { stringify } from "javascript-stringify";
3
+ import type { PerformanceEntryData } from "../types";
4
+ import * as css from "./PerformancePanel.css";
5
+
6
+ interface PerformancePanelProps {
7
+ performances: PerformanceEntryData[];
8
+ clearPerformances: () => void;
9
+ }
10
+
11
+ interface FcpMetric {
12
+ name: string;
13
+ duration: number;
14
+ }
15
+
16
+ interface MetricFcpEntry {
17
+ totalFcp?: FcpMetric;
18
+ lynxFcp?: FcpMetric;
19
+ fcp?: FcpMetric;
20
+ }
21
+
22
+ const isMetricFcpEntry = (entry: PerformanceEntryData): boolean => {
23
+ return entry.entryType === "metric" && entry.name === "fcp";
24
+ };
25
+
26
+ const extractFcpMetrics = (entry: PerformanceEntryData) => {
27
+ if (!isMetricFcpEntry(entry) || !entry.rawEntry) {
28
+ return null;
29
+ }
30
+
31
+ const metricEntry = entry.rawEntry as MetricFcpEntry;
32
+
33
+ return {
34
+ totalFcp: metricEntry.totalFcp ?? undefined,
35
+ lynxFcp: metricEntry.lynxFcp ?? undefined,
36
+ fcp: metricEntry.fcp ?? undefined,
37
+ };
38
+ };
39
+
40
+ const formatDuration = (ms?: number): string => {
41
+ if (ms === undefined) return "-";
42
+ return `${ms.toFixed(2)}ms`;
43
+ };
44
+
45
+ const getPrimaryFcpLabel = (entry: PerformanceEntryData): string => {
46
+ const fcpMetrics = extractFcpMetrics(entry);
47
+ if (!fcpMetrics) return "";
48
+
49
+ const { totalFcp, lynxFcp, fcp } = fcpMetrics;
50
+
51
+ if (totalFcp?.duration !== undefined) {
52
+ return `totalFcp: ${formatDuration(totalFcp.duration)}`;
53
+ }
54
+ if (lynxFcp?.duration !== undefined) {
55
+ return `lynxFcp: ${formatDuration(lynxFcp.duration)}`;
56
+ }
57
+ if (fcp?.duration !== undefined) {
58
+ return `fcp: ${formatDuration(fcp.duration)}`;
59
+ }
60
+ return "";
61
+ };
62
+
63
+ export const PerformancePanel = ({
64
+ performances,
65
+ clearPerformances,
66
+ }: PerformancePanelProps) => {
67
+ const [selectedId, setSelectedId] = useState<string | null>(null);
68
+
69
+ if (performances.length === 0) {
70
+ return (
71
+ <view className={css.container}>
72
+ <view className={css.header}>
73
+ <text className={css.count}>0 entries</text>
74
+ <view
75
+ bindtap={() => {
76
+ console.log("[PerformancePanel] performances", performances);
77
+ }}
78
+ style={{ padding: "10px", backgroundColor: "red" }}
79
+ >
80
+ <text>Log</text>
81
+ </view>
82
+ <view bindtap={clearPerformances} className={css.clearButton}>
83
+ <text className={css.clearButtonText}>Clear</text>
84
+ </view>
85
+ </view>
86
+ <view className={css.placeholder}>
87
+ <text className={css.placeholderText}>
88
+ No performance data yet...
89
+ </text>
90
+ </view>
91
+ </view>
92
+ );
93
+ }
94
+
95
+ return (
96
+ <view className={css.container}>
97
+ <view className={css.header}>
98
+ <text className={css.count}>{performances.length} entries</text>
99
+ <view bindtap={clearPerformances} className={css.clearButton}>
100
+ <text className={css.clearButtonText}>Clear</text>
101
+ </view>
102
+ </view>
103
+
104
+ <list className={css.list}>
105
+ {performances.map((perf) => {
106
+ const isMetricFcp = isMetricFcpEntry(perf);
107
+ const fcpMetrics = extractFcpMetrics(perf);
108
+ const primaryFcp = getPrimaryFcpLabel(perf);
109
+ const { totalFcp, lynxFcp, fcp } = fcpMetrics ?? {};
110
+
111
+ return (
112
+ <list-item key={perf.id} item-key={perf.id}>
113
+ <view className={css.item}>
114
+ <view
115
+ className={css.itemHeader}
116
+ bindtap={() =>
117
+ setSelectedId(selectedId === perf.id ? null : perf.id)
118
+ }
119
+ >
120
+ <text className={css.entryType({ type: perf.entryType })}>
121
+ {perf.entryType}
122
+ </text>
123
+ <text className={css.entryName}>{perf.name}</text>
124
+ <text className={css.timestamp}>
125
+ {new Date(perf.timestamp).toISOString()}
126
+ </text>
127
+ </view>
128
+
129
+ <view
130
+ bindtap={() =>
131
+ setSelectedId(selectedId === perf.id ? null : perf.id)
132
+ }
133
+ >
134
+ {isMetricFcp && primaryFcp && (
135
+ <text className={css.fcpHighlight}>{primaryFcp}</text>
136
+ )}
137
+ </view>
138
+
139
+ {selectedId === perf.id && (
140
+ <view className={css.detailsContainer}>
141
+ {isMetricFcp && fcpMetrics && (
142
+ <view className={css.fcpSection}>
143
+ {totalFcp !== undefined && (
144
+ <view className={css.fcpMetric}>
145
+ <view className={css.fcpMetricHeader}>
146
+ <text className={css.fcpMetricName}>
147
+ 전체 FCP
148
+ </text>
149
+ <text className={css.fcpMetricValue}>
150
+ {formatDuration(totalFcp.duration)}
151
+ </text>
152
+ </view>
153
+ <text className={css.fcpMetricDescription}>
154
+ PrepareTemplate Start부터 Paint End 까지 걸리는
155
+ 시간
156
+ </text>
157
+ </view>
158
+ )}
159
+
160
+ {lynxFcp !== undefined && (
161
+ <view className={css.fcpMetric}>
162
+ <view className={css.fcpMetricHeader}>
163
+ <text className={css.fcpMetricName}>LynxFCP</text>
164
+ <text className={css.fcpMetricValue}>
165
+ {formatDuration(lynxFcp.duration)}
166
+ </text>
167
+ </view>
168
+ <text className={css.fcpMetricDescription}>
169
+ Bundle Load 시작부터 Paint End 까지 걸리는 시간
170
+ </text>
171
+ </view>
172
+ )}
173
+
174
+ {fcp !== undefined && (
175
+ <view className={css.fcpMetric}>
176
+ <view className={css.fcpMetricHeader}>
177
+ <text className={css.fcpMetricName}>
178
+ 렌더링 FCP
179
+ </text>
180
+ <text className={css.fcpMetricValue}>
181
+ {formatDuration(fcp.duration)}
182
+ </text>
183
+ </view>
184
+ <text className={css.fcpMetricDescription}>
185
+ TemplateBundle 준비부터 Paint End 까지 걸리는 시간
186
+ </text>
187
+ </view>
188
+ )}
189
+ </view>
190
+ )}
191
+
192
+ {!!perf.rawEntry && (
193
+ <view className={css.rawEntrySection}>
194
+ <text className={css.detailTitle}>Raw Entry</text>
195
+ <text className={css.rawEntry}>
196
+ {String(stringify(perf.rawEntry, null, 2, { references: true }))}
197
+ </text>
198
+ </view>
199
+ )}
200
+ </view>
201
+ )}
202
+ </view>
203
+ </list-item>
204
+ );
205
+ })}
206
+ </list>
207
+ </view>
208
+ );
209
+ };