Newer
Older
navi-1 / webclient / src / components / artifacts / composables / useDrawerWidth.js
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'

/**
 * The docked panel's width: the persisted preference, the viewport clamp, and
 * the pointer drag that resizes it. Below the desktop breakpoint the panel is
 * a full-screen drawer and none of this applies.
 */

const DRAWER_DESKTOP_BREAKPOINT = 980
const CHAT_MIN_WIDTH = 500
const DRAWER_WIDTH_STORAGE_KEY = 'navi:artifacts-drawer-width'

export function useDrawerWidth() {
  const isResizing = ref(false)
  const viewportWidth = ref(typeof window === 'undefined' ? 0 : window.innerWidth)

  const drawerWidth = ref(readSavedDrawerWidth())

  const isDesktop = computed(() => viewportWidth.value > DRAWER_DESKTOP_BREAKPOINT)
  const panelStyle = computed(() => {
    if (!isDesktop.value) return {}
    return { '--artifacts-width': `${currentDrawerWidth()}px` }
  })

  function minDrawerWidth() {
    return Math.round(Math.min(600, viewportWidth.value * 0.42))
  }

  function maxDrawerWidth() {
    return Math.max(minDrawerWidth(), viewportWidth.value - CHAT_MIN_WIDTH)
  }

  function clampDrawerWidth(width) {
    return Math.min(Math.max(Math.round(width), minDrawerWidth()), maxDrawerWidth())
  }

  function currentDrawerWidth() {
    return clampDrawerWidth(drawerWidth.value ?? minDrawerWidth())
  }

  function syncViewportWidth() {
    viewportWidth.value = window.innerWidth
    if (drawerWidth.value !== null) drawerWidth.value = currentDrawerWidth()
  }

  function startResize(event) {
    if (!isDesktop.value) return
    event.preventDefault()
    isResizing.value = true
    document.body.style.cursor = 'col-resize'
    document.body.style.userSelect = 'none'
    window.addEventListener('pointermove', resizeDrawer)
    window.addEventListener('pointerup', stopResize, { once: true })
  }

  function resizeDrawer(event) {
    drawerWidth.value = clampDrawerWidth(viewportWidth.value - event.clientX)
  }

  function stopResize() {
    isResizing.value = false
    saveDrawerWidth()
    document.body.style.cursor = ''
    document.body.style.userSelect = ''
    window.removeEventListener('pointermove', resizeDrawer)
    window.removeEventListener('pointerup', stopResize)
  }

  function readSavedDrawerWidth() {
    if (typeof window === 'undefined') return null
    try {
      const value = Number(window.localStorage.getItem(DRAWER_WIDTH_STORAGE_KEY))
      return Number.isFinite(value) && value > 0 ? value : null
    } catch {
      return null
    }
  }

  function saveDrawerWidth() {
    if (typeof window === 'undefined' || drawerWidth.value === null) return
    try {
      window.localStorage.setItem(DRAWER_WIDTH_STORAGE_KEY, String(currentDrawerWidth()))
    } catch {
      // Storage may be unavailable in restricted browser contexts.
    }
  }

  onMounted(() => {
    window.addEventListener('resize', syncViewportWidth)
  })

  onBeforeUnmount(() => {
    window.removeEventListener('resize', syncViewportWidth)
    window.removeEventListener('pointermove', resizeDrawer)
    window.removeEventListener('pointerup', stopResize)
    if (isResizing.value) stopResize()
  })

  return { isResizing, isDesktop, panelStyle, startResize }
}