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.
@@ -1,5 +1,8 @@
1
1
  import type { ReactNode } from "@lynx-js/react";
2
- import { useLongPressDrag } from "../hooks/useLongPressDrag";
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 = ({ bindtap, children }: FloatingButtonProps) => {
33
+ export const FloatingButton = ({
34
+ bindtap,
35
+ children,
36
+ initialPosition,
37
+ }: FloatingButtonProps) => {
30
38
  const colors = useThemeColors();
31
- const { phase, right, bottom, clearTimer, handlers } =
32
- useLongPressDrag(bindtap);
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
- right: `${right}px`,
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
  };
@@ -1,3 +1,4 @@
1
1
  export { useConsole } from "./useConsole";
2
+ export { useLatestFcp } from "./useLatestFcp";
2
3
  export { useNetwork } from "./useNetwork";
3
4
  export { usePerformance } from "./usePerformance";
@@ -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
- let savedRight = DEFAULT_RIGHT;
11
- let savedBottom = DEFAULT_BOTTOM;
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(onTap: () => void) {
14
- const [right, setRight] = useState(savedRight);
15
- const [bottom, setBottom] = useState(savedBottom);
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 [tempRight, setTempRight] = useState(savedRight);
18
- const [tempBottom, setTempBottom] = useState(savedBottom);
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, r: 0, b: 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
- r: right,
36
- b: bottom,
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
- setTempRight(right);
44
- setTempBottom(bottom);
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
- // right/bottom 기준이므로 방향 반전
62
- setTempRight(startRef.current.r - dx);
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
- setRight(tempRight);
71
- setBottom(tempBottom);
72
- savedRight = tempRight;
73
- savedBottom = tempBottom;
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
- right: isDragging ? tempRight : right,
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 { usePerformance } from "./hooks/usePerformance";
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
- interface FcpMetric {
31
- name: string;
32
- duration: number;
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
- { theme = "light", safeAreaInsetBottom = "50px", customTabs },
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 { performances } = usePerformance();
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 bindtap={handleOpenBottomSheet}>
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 }}