11'use client'
22
3- import { useRef } from 'react'
3+ import { useCallback , useEffect , useRef , useState } from 'react'
44import { COLORS } from '../theme/theme'
5+ import { useIsMobile } from '../../hooks/useIsMobile'
56import styles from './OverlayButtons.module.css'
67
78export type Preset = 'default' | 'droideka'
@@ -30,39 +31,53 @@ export default function OverlayButtons({
3031 onSetPreset
3132} : OverlayButtonsProps ) {
3233 const fileInputRef = useRef < HTMLInputElement > ( null )
34+ const popoverRef = useRef < HTMLDivElement > ( null )
35+ const isMobile = useIsMobile ( )
36+ const [ open , setOpen ] = useState ( false )
3337
3438 const handleFileChange = ( e : React . ChangeEvent < HTMLInputElement > ) => {
3539 const file = e . target . files ?. [ 0 ]
3640 if ( file ) onLoadGlb ( file )
3741 e . target . value = ''
3842 }
3943
40- return (
41- < div
42- className = { styles . container }
43- style = {
44- {
45- '--overlay-bg' : COLORS . bg ,
46- '--overlay-surface' : COLORS . surface ,
47- '--overlay-border' : COLORS . border ,
48- '--overlay-text' : COLORS . text ,
49- '--overlay-accent' : COLORS . accent
50- } as React . CSSProperties
51- }
52- >
44+ const close = useCallback ( ( ) => setOpen ( false ) , [ ] )
45+
46+ // close popover on outside tap
47+ useEffect ( ( ) => {
48+ if ( ! open || ! isMobile ) return
49+ const handler = ( e : PointerEvent ) => {
50+ if ( popoverRef . current && ! popoverRef . current . contains ( e . target as Node ) ) close ( )
51+ }
52+ document . addEventListener ( 'pointerdown' , handler , true )
53+ return ( ) => document . removeEventListener ( 'pointerdown' , handler , true )
54+ } , [ open , isMobile , close ] )
55+
56+ const cssVars = {
57+ '--overlay-bg' : COLORS . bg ,
58+ '--overlay-surface' : COLORS . surface ,
59+ '--overlay-border' : COLORS . border ,
60+ '--overlay-text' : COLORS . text ,
61+ '--overlay-accent' : COLORS . accent
62+ } as React . CSSProperties
63+
64+ const fileInput = (
65+ < input ref = { fileInputRef } type = "file" accept = ".glb,.gltf" className = { styles . fileInput } onChange = { handleFileChange } />
66+ )
67+
68+ const controls = (
69+ < >
5370 { /* Preset selector */ }
5471 < div className = { styles . presetGroup } >
5572 < button
5673 onClick = { ( ) => onSetPreset ( 'default' ) }
5774 className = { `${ styles . presetBtn } ${ preset === 'default' ? styles . active : styles . inactive } ` }
58- title = "Default preset — sphere shield"
5975 >
6076 Default
6177 </ button >
6278 < button
6379 onClick = { ( ) => onSetPreset ( 'droideka' ) }
6480 className = { `${ styles . presetBtn } ${ preset === 'droideka' ? styles . active : styles . inactive } ` }
65- title = "Droideka preset — Star Wars droid"
6681 >
6782 Droideka
6883 </ button >
@@ -71,11 +86,9 @@ export default function OverlayButtons({
7186 < div className = { styles . separator } />
7287
7388 { /* Load GLB */ }
74- < input ref = { fileInputRef } type = "file" accept = ".glb,.gltf" className = { styles . fileInput } onChange = { handleFileChange } />
7589 < button
7690 onClick = { ( ) => fileInputRef . current ?. click ( ) }
7791 className = { `${ styles . importBtn } ${ hasGlb ? styles . active : '' } ` }
78- title = "Load GLB model"
7992 >
8093 < svg width = "16" height = "16" viewBox = "0 0 16 16" fill = "none" stroke = "currentColor" strokeWidth = "1.2" >
8194 < path d = "M8 2v8M5 7l3 3 3-3" />
@@ -86,7 +99,7 @@ export default function OverlayButtons({
8699
87100 { /* Clear GLB */ }
88101 { hasGlb && (
89- < button onClick = { onClearGlb } className = { styles . btn } title = "Remove model (back to sphere) " >
102+ < button onClick = { onClearGlb } className = { styles . btn } title = "Remove model" >
90103 < svg width = "16" height = "16" viewBox = "0 0 16 16" fill = "none" stroke = "currentColor" strokeWidth = "1.2" >
91104 < line x1 = "4" y1 = "4" x2 = "12" y2 = "12" />
92105 < line x1 = "12" y1 = "4" x2 = "4" y2 = "12" />
@@ -98,7 +111,6 @@ export default function OverlayButtons({
98111 < button
99112 onClick = { onToggleGrid }
100113 className = { `${ styles . btn } ${ showGrid ? styles . active : styles . inactive } ` }
101- title = { showGrid ? 'Hide Grid' : 'Show Grid' }
102114 >
103115 < svg width = "16" height = "16" viewBox = "0 0 16 16" fill = "none" stroke = "currentColor" strokeWidth = "1.2" >
104116 < rect x = "1" y = "1" width = "6" height = "6" />
@@ -112,7 +124,6 @@ export default function OverlayButtons({
112124 < button
113125 onClick = { onToggleLeva }
114126 className = { `${ styles . btn } ${ ! hideLeva ? styles . active : styles . inactive } ` }
115- title = { hideLeva ? 'Show Controls' : 'Hide Controls' }
116127 >
117128 < svg width = "16" height = "16" viewBox = "0 0 16 16" fill = "none" stroke = "currentColor" strokeWidth = "1.2" >
118129 < line x1 = "2" y1 = "4" x2 = "14" y2 = "4" />
@@ -123,6 +134,54 @@ export default function OverlayButtons({
123134 < circle cx = "9" cy = "12" r = "1.5" fill = "currentColor" />
124135 </ svg >
125136 </ button >
137+ </ >
138+ )
139+
140+ /* ── Desktop: inline strip (unchanged) ─────────────────────────── */
141+ if ( ! isMobile ) {
142+ return (
143+ < div className = { styles . container } style = { cssVars } >
144+ { fileInput }
145+ { controls }
146+ </ div >
147+ )
148+ }
149+
150+ /* ── Mobile: FAB + popover ─────────────────────────────────────── */
151+ return (
152+ < div ref = { popoverRef } className = { styles . mobileRoot } style = { cssVars } >
153+ { fileInput }
154+
155+ { /* Popover panel */ }
156+ < div className = { `${ styles . popover } ${ open ? styles . popoverOpen : '' } ` } >
157+ { controls }
158+ </ div >
159+
160+ { /* FAB trigger */ }
161+ < button className = { styles . fab } onClick = { ( ) => setOpen ( ( v ) => ! v ) } aria-label = "Options" >
162+ < svg
163+ width = "20"
164+ height = "20"
165+ viewBox = "0 0 20 20"
166+ fill = "none"
167+ stroke = "currentColor"
168+ strokeWidth = "1.4"
169+ className = { `${ styles . fabIcon } ${ open ? styles . fabIconOpen : '' } ` }
170+ >
171+ { open ? (
172+ < >
173+ < line x1 = "5" y1 = "5" x2 = "15" y2 = "15" />
174+ < line x1 = "15" y1 = "5" x2 = "5" y2 = "15" />
175+ </ >
176+ ) : (
177+ < >
178+ < circle cx = "10" cy = "4" r = "1.5" fill = "currentColor" />
179+ < circle cx = "10" cy = "10" r = "1.5" fill = "currentColor" />
180+ < circle cx = "10" cy = "16" r = "1.5" fill = "currentColor" />
181+ </ >
182+ ) }
183+ </ svg >
184+ </ button >
126185 </ div >
127186 )
128187}
0 commit comments