FloatingMobileToolbar.utils.ts 3.0 KB

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