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.
- package/README.md +1 -0
- package/dist/index.cjs +68 -30
- 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 +69 -31
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/BottomSheet.tsx +17 -3
- package/src/components/FloatingButton.tsx +15 -6
- package/src/hooks/useKeyboardHeight.ts +14 -0
- package/src/hooks/useLongPressDrag.ts +97 -21
- package/src/index.tsx +16 -2
|
@@ -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: `${
|
|
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:
|
|
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 {
|
|
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 {
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
@@ -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
|
-
{
|
|
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
|
|
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 }}
|