lynx-console 0.4.0 → 0.6.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.
@@ -1,5 +1,6 @@
1
1
  import { type ReactNode, useEffect, useState } from "@lynx-js/react";
2
2
  import type { BaseTouchEvent, Target } from "@lynx-js/types";
3
+ import { useKeyboardHeight } from "../hooks/useKeyboardHeight";
3
4
  import { useThemeColors } from "../styles/ThemeContext";
4
5
  import { duration, fontWeight } from "../styles/theme";
5
6
  import "./BottomSheet.css";
@@ -41,6 +42,7 @@ export default function BottomSheet({
41
42
  );
42
43
  const [isOpening, setIsOpening] = useState(true);
43
44
  const [isClosing, setIsClosing] = useState(false);
45
+ const keyboardHeight = useKeyboardHeight();
44
46
 
45
47
  // 닫기 애니메이션 처리
46
48
  const handleClose = () => {
@@ -114,12 +116,21 @@ export default function BottomSheet({
114
116
  catchtap={() => {}}
115
117
  style={{
116
118
  background: colors.bg.layerFloating,
117
- height: `${isDragging ? tempHeight : sheetHeight}px`,
119
+ height: `${
120
+ keyboardHeight > 0
121
+ ? Math.min(
122
+ MAX_HEIGHT,
123
+ (isDragging ? tempHeight : sheetHeight) + keyboardHeight,
124
+ )
125
+ : isDragging
126
+ ? tempHeight
127
+ : sheetHeight
128
+ }px`,
118
129
  transform:
119
130
  isOpening || isClosing ? "translateY(100%)" : "translateY(0)",
120
131
  transition: isDragging
121
132
  ? "none"
122
- : `transform ${duration.d6} cubic-bezier(0.4, 0, 0.2, 1)`,
133
+ : `transform ${duration.d6} cubic-bezier(0.4, 0, 0.2, 1), height ${duration.d6} cubic-bezier(0.4, 0, 0.2, 1)`,
123
134
  }}
124
135
  >
125
136
  {/* catchtap: 이벤트 버블링 차단 */}
@@ -150,7 +161,10 @@ export default function BottomSheet({
150
161
  <view
151
162
  className="bs-body"
152
163
  style={{
153
- paddingBottom: safeAreaInsetBottom,
164
+ paddingBottom:
165
+ keyboardHeight > 0
166
+ ? `${keyboardHeight}px`
167
+ : safeAreaInsetBottom,
154
168
  }}
155
169
  >
156
170
  {children}
@@ -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 {
@@ -48,8 +58,7 @@ export const FloatingButton = ({ bindtap, children }: FloatingButtonProps) => {
48
58
  className={"fb-wrapper"}
49
59
  consume-slide-event={[[-180, 180]]}
50
60
  style={{
51
- right: `${right}px`,
52
- bottom: `${bottom}px`,
61
+ ...positionStyle,
53
62
  transform: isDragging ? "scale(1.05)" : "scale(1)",
54
63
  transition: `transform ${duration.d4} cubic-bezier(0.4, 0, 0.2, 1)`,
55
64
  }}
@@ -0,0 +1,14 @@
1
+ import { useLynxGlobalEventListener, useState } from "@lynx-js/react";
2
+
3
+ export function useKeyboardHeight() {
4
+ const [keyboardHeight, setKeyboardHeight] = useState(0);
5
+
6
+ useLynxGlobalEventListener(
7
+ "keyboardstatuschanged",
8
+ (status: "on" | "off", height: number) => {
9
+ setKeyboardHeight(status === "on" ? height : 0);
10
+ },
11
+ );
12
+
13
+ return keyboardHeight;
14
+ }
@@ -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
@@ -25,6 +25,12 @@ export interface LynxConsoleProps {
25
25
  theme?: "light" | "dark";
26
26
  safeAreaInsetBottom?: string;
27
27
  customTabs?: CustomTab[];
28
+ initialPosition?: {
29
+ top?: number;
30
+ left?: number;
31
+ right?: number;
32
+ bottom?: number;
33
+ };
28
34
  }
29
35
 
30
36
  interface FcpMetric {
@@ -40,7 +46,12 @@ interface MetricFcpEntry {
40
46
 
41
47
  const LynxConsole = forwardRef<LynxConsoleHandle, LynxConsoleProps>(
42
48
  (
43
- { theme = "light", safeAreaInsetBottom = "50px", customTabs },
49
+ {
50
+ theme = "light",
51
+ safeAreaInsetBottom = "50px",
52
+ customTabs,
53
+ initialPosition,
54
+ },
44
55
  ref: ForwardedRef<LynxConsoleHandle>,
45
56
  ) => {
46
57
  const [isOpen, setIsOpen] = useState(false);
@@ -94,7 +105,10 @@ const LynxConsole = forwardRef<LynxConsoleHandle, LynxConsoleProps>(
94
105
  color: colors.fg.neutral,
95
106
  }}
96
107
  >
97
- <FloatingButton bindtap={handleOpenBottomSheet}>
108
+ <FloatingButton
109
+ bindtap={handleOpenBottomSheet}
110
+ initialPosition={initialPosition}
111
+ >
98
112
  <text
99
113
  className="fb-title t4"
100
114
  style={{ fontWeight: "400", color: colors.palette.staticWhite }}