FloatingMobileToolbar.utils.ts109 lines · main
1import type { CSSProperties } from 'react'
2
3export const DRAG_THRESHOLD_PX = 8
4export const GAP_FROM_BOTTOM = 50
5export const SHEET_OPEN_GAP_FRACTION = 0.15
6export const DEFAULT_NAV_HEIGHT = 56
7
8export function isMenuContent(content: unknown): boolean {
9 return content !== null && typeof content !== 'string'
10}
11
12export function shouldShowMenuButton(pathname: string): boolean {
13 return (
14 pathname.startsWith('/project/') ||
15 pathname.startsWith('/org/') ||
16 pathname.startsWith('/account')
17 )
18}
19
20export type Viewport = { width: number; height: number }
21export type NavSize = { width: number; height: number }
22export type Position = { x: number; y: number }
23
24export function clampPosition(
25 current: Position,
26 delta: { dx: number; dy: number },
27 viewport: Viewport,
28 navSize: NavSize
29): Position {
30 const maxX = Math.max(0, viewport.width - navSize.width)
31 const maxY = Math.max(0, viewport.height - navSize.height)
32 return {
33 x: Math.max(0, Math.min(maxX, current.x + delta.dx)),
34 y: Math.max(0, Math.min(maxY, current.y + delta.dy)),
35 }
36}
37
38export type DragStartState = {
39 x: number
40 y: number
41 startX: number
42 startY: number
43}
44
45export function getNextPosition(
46 dragStart: DragStartState,
47 clientX: number,
48 clientY: number,
49 viewport: Viewport,
50 navSize: NavSize,
51 threshold: number = DRAG_THRESHOLD_PX
52): Position | null {
53 const dist = Math.hypot(clientX - dragStart.startX, clientY - dragStart.startY)
54 if (dist < threshold) return null
55 const dx = clientX - dragStart.startX
56 const dy = clientY - dragStart.startY
57 return clampPosition({ x: dragStart.x, y: dragStart.y }, { dx, dy }, viewport, navSize)
58}
59
60export function getToolbarStyle(params: {
61 position: Position | null
62 navSize: NavSize
63 isSheetOpen: boolean
64 viewport: Viewport
65 isDragging: boolean
66}): CSSProperties {
67 const { position, navSize, isSheetOpen, viewport, isDragging } = params
68 const { width: navW, height: navH } = navSize
69 const vw = viewport.width
70 const vh = viewport.height
71 const transition = isDragging
72 ? 'transform 0ms, z-index 0s'
73 : 'transform 300ms cubic-bezier(0.4, 0, 0.2, 1), z-index 0s'
74 const base: CSSProperties = {
75 zIndex: isSheetOpen ? 101 : 41,
76 transition,
77 touchAction: 'none',
78 }
79
80 const centerX = vw > 0 && navW > 0 ? vw / 2 - navW / 2 : 0
81 const sheetOpenGapPx = vh * SHEET_OPEN_GAP_FRACTION
82 const topWhenSheetOpen = vh > 0 ? sheetOpenGapPx / 2 - navH / 2 : 0
83 const defaultYClosed = vh > 0 ? vh - GAP_FROM_BOTTOM - (navH > 0 ? navH : DEFAULT_NAV_HEIGHT) : 0
84
85 if (position === null) {
86 return {
87 ...base,
88 left: '50%',
89 top: 0,
90 transform: isSheetOpen
91 ? `translate(-50%, ${topWhenSheetOpen}px)`
92 : `translate(-50%, ${defaultYClosed}px)`,
93 }
94 }
95 if (isSheetOpen) {
96 return {
97 ...base,
98 left: 0,
99 top: 0,
100 transform: `translate(${centerX}px, ${topWhenSheetOpen}px)`,
101 }
102 }
103 return {
104 ...base,
105 left: 0,
106 top: 0,
107 transform: `translate(${position.x}px, ${position.y}px)`,
108 }
109}