lynx-console 0.5.0 → 0.6.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 +1 -0
- package/dist/index.cjs +87 -40
- package/dist/index.d.cts +6 -0
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +6 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +87 -40
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/FloatingButton.tsx +15 -7
- package/src/components/LogPanel.tsx +2 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useLatestFcp.ts +48 -0
- package/src/hooks/useLongPressDrag.ts +97 -21
- package/src/index.tsx +18 -32
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { ReactNode } from "@lynx-js/react";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
type InitialPosition,
|
|
4
|
+
useLongPressDrag,
|
|
5
|
+
} from "../hooks/useLongPressDrag";
|
|
3
6
|
import { useThemeColors } from "../styles/ThemeContext";
|
|
4
7
|
import { duration } from "../styles/theme";
|
|
5
8
|
import "./FloatingButton.css";
|
|
@@ -7,6 +10,7 @@ import "./FloatingButton.css";
|
|
|
7
10
|
interface FloatingButtonProps {
|
|
8
11
|
bindtap: () => void;
|
|
9
12
|
children: ReactNode;
|
|
13
|
+
initialPosition?: InitialPosition;
|
|
10
14
|
}
|
|
11
15
|
|
|
12
16
|
const SHINE_STYLES = {
|
|
@@ -26,10 +30,16 @@ const SHINE_STYLES = {
|
|
|
26
30
|
},
|
|
27
31
|
} as const;
|
|
28
32
|
|
|
29
|
-
export const FloatingButton = ({
|
|
33
|
+
export const FloatingButton = ({
|
|
34
|
+
bindtap,
|
|
35
|
+
children,
|
|
36
|
+
initialPosition,
|
|
37
|
+
}: FloatingButtonProps) => {
|
|
30
38
|
const colors = useThemeColors();
|
|
31
|
-
const { phase,
|
|
32
|
-
|
|
39
|
+
const { phase, positionStyle, clearTimer, handlers } = useLongPressDrag(
|
|
40
|
+
bindtap,
|
|
41
|
+
{ initialPosition },
|
|
42
|
+
);
|
|
33
43
|
|
|
34
44
|
const handleReload = () => {
|
|
35
45
|
try {
|
|
@@ -46,10 +56,8 @@ export const FloatingButton = ({ bindtap, children }: FloatingButtonProps) => {
|
|
|
46
56
|
return (
|
|
47
57
|
<view
|
|
48
58
|
className={"fb-wrapper"}
|
|
49
|
-
consume-slide-event={[[-180, 180]]}
|
|
50
59
|
style={{
|
|
51
|
-
|
|
52
|
-
bottom: `${bottom}px`,
|
|
60
|
+
...positionStyle,
|
|
53
61
|
transform: isDragging ? "scale(1.05)" : "scale(1)",
|
|
54
62
|
transition: `transform ${duration.d4} cubic-bezier(0.4, 0, 0.2, 1)`,
|
|
55
63
|
}}
|
|
@@ -154,6 +154,8 @@ export const LogPanel = ({ logs, clearLogs }: LogPanelProps) => {
|
|
|
154
154
|
?.invoke({
|
|
155
155
|
method: "scrollToPosition",
|
|
156
156
|
params: { position: logsRef.current.length - 1, smooth },
|
|
157
|
+
// 연속 로그로 진행 중이던 smooth 스크롤이 중단될 때 나는 무해한 경고를 무시
|
|
158
|
+
fail: () => {},
|
|
157
159
|
})
|
|
158
160
|
.exec();
|
|
159
161
|
};
|
package/src/hooks/index.ts
CHANGED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { useEffect, useState } from "@lynx-js/react";
|
|
2
|
+
import type { PerformanceEntryData } from "../types";
|
|
3
|
+
|
|
4
|
+
interface FcpMetric {
|
|
5
|
+
name: string;
|
|
6
|
+
duration: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface MetricFcpRawEntry {
|
|
10
|
+
totalFcp?: FcpMetric;
|
|
11
|
+
lynxFcp?: FcpMetric;
|
|
12
|
+
fcp?: FcpMetric;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const pickFcp = (entry: PerformanceEntryData): FcpMetric | undefined => {
|
|
16
|
+
if (entry.entryType !== "metric" || entry.name !== "fcp") return undefined;
|
|
17
|
+
const raw = entry.rawEntry as MetricFcpRawEntry | undefined;
|
|
18
|
+
if (raw?.totalFcp?.duration !== undefined) return raw.totalFcp;
|
|
19
|
+
if (raw?.lynxFcp?.duration !== undefined) return raw.lynxFcp;
|
|
20
|
+
return undefined;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const useLatestFcp = (): FcpMetric | undefined => {
|
|
24
|
+
const [fcp, setFcp] = useState<FcpMetric | undefined>(() => {
|
|
25
|
+
const performances = globalThis.__LYNX_CONSOLE__?.state?.performances ?? [];
|
|
26
|
+
for (let i = performances.length - 1; i >= 0; i--) {
|
|
27
|
+
const entry = performances[i];
|
|
28
|
+
if (!entry) continue;
|
|
29
|
+
const found = pickFcp(entry);
|
|
30
|
+
if (found) return found;
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
const state = globalThis.__LYNX_CONSOLE__?.state;
|
|
37
|
+
if (!state?.subscribePerformance) return;
|
|
38
|
+
|
|
39
|
+
const unsubscribe = state.subscribePerformance((entry) => {
|
|
40
|
+
const found = pickFcp(entry);
|
|
41
|
+
if (found) setFcp(found);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return unsubscribe;
|
|
45
|
+
}, []);
|
|
46
|
+
|
|
47
|
+
return fcp;
|
|
48
|
+
};
|
|
@@ -7,19 +7,86 @@ const MOVE_THRESHOLD = 5;
|
|
|
7
7
|
const DEFAULT_RIGHT = 16;
|
|
8
8
|
const DEFAULT_BOTTOM = 84;
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
type VerticalAxis = "top" | "bottom";
|
|
11
|
+
type HorizontalAxis = "left" | "right";
|
|
12
|
+
|
|
13
|
+
export interface InitialPosition {
|
|
14
|
+
top?: number;
|
|
15
|
+
left?: number;
|
|
16
|
+
right?: number;
|
|
17
|
+
bottom?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface ResolvedAnchors {
|
|
21
|
+
vertical: VerticalAxis;
|
|
22
|
+
horizontal: HorizontalAxis;
|
|
23
|
+
x: number;
|
|
24
|
+
y: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function resolveAnchors(initial?: InitialPosition): ResolvedAnchors {
|
|
28
|
+
// top/left이 명시되면 그것이 anchor가 돼요. 둘 다 명시되면 top/left가 이겨요.
|
|
29
|
+
const vertical: VerticalAxis = initial?.top !== undefined ? "top" : "bottom";
|
|
30
|
+
const horizontal: HorizontalAxis =
|
|
31
|
+
initial?.left !== undefined ? "left" : "right";
|
|
32
|
+
|
|
33
|
+
const y =
|
|
34
|
+
vertical === "top"
|
|
35
|
+
? (initial?.top ?? 0)
|
|
36
|
+
: (initial?.bottom ?? DEFAULT_BOTTOM);
|
|
37
|
+
const x =
|
|
38
|
+
horizontal === "left"
|
|
39
|
+
? (initial?.left ?? 0)
|
|
40
|
+
: (initial?.right ?? DEFAULT_RIGHT);
|
|
41
|
+
|
|
42
|
+
return { vertical, horizontal, x, y };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface SavedState {
|
|
46
|
+
vertical: VerticalAxis;
|
|
47
|
+
horizontal: HorizontalAxis;
|
|
48
|
+
x: number;
|
|
49
|
+
y: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let saved: SavedState | null = null;
|
|
53
|
+
|
|
54
|
+
interface UseLongPressDragOptions {
|
|
55
|
+
initialPosition?: InitialPosition;
|
|
56
|
+
}
|
|
12
57
|
|
|
13
|
-
export function useLongPressDrag(
|
|
14
|
-
|
|
15
|
-
|
|
58
|
+
export function useLongPressDrag(
|
|
59
|
+
onTap: () => void,
|
|
60
|
+
options?: UseLongPressDragOptions,
|
|
61
|
+
) {
|
|
62
|
+
const anchors = resolveAnchors(options?.initialPosition);
|
|
63
|
+
|
|
64
|
+
// 저장된 위치는 anchor 조합이 동일할 때만 복원해요.
|
|
65
|
+
const snapshot = saved;
|
|
66
|
+
let initX = anchors.x;
|
|
67
|
+
let initY = anchors.y;
|
|
68
|
+
if (
|
|
69
|
+
snapshot !== null &&
|
|
70
|
+
snapshot.vertical === anchors.vertical &&
|
|
71
|
+
snapshot.horizontal === anchors.horizontal
|
|
72
|
+
) {
|
|
73
|
+
initX = snapshot.x;
|
|
74
|
+
initY = snapshot.y;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const [x, setX] = useState(initX);
|
|
78
|
+
const [y, setY] = useState(initY);
|
|
16
79
|
const [phase, setPhase] = useState<"idle" | "dragging" | "releasing">("idle");
|
|
17
|
-
const [
|
|
18
|
-
const [
|
|
80
|
+
const [tempX, setTempX] = useState(initX);
|
|
81
|
+
const [tempY, setTempY] = useState(initY);
|
|
19
82
|
|
|
20
83
|
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
21
84
|
const draggingRef = useRef(false);
|
|
22
|
-
const startRef = useRef({ x: 0, y: 0,
|
|
85
|
+
const startRef = useRef({ x: 0, y: 0, ax: 0, ay: 0 });
|
|
86
|
+
|
|
87
|
+
// anchor 방향에 따라 드래그 부호 결정. right/bottom anchor면 드래그 방향과 값 변화가 반대.
|
|
88
|
+
const xSign = anchors.horizontal === "right" ? -1 : 1;
|
|
89
|
+
const ySign = anchors.vertical === "bottom" ? -1 : 1;
|
|
23
90
|
|
|
24
91
|
const clearTimer = () => {
|
|
25
92
|
if (timerRef.current) {
|
|
@@ -32,16 +99,16 @@ export function useLongPressDrag(onTap: () => void) {
|
|
|
32
99
|
startRef.current = {
|
|
33
100
|
x: e.detail.x,
|
|
34
101
|
y: e.detail.y,
|
|
35
|
-
|
|
36
|
-
|
|
102
|
+
ax: x,
|
|
103
|
+
ay: y,
|
|
37
104
|
};
|
|
38
105
|
draggingRef.current = false;
|
|
39
106
|
|
|
40
107
|
timerRef.current = setTimeout(() => {
|
|
41
108
|
draggingRef.current = true;
|
|
42
109
|
setPhase("dragging");
|
|
43
|
-
|
|
44
|
-
|
|
110
|
+
setTempX(x);
|
|
111
|
+
setTempY(y);
|
|
45
112
|
}, LONG_PRESS_DURATION);
|
|
46
113
|
};
|
|
47
114
|
|
|
@@ -58,19 +125,22 @@ export function useLongPressDrag(onTap: () => void) {
|
|
|
58
125
|
|
|
59
126
|
if (!draggingRef.current) return;
|
|
60
127
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
setTempBottom(startRef.current.b - dy);
|
|
128
|
+
setTempX(startRef.current.ax + xSign * dx);
|
|
129
|
+
setTempY(startRef.current.ay + ySign * dy);
|
|
64
130
|
};
|
|
65
131
|
|
|
66
132
|
const handleTouchEnd = () => {
|
|
67
133
|
clearTimer();
|
|
68
134
|
|
|
69
135
|
if (draggingRef.current) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
136
|
+
setX(tempX);
|
|
137
|
+
setY(tempY);
|
|
138
|
+
saved = {
|
|
139
|
+
vertical: anchors.vertical,
|
|
140
|
+
horizontal: anchors.horizontal,
|
|
141
|
+
x: tempX,
|
|
142
|
+
y: tempY,
|
|
143
|
+
};
|
|
74
144
|
setPhase("releasing");
|
|
75
145
|
draggingRef.current = false;
|
|
76
146
|
setTimeout(() => setPhase("idle"), 300);
|
|
@@ -80,11 +150,17 @@ export function useLongPressDrag(onTap: () => void) {
|
|
|
80
150
|
};
|
|
81
151
|
|
|
82
152
|
const isDragging = phase === "dragging";
|
|
153
|
+
const currentX = isDragging ? tempX : x;
|
|
154
|
+
const currentY = isDragging ? tempY : y;
|
|
155
|
+
|
|
156
|
+
const positionStyle = {
|
|
157
|
+
[anchors.horizontal]: `${currentX}px`,
|
|
158
|
+
[anchors.vertical]: `${currentY}px`,
|
|
159
|
+
} as { top?: string; left?: string; right?: string; bottom?: string };
|
|
83
160
|
|
|
84
161
|
return {
|
|
85
162
|
phase,
|
|
86
|
-
|
|
87
|
-
bottom: isDragging ? tempBottom : bottom,
|
|
163
|
+
positionStyle,
|
|
88
164
|
clearTimer,
|
|
89
165
|
handlers: {
|
|
90
166
|
catchtouchstart: handleTouchStart,
|
package/src/index.tsx
CHANGED
|
@@ -10,7 +10,7 @@ import { ConsolePanel } from "./components/ConsolePanel.jsx";
|
|
|
10
10
|
import "./components/FloatingButton.css";
|
|
11
11
|
import "./styles/tokens.css";
|
|
12
12
|
import { FloatingButton } from "./components/FloatingButton.jsx";
|
|
13
|
-
import {
|
|
13
|
+
import { useLatestFcp } from "./hooks/useLatestFcp";
|
|
14
14
|
import { ThemeProvider } from "./styles/ThemeContext";
|
|
15
15
|
import { getColors } from "./styles/theme";
|
|
16
16
|
import type { CustomTab } from "./types";
|
|
@@ -25,46 +25,29 @@ export interface LynxConsoleProps {
|
|
|
25
25
|
theme?: "light" | "dark";
|
|
26
26
|
safeAreaInsetBottom?: string;
|
|
27
27
|
customTabs?: CustomTab[];
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
interface MetricFcpEntry {
|
|
36
|
-
totalFcp?: FcpMetric;
|
|
37
|
-
lynxFcp?: FcpMetric;
|
|
38
|
-
fcp?: FcpMetric;
|
|
28
|
+
initialPosition?: {
|
|
29
|
+
top?: number;
|
|
30
|
+
left?: number;
|
|
31
|
+
right?: number;
|
|
32
|
+
bottom?: number;
|
|
33
|
+
};
|
|
39
34
|
}
|
|
40
35
|
|
|
41
36
|
const LynxConsole = forwardRef<LynxConsoleHandle, LynxConsoleProps>(
|
|
42
37
|
(
|
|
43
|
-
{
|
|
38
|
+
{
|
|
39
|
+
theme = "light",
|
|
40
|
+
safeAreaInsetBottom = "50px",
|
|
41
|
+
customTabs,
|
|
42
|
+
initialPosition,
|
|
43
|
+
},
|
|
44
44
|
ref: ForwardedRef<LynxConsoleHandle>,
|
|
45
45
|
) => {
|
|
46
46
|
const [isOpen, setIsOpen] = useState(false);
|
|
47
47
|
const [shouldClose, setShouldClose] = useState(false);
|
|
48
|
-
const
|
|
48
|
+
const latestFcp = useLatestFcp();
|
|
49
49
|
const colors = useMemo(() => getColors(theme), [theme]);
|
|
50
50
|
|
|
51
|
-
const latestFcp = useMemo(() => {
|
|
52
|
-
for (let i = performances.length - 1; i >= 0; i--) {
|
|
53
|
-
const perf = performances[i];
|
|
54
|
-
if (perf && perf.entryType === "metric" && perf.name === "fcp") {
|
|
55
|
-
const metricEntry = perf.rawEntry as MetricFcpEntry | undefined;
|
|
56
|
-
// totalFcp를 먼저 시도하고, 없으면 lynxFcp 반환
|
|
57
|
-
if (metricEntry?.totalFcp?.duration !== undefined) {
|
|
58
|
-
return metricEntry.totalFcp;
|
|
59
|
-
}
|
|
60
|
-
if (metricEntry?.lynxFcp?.duration !== undefined) {
|
|
61
|
-
return metricEntry.lynxFcp;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return undefined;
|
|
66
|
-
}, [performances]);
|
|
67
|
-
|
|
68
51
|
useImperativeHandle(ref, () => ({
|
|
69
52
|
open: () => {
|
|
70
53
|
setIsOpen(true);
|
|
@@ -94,7 +77,10 @@ const LynxConsole = forwardRef<LynxConsoleHandle, LynxConsoleProps>(
|
|
|
94
77
|
color: colors.fg.neutral,
|
|
95
78
|
}}
|
|
96
79
|
>
|
|
97
|
-
<FloatingButton
|
|
80
|
+
<FloatingButton
|
|
81
|
+
bindtap={handleOpenBottomSheet}
|
|
82
|
+
initialPosition={initialPosition}
|
|
83
|
+
>
|
|
98
84
|
<text
|
|
99
85
|
className="fb-title t4"
|
|
100
86
|
style={{ fontWeight: "400", color: colors.palette.staticWhite }}
|