lynx-console 0.0.1 → 0.1.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.
- package/dist/assets/src/components/{BottomSheet.css.ts.vanilla-D-1A77Ik.css → BottomSheet.css.ts.vanilla-CulwSDhG.css} +2 -2
- package/dist/assets/src/components/ConsolePanel.css.ts.vanilla-DFvHPEyg.css +340 -0
- package/dist/assets/src/components/{FloatingButton.css.ts.vanilla-rPj35oLW.css → FloatingButton.css.ts.vanilla-BaG0OI6p.css} +15 -3
- package/dist/index.cjs +700 -70
- package/dist/index.mjs +700 -70
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/BottomSheet.css.ts +2 -2
- package/src/components/ConsolePanel.css.ts +108 -15
- package/src/components/ConsolePanel.tsx +2 -1
- package/src/components/FloatingButton.css.ts +15 -4
- package/src/components/FloatingButton.tsx +40 -7
- package/src/components/LogPanel.tsx +140 -10
- package/src/components/Tabs.tsx +3 -0
- package/src/hooks/useLongPressDrag.ts +95 -0
- package/src/index.tsx +1 -1
- package/dist/assets/src/components/ConsolePanel.css.ts.vanilla-B3avfSlI.css +0 -246
|
@@ -33,20 +33,113 @@ export const logHeader = style({
|
|
|
33
33
|
flexDirection: "row",
|
|
34
34
|
alignItems: "center",
|
|
35
35
|
justifyContent: "space-between",
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
paddingBottom: 3,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export const fadeTop = style({
|
|
40
|
+
height: 20,
|
|
41
|
+
marginBottom: -20,
|
|
42
|
+
zIndex: 1,
|
|
43
|
+
background: `linear-gradient(to bottom, ${vars.$color.bg.layerDefault}, #ffffff00)`,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export const filterWrapper = style({
|
|
47
|
+
position: "relative",
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
export const filterButton = style({
|
|
51
|
+
display: "flex",
|
|
52
|
+
flexDirection: "row",
|
|
53
|
+
alignItems: "center",
|
|
54
|
+
padding: "3px 6px",
|
|
55
|
+
backgroundColor: vars.$color.bg.neutralWeak,
|
|
56
|
+
borderRadius: 4,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
export const filterButtonText = style({
|
|
60
|
+
...typography("t3", "medium"),
|
|
61
|
+
color: vars.$color.fg.neutral,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
export const filterDropdown = style({
|
|
65
|
+
position: "absolute",
|
|
66
|
+
top: "100%",
|
|
67
|
+
left: 0,
|
|
68
|
+
marginTop: 4,
|
|
69
|
+
backgroundColor: vars.$color.bg.layerDefault,
|
|
70
|
+
borderWidth: 1,
|
|
71
|
+
borderColor: vars.$color.stroke.neutralSubtle,
|
|
72
|
+
borderStyle: "solid",
|
|
73
|
+
borderRadius: 8,
|
|
74
|
+
padding: "4px 0",
|
|
75
|
+
zIndex: 100,
|
|
76
|
+
minWidth: 90,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
export const filterOption = style({
|
|
80
|
+
display: "flex",
|
|
81
|
+
flexDirection: "row",
|
|
82
|
+
alignItems: "center",
|
|
83
|
+
gap: 4,
|
|
84
|
+
padding: "8px 12px",
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
export const filterCheckbox = recipe({
|
|
88
|
+
base: {
|
|
89
|
+
...typography("t3", "medium"),
|
|
90
|
+
width: 16,
|
|
91
|
+
},
|
|
92
|
+
variants: {
|
|
93
|
+
level: {
|
|
94
|
+
log: { color: vars.$color.palette.green600 },
|
|
95
|
+
info: { color: vars.$color.palette.blue600 },
|
|
96
|
+
warn: { color: vars.$color.palette.yellow600 },
|
|
97
|
+
error: { color: vars.$color.palette.red600 },
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
export const filterLabel = recipe({
|
|
103
|
+
base: {
|
|
104
|
+
...typography("t3", "medium"),
|
|
105
|
+
},
|
|
106
|
+
variants: {
|
|
107
|
+
level: {
|
|
108
|
+
log: { color: vars.$color.palette.green600 },
|
|
109
|
+
info: { color: vars.$color.palette.blue600 },
|
|
110
|
+
warn: { color: vars.$color.palette.yellow600 },
|
|
111
|
+
error: { color: vars.$color.palette.red600 },
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
export const searchWrapper = style({
|
|
117
|
+
display: "flex",
|
|
118
|
+
flexDirection: "row",
|
|
119
|
+
alignItems: "center",
|
|
120
|
+
flex: 1,
|
|
121
|
+
marginLeft: 8,
|
|
122
|
+
marginRight: 8,
|
|
38
123
|
borderBottomWidth: 1,
|
|
39
124
|
borderBottomColor: vars.$color.stroke.neutralSubtle,
|
|
40
125
|
borderBottomStyle: "solid",
|
|
126
|
+
gap: 8,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
export const searchPrompt = style({
|
|
130
|
+
...typography("t6", "medium"),
|
|
131
|
+
color: vars.$color.fg.placeholder,
|
|
41
132
|
});
|
|
42
133
|
|
|
43
|
-
export const
|
|
134
|
+
export const searchInput = style({
|
|
135
|
+
flex: 1,
|
|
44
136
|
...typography("t3", "regular"),
|
|
45
|
-
color: vars.$color.fg.
|
|
137
|
+
color: vars.$color.fg.neutral,
|
|
138
|
+
caretColor: vars.$color.palette.green600,
|
|
46
139
|
});
|
|
47
140
|
|
|
48
141
|
export const clearButton = style({
|
|
49
|
-
padding: "6px
|
|
142
|
+
padding: "3px 6px",
|
|
50
143
|
backgroundColor: vars.$color.bg.neutralWeak,
|
|
51
144
|
borderRadius: 4,
|
|
52
145
|
});
|
|
@@ -58,6 +151,8 @@ export const clearButtonText = style({
|
|
|
58
151
|
|
|
59
152
|
export const logList = style({
|
|
60
153
|
flex: 1,
|
|
154
|
+
paddingTop: 0,
|
|
155
|
+
paddingBottom: 0,
|
|
61
156
|
});
|
|
62
157
|
|
|
63
158
|
export const logItem = recipe({
|
|
@@ -216,22 +311,20 @@ export const argObjectJson = style({
|
|
|
216
311
|
color: vars.$color.fg.neutral,
|
|
217
312
|
});
|
|
218
313
|
|
|
314
|
+
export const fadeBottom = style({
|
|
315
|
+
height: 20,
|
|
316
|
+
marginTop: -20,
|
|
317
|
+
zIndex: 1,
|
|
318
|
+
background: `linear-gradient(to top, ${vars.$color.bg.layerDefault}, #ffffff00)`,
|
|
319
|
+
});
|
|
320
|
+
|
|
219
321
|
export const replInputRow = style({
|
|
220
322
|
display: "flex",
|
|
221
323
|
flexDirection: "row",
|
|
222
324
|
alignItems: "center",
|
|
223
325
|
gap: 8,
|
|
224
|
-
paddingTop:
|
|
326
|
+
paddingTop: 0,
|
|
225
327
|
paddingBottom: 8,
|
|
226
|
-
marginTop: -1,
|
|
227
|
-
borderTopWidth: 1,
|
|
228
|
-
borderTopColor: vars.$color.stroke.neutralSubtle,
|
|
229
|
-
borderTopStyle: "solid",
|
|
230
|
-
backgroundImage: `linear-gradient(to bottom, transparent, ${vars.$color.bg.layerDefault})`,
|
|
231
|
-
backgroundSize: "100% 32px",
|
|
232
|
-
backgroundRepeat: "no-repeat",
|
|
233
|
-
backgroundPosition: "top",
|
|
234
|
-
backgroundColor: vars.$color.bg.layerDefault,
|
|
235
328
|
});
|
|
236
329
|
|
|
237
330
|
export const replPrompt = style({
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useConsole, useNetwork, usePerformance } from "../hooks";
|
|
2
2
|
import * as css from "./ConsolePanel.css";
|
|
3
|
-
import { LogPanel } from "./LogPanel";
|
|
3
|
+
import { LogPanel, dismissFilterDropdown } from "./LogPanel";
|
|
4
4
|
import { NetworkPanel } from "./NetworkPanel";
|
|
5
5
|
import { PerformancePanel } from "./PerformancePanel";
|
|
6
6
|
import Tabs from "./Tabs";
|
|
@@ -13,6 +13,7 @@ export const ConsolePanel = () => {
|
|
|
13
13
|
return (
|
|
14
14
|
<view className={css.container}>
|
|
15
15
|
<Tabs
|
|
16
|
+
onTabChange={dismissFilterDropdown}
|
|
16
17
|
items={[
|
|
17
18
|
{
|
|
18
19
|
key: "log",
|
|
@@ -4,18 +4,18 @@ import { vars } from "../styles/vars";
|
|
|
4
4
|
|
|
5
5
|
export const wrapper = style({
|
|
6
6
|
position: "fixed",
|
|
7
|
-
right: "16px",
|
|
8
|
-
bottom: "84px",
|
|
9
7
|
zIndex: 9999,
|
|
10
8
|
display: "flex",
|
|
11
9
|
flexDirection: "row",
|
|
12
10
|
alignItems: "center",
|
|
13
11
|
gap: "8px",
|
|
12
|
+
overflow: "visible",
|
|
13
|
+
transition: `transform ${vars.$duration.d4} cubic-bezier(0.4, 0, 0.2, 1)`,
|
|
14
14
|
});
|
|
15
15
|
|
|
16
|
-
export const container = style({});
|
|
17
|
-
|
|
18
16
|
export const button = style({
|
|
17
|
+
position: "relative",
|
|
18
|
+
overflow: "hidden",
|
|
19
19
|
paddingLeft: "8px",
|
|
20
20
|
paddingRight: "8px",
|
|
21
21
|
paddingTop: "4px",
|
|
@@ -30,6 +30,16 @@ export const button = style({
|
|
|
30
30
|
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
+
export const shineOverlay = style({
|
|
34
|
+
position: "absolute",
|
|
35
|
+
top: "-50%",
|
|
36
|
+
left: "-25%",
|
|
37
|
+
width: "150%",
|
|
38
|
+
height: "200%",
|
|
39
|
+
backgroundColor: "rgba(255, 255, 255, 0.2)",
|
|
40
|
+
borderRadius: "9999px",
|
|
41
|
+
});
|
|
42
|
+
|
|
33
43
|
export const title = style({
|
|
34
44
|
...typography("t4", "regular"),
|
|
35
45
|
color: vars.$color.palette.staticWhite,
|
|
@@ -43,6 +53,7 @@ export const subtitle = style({
|
|
|
43
53
|
});
|
|
44
54
|
|
|
45
55
|
export const reloadButton = style({
|
|
56
|
+
overflow: "visible",
|
|
46
57
|
width: "32px",
|
|
47
58
|
height: "32px",
|
|
48
59
|
borderRadius: "16px",
|
|
@@ -1,18 +1,36 @@
|
|
|
1
1
|
import type { ReactNode } from "@lynx-js/react";
|
|
2
|
+
import { useLongPressDrag } from "../hooks/useLongPressDrag";
|
|
2
3
|
import * as css from "./FloatingButton.css";
|
|
3
4
|
|
|
4
5
|
interface FloatingButtonProps {
|
|
5
6
|
bindtap: () => void;
|
|
6
|
-
isVisible: boolean;
|
|
7
7
|
children: ReactNode;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
const SHINE_STYLES = {
|
|
11
|
+
idle: {
|
|
12
|
+
transform: "scale(0)",
|
|
13
|
+
opacity: 0,
|
|
14
|
+
},
|
|
15
|
+
dragging: {
|
|
16
|
+
transform: "scale(1)",
|
|
17
|
+
opacity: 1,
|
|
18
|
+
transition: "transform 300ms cubic-bezier(0.4, 0, 0.2, 1)",
|
|
19
|
+
},
|
|
20
|
+
releasing: {
|
|
21
|
+
transform: "scale(1)",
|
|
22
|
+
opacity: 0,
|
|
23
|
+
transition: "opacity 300ms cubic-bezier(0.4, 0, 0.2, 1)",
|
|
24
|
+
},
|
|
25
|
+
} as const;
|
|
26
|
+
|
|
10
27
|
export const FloatingButton = ({
|
|
11
28
|
bindtap,
|
|
12
|
-
isVisible,
|
|
13
29
|
children,
|
|
14
30
|
}: FloatingButtonProps) => {
|
|
15
|
-
|
|
31
|
+
const { phase, right, bottom, clearTimer, handlers } =
|
|
32
|
+
useLongPressDrag(bindtap);
|
|
33
|
+
|
|
16
34
|
|
|
17
35
|
const handleReload = () => {
|
|
18
36
|
try {
|
|
@@ -24,12 +42,27 @@ export const FloatingButton = ({
|
|
|
24
42
|
}
|
|
25
43
|
};
|
|
26
44
|
|
|
45
|
+
const isDragging = phase === "dragging";
|
|
46
|
+
|
|
27
47
|
return (
|
|
28
|
-
<view
|
|
29
|
-
|
|
30
|
-
|
|
48
|
+
<view
|
|
49
|
+
className={css.wrapper}
|
|
50
|
+
style={{
|
|
51
|
+
right: `${right}px`,
|
|
52
|
+
bottom: `${bottom}px`,
|
|
53
|
+
transform: isDragging ? "scale(1.05)" : "scale(1)",
|
|
54
|
+
}}
|
|
55
|
+
{...handlers}
|
|
56
|
+
>
|
|
57
|
+
<view className={css.button}>
|
|
58
|
+
{children}
|
|
59
|
+
<view className={css.shineOverlay} style={SHINE_STYLES[phase]} />
|
|
31
60
|
</view>
|
|
32
|
-
<view
|
|
61
|
+
<view
|
|
62
|
+
className={css.reloadButton}
|
|
63
|
+
catchtouchstart={() => clearTimer()}
|
|
64
|
+
bindtap={handleReload}
|
|
65
|
+
>
|
|
33
66
|
<text className={css.reloadIcon}>{"\u21BB"}</text>
|
|
34
67
|
</view>
|
|
35
68
|
</view>
|
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from "@lynx-js/react";
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from "@lynx-js/react";
|
|
2
2
|
import type { BaseEvent, InputInputEvent, NodesRef } from "@lynx-js/types";
|
|
3
3
|
import { stringify } from "javascript-stringify";
|
|
4
|
-
import type { LogEntry } from "../types";
|
|
4
|
+
import type { LogEntry, LogLevel } from "../types";
|
|
5
|
+
import { vars } from "../styles/vars";
|
|
5
6
|
import * as css from "./ConsolePanel.css";
|
|
6
7
|
|
|
8
|
+
const LOG_LEVELS: LogLevel[] = ["log", "info", "warn", "error"];
|
|
9
|
+
|
|
10
|
+
let savedEnabledLevels: Set<LogLevel> | null = null;
|
|
11
|
+
let savedSearchQuery = "";
|
|
12
|
+
let closeFilterDropdown: (() => void) | null = null;
|
|
13
|
+
|
|
14
|
+
export const dismissFilterDropdown = () => closeFilterDropdown?.();
|
|
15
|
+
|
|
7
16
|
interface LogPanelProps {
|
|
8
17
|
logs: LogEntry[];
|
|
9
18
|
clearLogs: () => void;
|
|
@@ -26,10 +35,64 @@ const runCode = (code: string) => {
|
|
|
26
35
|
export const LogPanel = ({ logs, clearLogs }: LogPanelProps) => {
|
|
27
36
|
const [expandedArgs, setExpandedArgs] = useState(new Set());
|
|
28
37
|
const [code, setCode] = useState("");
|
|
38
|
+
const [enabledLevels, setEnabledLevels] = useState<Set<LogLevel>>(
|
|
39
|
+
() => savedEnabledLevels ?? new Set(LOG_LEVELS),
|
|
40
|
+
);
|
|
41
|
+
const [filterOpen, setFilterOpen] = useState(false);
|
|
42
|
+
const [searchQuery, setSearchQuery] = useState(savedSearchQuery);
|
|
43
|
+
const [fadeState, setFadeState] = useState({ atTop: true, atBottom: true });
|
|
44
|
+
const fadeRef = useRef({ atTop: true, atBottom: true });
|
|
29
45
|
const inputRef = useRef<NodesRef>(null);
|
|
46
|
+
const searchInputRef = useRef<NodesRef>(null);
|
|
30
47
|
const listRef = useRef<NodesRef>(null);
|
|
31
|
-
|
|
32
|
-
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
savedEnabledLevels = enabledLevels;
|
|
51
|
+
}, [enabledLevels]);
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
savedSearchQuery = searchQuery;
|
|
55
|
+
}, [searchQuery]);
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (savedSearchQuery) {
|
|
59
|
+
searchInputRef.current
|
|
60
|
+
?.invoke({ method: "setValue", params: { value: savedSearchQuery } })
|
|
61
|
+
.exec();
|
|
62
|
+
}
|
|
63
|
+
}, []);
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
closeFilterDropdown = () => setFilterOpen(false);
|
|
67
|
+
return () => { closeFilterDropdown = null; };
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
const filteredLogs = useMemo(
|
|
71
|
+
() =>
|
|
72
|
+
logs.filter((log) => {
|
|
73
|
+
if (!enabledLevels.has(log.level)) return false;
|
|
74
|
+
if (searchQuery) {
|
|
75
|
+
const query = searchQuery.toLowerCase();
|
|
76
|
+
return log.args.some((arg) => String(arg).toLowerCase().includes(query));
|
|
77
|
+
}
|
|
78
|
+
return true;
|
|
79
|
+
}),
|
|
80
|
+
[logs, enabledLevels, searchQuery],
|
|
81
|
+
);
|
|
82
|
+
const logsRef = useRef(filteredLogs);
|
|
83
|
+
logsRef.current = filteredLogs;
|
|
84
|
+
|
|
85
|
+
const toggleLevel = (level: LogLevel) => {
|
|
86
|
+
setEnabledLevels((prev) => {
|
|
87
|
+
const next = new Set(prev);
|
|
88
|
+
if (next.has(level)) {
|
|
89
|
+
next.delete(level);
|
|
90
|
+
} else {
|
|
91
|
+
next.add(level);
|
|
92
|
+
}
|
|
93
|
+
return next;
|
|
94
|
+
});
|
|
95
|
+
};
|
|
33
96
|
|
|
34
97
|
const scrollToBottom = (smooth: boolean) => {
|
|
35
98
|
if (logsRef.current.length === 0) return;
|
|
@@ -43,7 +106,7 @@ export const LogPanel = ({ logs, clearLogs }: LogPanelProps) => {
|
|
|
43
106
|
|
|
44
107
|
useEffect(() => {
|
|
45
108
|
scrollToBottom(true);
|
|
46
|
-
}, [
|
|
109
|
+
}, [filteredLogs]);
|
|
47
110
|
|
|
48
111
|
const toggleArg = (key: string) => {
|
|
49
112
|
setExpandedArgs((prev) => {
|
|
@@ -165,22 +228,81 @@ export const LogPanel = ({ logs, clearLogs }: LogPanelProps) => {
|
|
|
165
228
|
};
|
|
166
229
|
|
|
167
230
|
return (
|
|
168
|
-
<view
|
|
231
|
+
<view
|
|
232
|
+
className={css.logContainer}
|
|
233
|
+
bindtap={() => { if (filterOpen) setFilterOpen(false); }}
|
|
234
|
+
>
|
|
169
235
|
<view className={css.logHeader}>
|
|
170
|
-
<
|
|
236
|
+
<view className={css.filterWrapper}>
|
|
237
|
+
<view
|
|
238
|
+
className={css.filterButton}
|
|
239
|
+
catchtap={() => setFilterOpen((v) => !v)}
|
|
240
|
+
>
|
|
241
|
+
<text className={css.filterButtonText}>Filter ▼</text>
|
|
242
|
+
</view>
|
|
243
|
+
{filterOpen && (
|
|
244
|
+
<view className={css.filterDropdown} catchtap={() => {}}>
|
|
245
|
+
{LOG_LEVELS.map((level) => (
|
|
246
|
+
<view
|
|
247
|
+
key={level}
|
|
248
|
+
className={css.filterOption}
|
|
249
|
+
bindtap={() => toggleLevel(level)}
|
|
250
|
+
>
|
|
251
|
+
<text className={css.filterCheckbox({ level })}>
|
|
252
|
+
{enabledLevels.has(level) ? "✅" : "⬜"}
|
|
253
|
+
</text>
|
|
254
|
+
<text className={css.filterLabel({ level })}>
|
|
255
|
+
{level.toUpperCase()}
|
|
256
|
+
</text>
|
|
257
|
+
</view>
|
|
258
|
+
))}
|
|
259
|
+
</view>
|
|
260
|
+
)}
|
|
261
|
+
</view>
|
|
262
|
+
<view className={css.searchWrapper}>
|
|
263
|
+
<text className={css.searchPrompt}>{"›"}</text>
|
|
264
|
+
<input
|
|
265
|
+
ref={searchInputRef}
|
|
266
|
+
className={css.searchInput}
|
|
267
|
+
placeholder="Search logs..."
|
|
268
|
+
bindinput={(e: BaseEvent<"bindinput", InputInputEvent>) =>
|
|
269
|
+
setSearchQuery(e.detail.value)
|
|
270
|
+
}
|
|
271
|
+
/>
|
|
272
|
+
</view>
|
|
171
273
|
<view style={{ display: "flex", flexDirection: "row", gap: 8 }}>
|
|
172
274
|
<view className={css.clearButton} bindtap={clearLogs}>
|
|
173
275
|
<text className={css.clearButtonText}>Clear</text>
|
|
174
276
|
</view>
|
|
175
277
|
</view>
|
|
176
278
|
</view>
|
|
279
|
+
<view
|
|
280
|
+
className={css.fadeTop}
|
|
281
|
+
style={{
|
|
282
|
+
background: fadeState.atTop
|
|
283
|
+
? `linear-gradient(to bottom, #ffffff00, #ffffff00)`
|
|
284
|
+
: `linear-gradient(to bottom, ${vars.$color.bg.layerDefault}, #ffffff00)`,
|
|
285
|
+
}}
|
|
286
|
+
/>
|
|
177
287
|
<list
|
|
178
288
|
ref={listRef}
|
|
179
289
|
scroll-orientation="vertical"
|
|
180
290
|
className={css.logList}
|
|
181
|
-
|
|
291
|
+
preload-buffer-count={10}
|
|
292
|
+
initial-scroll-index={Math.max(0, filteredLogs.length - 1)}
|
|
293
|
+
scroll-event-throttle={16}
|
|
294
|
+
bindscroll={(e: BaseEvent<"bindscroll", { scrollTop: number; scrollHeight: number; listHeight: number }>) => {
|
|
295
|
+
const { scrollTop, scrollHeight, listHeight } = e.detail;
|
|
296
|
+
const atTop = scrollTop <= 10;
|
|
297
|
+
const atBottom = scrollTop + listHeight >= scrollHeight - 10;
|
|
298
|
+
if (atTop !== fadeRef.current.atTop || atBottom !== fadeRef.current.atBottom) {
|
|
299
|
+
fadeRef.current.atTop = atTop;
|
|
300
|
+
fadeRef.current.atBottom = atBottom;
|
|
301
|
+
setFadeState({ atTop, atBottom });
|
|
302
|
+
}
|
|
303
|
+
}}
|
|
182
304
|
>
|
|
183
|
-
{
|
|
305
|
+
{filteredLogs.length === 0 ? (
|
|
184
306
|
<list-item item-key="empty-state">
|
|
185
307
|
<view className={css.placeholder}>
|
|
186
308
|
<text className={css.placeholderText}>
|
|
@@ -189,7 +311,7 @@ export const LogPanel = ({ logs, clearLogs }: LogPanelProps) => {
|
|
|
189
311
|
</view>
|
|
190
312
|
</list-item>
|
|
191
313
|
) : (
|
|
192
|
-
|
|
314
|
+
filteredLogs.map((log) => {
|
|
193
315
|
return (
|
|
194
316
|
<list-item key={log.id} item-key={log.id}>
|
|
195
317
|
<view className={css.logItem({ level: log.level })}>
|
|
@@ -221,6 +343,14 @@ export const LogPanel = ({ logs, clearLogs }: LogPanelProps) => {
|
|
|
221
343
|
})
|
|
222
344
|
)}
|
|
223
345
|
</list>
|
|
346
|
+
<view
|
|
347
|
+
className={css.fadeBottom}
|
|
348
|
+
style={{
|
|
349
|
+
background: fadeState.atBottom
|
|
350
|
+
? `linear-gradient(to top, #ffffff00, #ffffff00)`
|
|
351
|
+
: `linear-gradient(to top, ${vars.$color.bg.layerDefault}, #ffffff00)`,
|
|
352
|
+
}}
|
|
353
|
+
/>
|
|
224
354
|
<view className={css.replInputRow}>
|
|
225
355
|
<text className={css.replPrompt}>{"›"}</text>
|
|
226
356
|
<input
|
package/src/components/Tabs.tsx
CHANGED
|
@@ -8,6 +8,7 @@ type TabsProps = {
|
|
|
8
8
|
label: string;
|
|
9
9
|
renderContent: () => ReactNode;
|
|
10
10
|
}>;
|
|
11
|
+
onTabChange?: () => void;
|
|
11
12
|
};
|
|
12
13
|
|
|
13
14
|
export default function Tabs(props: TabsProps) {
|
|
@@ -23,6 +24,7 @@ export default function Tabs(props: TabsProps) {
|
|
|
23
24
|
className={css.tabTriggerButton}
|
|
24
25
|
bindtap={() => {
|
|
25
26
|
setActiveIndex(i);
|
|
27
|
+
props.onTabChange?.();
|
|
26
28
|
|
|
27
29
|
tabContentsRef.current
|
|
28
30
|
?.invoke({
|
|
@@ -59,6 +61,7 @@ export default function Tabs(props: TabsProps) {
|
|
|
59
61
|
className={css.tabContents}
|
|
60
62
|
scroll-orientation="horizontal"
|
|
61
63
|
item-snap={{ factor: 0, offset: 0 }}
|
|
64
|
+
bindscroll={() => props.onTabChange?.()}
|
|
62
65
|
bindsnap={(e: ListSnapEvent) => {
|
|
63
66
|
setActiveIndex(e.detail.position);
|
|
64
67
|
}}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { useRef, useState } from "@lynx-js/react";
|
|
2
|
+
import type { BaseTouchEvent, Target } from "@lynx-js/types";
|
|
3
|
+
|
|
4
|
+
const LONG_PRESS_DURATION = 400;
|
|
5
|
+
const MOVE_THRESHOLD = 5;
|
|
6
|
+
|
|
7
|
+
const DEFAULT_RIGHT = 16;
|
|
8
|
+
const DEFAULT_BOTTOM = 84;
|
|
9
|
+
|
|
10
|
+
let savedRight = DEFAULT_RIGHT;
|
|
11
|
+
let savedBottom = DEFAULT_BOTTOM;
|
|
12
|
+
|
|
13
|
+
export function useLongPressDrag(onTap: () => void) {
|
|
14
|
+
const [right, setRight] = useState(savedRight);
|
|
15
|
+
const [bottom, setBottom] = useState(savedBottom);
|
|
16
|
+
const [phase, setPhase] = useState<"idle" | "dragging" | "releasing">("idle");
|
|
17
|
+
const [tempRight, setTempRight] = useState(savedRight);
|
|
18
|
+
const [tempBottom, setTempBottom] = useState(savedBottom);
|
|
19
|
+
|
|
20
|
+
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
21
|
+
const draggingRef = useRef(false);
|
|
22
|
+
const startRef = useRef({ x: 0, y: 0, r: 0, b: 0 });
|
|
23
|
+
|
|
24
|
+
const clearTimer = () => {
|
|
25
|
+
if (timerRef.current) {
|
|
26
|
+
clearTimeout(timerRef.current);
|
|
27
|
+
timerRef.current = null;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const handleTouchStart = (e: BaseTouchEvent<Target>) => {
|
|
32
|
+
startRef.current = {
|
|
33
|
+
x: e.detail.x,
|
|
34
|
+
y: e.detail.y,
|
|
35
|
+
r: right,
|
|
36
|
+
b: bottom,
|
|
37
|
+
};
|
|
38
|
+
draggingRef.current = false;
|
|
39
|
+
|
|
40
|
+
timerRef.current = setTimeout(() => {
|
|
41
|
+
draggingRef.current = true;
|
|
42
|
+
setPhase("dragging");
|
|
43
|
+
setTempRight(right);
|
|
44
|
+
setTempBottom(bottom);
|
|
45
|
+
}, LONG_PRESS_DURATION);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const handleTouchMove = (e: BaseTouchEvent<Target>) => {
|
|
49
|
+
const dx = e.detail.x - startRef.current.x;
|
|
50
|
+
const dy = e.detail.y - startRef.current.y;
|
|
51
|
+
|
|
52
|
+
if (
|
|
53
|
+
!draggingRef.current &&
|
|
54
|
+
(Math.abs(dx) > MOVE_THRESHOLD || Math.abs(dy) > MOVE_THRESHOLD)
|
|
55
|
+
) {
|
|
56
|
+
clearTimer();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!draggingRef.current) return;
|
|
60
|
+
|
|
61
|
+
// right/bottom 기준이므로 방향 반전
|
|
62
|
+
setTempRight(startRef.current.r - dx);
|
|
63
|
+
setTempBottom(startRef.current.b - dy);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const handleTouchEnd = () => {
|
|
67
|
+
clearTimer();
|
|
68
|
+
|
|
69
|
+
if (draggingRef.current) {
|
|
70
|
+
setRight(tempRight);
|
|
71
|
+
setBottom(tempBottom);
|
|
72
|
+
savedRight = tempRight;
|
|
73
|
+
savedBottom = tempBottom;
|
|
74
|
+
setPhase("releasing");
|
|
75
|
+
draggingRef.current = false;
|
|
76
|
+
setTimeout(() => setPhase("idle"), 300);
|
|
77
|
+
} else {
|
|
78
|
+
onTap();
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const isDragging = phase === "dragging";
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
phase,
|
|
86
|
+
right: isDragging ? tempRight : right,
|
|
87
|
+
bottom: isDragging ? tempBottom : bottom,
|
|
88
|
+
clearTimer,
|
|
89
|
+
handlers: {
|
|
90
|
+
bindtouchstart: handleTouchStart,
|
|
91
|
+
bindtouchmove: handleTouchMove,
|
|
92
|
+
bindtouchend: handleTouchEnd,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
package/src/index.tsx
CHANGED
|
@@ -85,7 +85,7 @@ const LynxConsole = forwardRef<LynxConsoleHandle, LynxConsoleProps>(
|
|
|
85
85
|
|
|
86
86
|
return (
|
|
87
87
|
<view className={themeClass}>
|
|
88
|
-
<FloatingButton bindtap={handleOpenBottomSheet}
|
|
88
|
+
<FloatingButton bindtap={handleOpenBottomSheet}>
|
|
89
89
|
<text className={floatingButtonCss.title}>LynxConsole</text>
|
|
90
90
|
<text className={floatingButtonCss.subtitle}>
|
|
91
91
|
{`${latestFcp?.name ?? "FCP"}: ${latestFcp?.duration ? latestFcp.duration.toFixed(2) : "--"}ms`}
|