@@ -6,9 +6,10 @@ import { DialogClose } from "@radix-ui/react-dialog";
66import { IconBraces , IconChartHistogram , IconFileTypeCsv } from "@tabler/icons-react" ;
77import { assertNever } from "assert-never" ;
88import { Maximize2 } from "lucide-react" ;
9- import { useCallback , useState , type ReactNode } from "react" ;
9+ import { useCallback , useEffect , useRef , useState , type ReactNode } from "react" ;
1010import { z } from "zod" ;
1111import { Card } from "~/components/primitives/charts/Card" ;
12+ import { ShortcutKey } from "~/components/primitives/ShortcutKey" ;
1213import { SimpleTooltip } from "~/components/primitives/Tooltip" ;
1314import { cn } from "~/utils/cn" ;
1415import { rowsToCSV , rowsToJSON } from "~/utils/dataExport" ;
@@ -174,10 +175,38 @@ export function QueryWidget({
174175 const [ isMenuOpen , setIsMenuOpen ] = useState ( false ) ;
175176 const [ isRenameDialogOpen , setIsRenameDialogOpen ] = useState ( false ) ;
176177 const [ renameValue , setRenameValue ] = useState ( titleString ?? "" ) ;
178+ const containerRef = useRef < HTMLDivElement > ( null ) ;
177179
178180 const hasEditActions = onEdit || onRename || onDelete || onDuplicate ;
179181 const hasData = props . data . rows . length > 0 ;
180182
183+ // "v" to fullscreen the hovered widget
184+ useEffect ( ( ) => {
185+ const handleKeyDown = ( e : KeyboardEvent ) => {
186+ if ( isRenameDialogOpen || isMenuOpen ) return ;
187+ if ( e . key !== "v" || e . metaKey || e . ctrlKey || e . altKey || e . shiftKey ) return ;
188+
189+ // Ignore when typing in inputs/textareas/contenteditable
190+ const target = e . target as HTMLElement ;
191+ if (
192+ target . tagName === "INPUT" ||
193+ target . tagName === "TEXTAREA" ||
194+ target . tagName === "SELECT" ||
195+ target . isContentEditable
196+ ) {
197+ return ;
198+ }
199+
200+ // When not fullscreen, require hover to activate
201+ if ( ! isFullscreen && ! containerRef . current ?. matches ( ":hover" ) ) return ;
202+
203+ e . preventDefault ( ) ;
204+ setIsFullscreen ( ( prev ) => ! prev ) ;
205+ } ;
206+ document . addEventListener ( "keydown" , handleKeyDown ) ;
207+ return ( ) => document . removeEventListener ( "keydown" , handleKeyDown ) ;
208+ } , [ isFullscreen , isRenameDialogOpen , isMenuOpen ] ) ;
209+
181210 const copyToClipboard = useCallback ( ( text : string ) => {
182211 navigator . clipboard . writeText ( text ) ;
183212 } , [ ] ) ;
@@ -197,7 +226,7 @@ export function QueryWidget({
197226 } , [ props . data , copyToClipboard ] ) ;
198227
199228 return (
200- < div className = "group h-full" >
229+ < div ref = { containerRef } className = "group h-full" >
201230 < Card className = { cn ( "h-full overflow-hidden px-0 pb-0" , className ) } >
202231 < Card . Header draggable = { isDraggable } >
203232 < div className = "flex items-center gap-1.5" > { title } </ div >
@@ -214,7 +243,12 @@ export function QueryWidget({
214243 />
215244 </ span >
216245 }
217- content = "Maximize"
246+ content = {
247+ < span className = "flex items-center gap-1" >
248+ Maximize
249+ < ShortcutKey shortcut = { { key : "v" } } variant = "small/bright" />
250+ </ span >
251+ }
218252 asChild
219253 />
220254 < Popover open = { isMenuOpen } onOpenChange = { setIsMenuOpen } >
0 commit comments