11import { useUrlParam } from '@rdub/use-url-params'
2- import React , { useState , useMemo , useCallback , useEffect , useRef } from 'react'
2+ import React , { useState , useMemo , useCallback , useEffect , useRef , type MutableRefObject } from 'react'
33import Plot from 'react-plotly.js'
44import { ChartControls , metricConfig , getRangeFloor } from './ChartControls'
55import { CustomLegend } from './CustomLegend'
66import { DataTable } from './DataTable'
7+ import { SequenceModal } from './SequenceModal'
78import { TIME_WINDOWS , getWindowForDuration } from '../hooks/useDataAggregation'
89import { useKeyboardShortcuts } from '../hooks/useKeyboardShortcuts'
910import { useLatestMode } from '../hooks/useLatestMode'
@@ -47,9 +48,11 @@ interface Props {
4748 setTimeRange : ( range : { timestamp : Date | null ; duration : number } ) => void
4849 isOgMode ?: boolean
4950 onOpenShortcuts ?: ( ) => void
51+ onOpenOmnibar ?: ( ) => void
52+ handlersRef ?: MutableRefObject < Record < string , ( ) => void > >
5053}
5154
52- export const AwairChart = React . memo ( function AwairChart ( { deviceDataResults, summary, devices, selectedDeviceIds, onDeviceSelectionChange, timeRange : timeRangeFromProps , setTimeRange : setTimeRangeFromProps , isOgMode = false , onOpenShortcuts } : Props ) {
55+ export const AwairChart = React . memo ( function AwairChart ( { deviceDataResults, summary, devices, selectedDeviceIds, onDeviceSelectionChange, timeRange : timeRangeFromProps , setTimeRange : setTimeRangeFromProps , isOgMode = false , onOpenShortcuts, onOpenOmnibar , handlersRef } : Props ) {
5356
5457 // Combine data from all devices for time range calculations and bounds checking
5558 // Sorted newest-first for efficient latest record access
@@ -96,6 +99,7 @@ export const AwairChart = React.memo(function AwairChart({ deviceDataResults, su
9699
97100 // Refs for handling programmatic updates
98101 const ignoreNextRelayoutRef = useRef ( false )
102+ const plotContainerRef = useRef < HTMLDivElement > ( null )
99103
100104 // Compute window size for buffer calculation (before useTimeRangeParam)
101105 const windowMinutes = useMemo ( ( ) => {
@@ -284,7 +288,7 @@ export const AwairChart = React.memo(function AwairChart({ deviceDataResults, su
284288 } , [ setXAxisRange , getAllDeviceBounds , formatForPlotly ] )
285289
286290 // Register keyboard shortcuts (keymap managed by context, handlers defined here)
287- const { pendingKeys, isAwaitingSequence, timeoutStartedAt, sequenceTimeout } = useKeyboardShortcuts ( {
291+ const { pendingKeys, isAwaitingSequence, timeoutStartedAt, sequenceTimeout, handlers } = useKeyboardShortcuts ( {
288292 metrics,
289293 xAxisRange,
290294 setXAxisRange,
@@ -296,6 +300,7 @@ export const AwairChart = React.memo(function AwairChart({ deviceDataResults, su
296300 handleAllClick,
297301 setIgnoreNextPanCheck,
298302 openShortcutsModal : onOpenShortcuts || noop ,
303+ openOmnibar : onOpenOmnibar || noop ,
299304 devices,
300305 selectedDeviceIds,
301306 setSelectedDeviceIds : onDeviceSelectionChange ,
@@ -307,6 +312,13 @@ export const AwairChart = React.memo(function AwairChart({ deviceDataResults, su
307312 tableLastPage : tableNavHandlers ?. lastPage ,
308313 } )
309314
315+ // Update handlersRef for omnibar to use
316+ useEffect ( ( ) => {
317+ if ( handlersRef ) {
318+ handlersRef . current = handlers
319+ }
320+ } , [ handlersRef , handlers ] )
321+
310322 // Handle responsive plot height and viewport width using matchMedia
311323 useEffect ( ( ) => {
312324 const mobileQuery = window . matchMedia ( '(max-width: 767px) or (max-height: 599px)' )
@@ -330,6 +342,23 @@ export const AwairChart = React.memo(function AwairChart({ deviceDataResults, su
330342 }
331343 } , [ ] )
332344
345+ // Gate scroll zoom behind meta+scroll to avoid accidental zooms while scrolling page
346+ useEffect ( ( ) => {
347+ const container = plotContainerRef . current
348+ if ( ! container ) return
349+
350+ const handleWheel = ( e : WheelEvent ) => {
351+ // Only allow scroll zoom if meta key is pressed
352+ if ( ! e . metaKey && ! e . ctrlKey ) {
353+ e . stopPropagation ( )
354+ }
355+ }
356+
357+ // Use capture phase to intercept before plotly sees it
358+ container . addEventListener ( 'wheel' , handleWheel , { capture : true } )
359+ return ( ) => container . removeEventListener ( 'wheel' , handleWheel , { capture : true } )
360+ } , [ ] )
361+
333362 // Theme-aware plot colors
334363 const computePlotColors = useCallback ( ( ) => {
335364 const isDark = document . documentElement . getAttribute ( 'data-theme' ) === 'dark'
@@ -685,36 +714,15 @@ export const AwairChart = React.memo(function AwairChart({ deviceDataResults, su
685714 }
686715 } , [ selectedDeviceIdForTable , selectedWindow , deviceDataResults ] )
687716
688- // Format pending keys for display
689- const formatPendingKeys = ( ) => {
690- return pendingKeys . map ( combo => {
691- let key = combo . key . toUpperCase ( )
692- if ( combo . modifiers . shift ) key = `⇧${ key } `
693- if ( combo . modifiers . ctrl ) key = `⌃${ key } `
694- if ( combo . modifiers . alt ) key = `⌥${ key } `
695- if ( combo . modifiers . meta ) key = `⌘${ key } `
696- return key
697- } ) . join ( ' ' )
698- }
699-
700717 return (
701718 < div className = { `awair-chart${ isOgMode ? ' og-mode' : '' } ` } >
702- { /* Sequence indicator */ }
703- { isAwaitingSequence && pendingKeys . length > 0 && (
704- < div className = "sequence-indicator" >
705- < kbd > { formatPendingKeys ( ) } </ kbd >
706- < span className = "sequence-waiting" > …</ span >
707- { timeoutStartedAt && (
708- < div
709- className = "sequence-timeout-bar"
710- style = { {
711- animationDuration : `${ sequenceTimeout } ms` ,
712- } }
713- key = { timeoutStartedAt }
714- />
715- ) }
716- </ div >
717- ) }
719+ { /* Sequence modal (omnibar-style with completions) */ }
720+ < SequenceModal
721+ pendingKeys = { pendingKeys }
722+ isAwaitingSequence = { isAwaitingSequence }
723+ timeoutStartedAt = { timeoutStartedAt }
724+ sequenceTimeout = { sequenceTimeout }
725+ />
718726 { /* OG mode title overlay */ }
719727 { isOgMode && (
720728 < div className = "og-title" style = { { color : plotColors . textColor } } >
@@ -723,6 +731,7 @@ export const AwairChart = React.memo(function AwairChart({ deviceDataResults, su
723731 </ div >
724732 ) }
725733 < div
734+ ref = { plotContainerRef }
726735 className = "plot-container"
727736 onMouseEnter = { ( ) => setHoverState ( null ) }
728737 >
0 commit comments