glb-studio / src /components /SkyboxPanel.jsx
GLB Studio Deploy
redesign: complete modern UI overhaul + new features
fd8bcfa
import { useRef, useState } from 'react'
import useStore from '../store/useStore'
const PRESETS = [
{ id:'studio', label:'Studio', icon:'◎' },
{ id:'outdoor', label:'Outdoor', icon:'◉' },
{ id:'dramatic', label:'Dramatic', icon:'◈' },
{ id:'neon', label:'Neon', icon:'◆' },
]
const COLORS = ['#0c0c10','#000000','#0a0a1a','#0d1a0a','#1a0a0a','#ffffff','#87ceeb','#1a0a2e']
export default function SkyboxPanel() {
const skybox = useStore(s => s.skybox)
const lightingPreset = useStore(s => s.lightingPreset)
const { setSkybox, setLightingPreset } = useStore.getState()
const [url, setUrl] = useState('')
const [urlType, setUrlType] = useState('image')
const fileRef = useRef()
const handleFile = (e) => {
const f = e.target.files[0]; if(!f) return
const isHdr = /\.(hdr|exr)$/i.test(f.name)
setSkybox({ type: isHdr?'hdr':'image', value: URL.createObjectURL(f), showBg:true })
}
const applyUrl = () => {
if(!url.trim()) return
const isHdr = /\.(hdr|exr)/i.test(url)
setSkybox({ type:isHdr?'hdr':'image', value:url.trim(), showBg:true })
setUrl('')
}
return (
<div style={{ padding:12, display:'flex', flexDirection:'column', gap:12 }}>
{/* Show background toggle */}
<div style={{ display:'flex', alignItems:'center', justifyContent:'space-between',
padding:'8px 10px', background:'var(--bg2)', borderRadius:'var(--radius)',
border:'1px solid var(--border)' }}>
<div>
<div style={{ fontSize:12, fontWeight:600, color:'var(--text0)' }}>Show Background</div>
<div style={{ fontSize:10, color:'var(--text2)', marginTop:2 }}>
{skybox.type==='preset' ? `Preset — ${lightingPreset}` :
skybox.type==='color' ? `Color — ${skybox.bgColor}` :
skybox.type==='image' ? 'Custom Image' : 'HDR'}
</div>
</div>
<button onClick={() => setSkybox({ showBg:!skybox.showBg })} style={{
width:40, height:22, borderRadius:11,
background: skybox.showBg ? 'var(--accent)' : 'var(--bg4)',
border:'none', cursor:'pointer', position:'relative', transition:'background 0.2s',
boxShadow: skybox.showBg ? '0 0 8px rgba(79,142,255,0.4)' : 'none',
}}>
<div style={{
position:'absolute', top:3, left: skybox.showBg ? 20 : 3,
width:16, height:16, borderRadius:8, background:'#fff',
transition:'left 0.2s', boxShadow:'0 1px 3px rgba(0,0,0,0.4)',
}}/>
</button>
</div>
{/* Preset environments */}
<div>
<div style={{ fontSize:10, color:'var(--text2)', fontWeight:600, letterSpacing:'0.08em',
marginBottom:6, textTransform:'uppercase' }}>Preset Environments</div>
<div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:5 }}>
{PRESETS.map(p => (
<button key={p.id}
onClick={() => { setLightingPreset(p.id); setSkybox({ type:'preset', value:null }) }}
style={{
padding:'9px 8px', borderRadius:'var(--radius-sm)',
background: skybox.type==='preset' && lightingPreset===p.id ? 'rgba(79,142,255,0.12)' : 'var(--bg2)',
border:`1px solid ${skybox.type==='preset' && lightingPreset===p.id ? 'rgba(79,142,255,0.3)' : 'var(--border)'}`,
color: skybox.type==='preset' && lightingPreset===p.id ? 'var(--accent)' : 'var(--text1)',
fontSize:12, cursor:'pointer', textAlign:'left', transition:'all 0.12s',
display:'flex', alignItems:'center', gap:6,
}}
><span style={{ opacity:0.7 }}>{p.icon}</span> {p.label}</button>
))}
</div>
</div>
{/* Solid color */}
<div>
<div style={{ fontSize:10, color:'var(--text2)', fontWeight:600, letterSpacing:'0.08em',
marginBottom:6, textTransform:'uppercase' }}>Solid Color</div>
<div style={{ display:'flex', flexWrap:'wrap', gap:6 }}>
{COLORS.map(col => (
<div key={col} onClick={() => setSkybox({ type:'color', bgColor:col, showBg:true })}
style={{
width:30, height:30, borderRadius:'var(--radius-sm)',
background:col, cursor:'pointer', transition:'transform 0.1s',
border:`2px solid ${skybox.type==='color'&&skybox.bgColor===col ? 'var(--accent)' : 'rgba(255,255,255,0.1)'}`,
boxShadow: skybox.type==='color'&&skybox.bgColor===col ? '0 0 8px rgba(79,142,255,0.5)' : 'none',
}}
onMouseEnter={e=>e.currentTarget.style.transform='scale(1.1)'}
onMouseLeave={e=>e.currentTarget.style.transform='scale(1)'}
/>
))}
{/* Color picker */}
<label style={{ width:30, height:30, borderRadius:'var(--radius-sm)', cursor:'pointer',
border:'1px dashed var(--border-hi)', display:'flex', alignItems:'center', justifyContent:'center',
color:'var(--text2)', fontSize:18, overflow:'hidden' }}>
+
<input type="color" onChange={e=>setSkybox({type:'color',bgColor:e.target.value,showBg:true})}
style={{ position:'absolute', opacity:0, width:0, height:0 }} />
</label>
</div>
</div>
{/* Upload */}
<div>
<div style={{ fontSize:10, color:'var(--text2)', fontWeight:600, letterSpacing:'0.08em',
marginBottom:6, textTransform:'uppercase' }}>Upload Skybox</div>
<button onClick={() => fileRef.current?.click()} style={{
width:'100%', padding:'10px', borderRadius:'var(--radius)',
background:'var(--bg2)', border:'2px dashed var(--border-hi)',
color:'var(--text1)', fontSize:12, cursor:'pointer', transition:'all 0.15s',
}}
onMouseEnter={e=>{e.currentTarget.style.borderColor='var(--accent)';e.currentTarget.style.color='var(--text0)'}}
onMouseLeave={e=>{e.currentTarget.style.borderColor='var(--border-hi)';e.currentTarget.style.color='var(--text1)'}}
>📁 Upload Image / HDR / EXR</button>
<input ref={fileRef} type="file" accept=".jpg,.jpeg,.png,.hdr,.exr" style={{display:'none'}} onChange={handleFile} />
<div style={{ fontSize:10, color:'var(--text3)', marginTop:4 }}>
JPG/PNG equirectangular · HDR · EXR
</div>
</div>
{/* URL */}
<div>
<div style={{ fontSize:10, color:'var(--text2)', fontWeight:600, letterSpacing:'0.08em',
marginBottom:6, textTransform:'uppercase' }}>Paste URL</div>
<div style={{ display:'flex', gap:5, marginBottom:4 }}>
{['image','hdr'].map(t=>(
<button key={t} onClick={()=>setUrlType(t)} style={{
flex:1, padding:'4px 0', borderRadius:'var(--radius-sm)',
background:urlType===t?'rgba(79,142,255,0.12)':'var(--bg2)',
border:`1px solid ${urlType===t?'rgba(79,142,255,0.3)':'var(--border)'}`,
color:urlType===t?'var(--accent)':'var(--text1)',
fontSize:10, fontWeight:600, cursor:'pointer',
}}>{t.toUpperCase()}</button>
))}
</div>
<div style={{ display:'flex', gap:5 }}>
<input value={url} onChange={e=>setUrl(e.target.value)}
onKeyDown={e=>e.key==='Enter'&&applyUrl()}
placeholder="https://…equirectangular.jpg" style={{ flex:1 }} />
<button onClick={applyUrl} style={{
padding:'5px 10px', borderRadius:'var(--radius-sm)',
background:'var(--accent)', border:'none', color:'#fff',
fontSize:11, fontWeight:600, cursor:'pointer', flexShrink:0,
}}>Apply</button>
</div>
</div>
{/* Free HDR link */}
<div style={{ padding:'10px', background:'var(--bg2)', borderRadius:'var(--radius)',
border:'1px solid var(--border)', fontSize:11, color:'var(--text2)', lineHeight:1.6 }}>
💡 Free HDRIs at{' '}
<a href="https://polyhaven.com/hdris" target="_blank"
style={{ color:'var(--accent)', textDecoration:'none' }}>polyhaven.com ↗</a>
<br/>Right-click 1K JPEG → Copy image address → paste above
</div>
{/* Clear */}
{(skybox.type==='image'||skybox.type==='hdr') && (
<button onClick={() => setSkybox({type:'preset',value:null,showBg:false,bgColor:'#0c0c10'})} style={{
padding:'7px 0', borderRadius:'var(--radius-sm)',
background:'rgba(239,68,68,0.08)', border:'1px solid rgba(239,68,68,0.2)',
color:'var(--danger)', fontSize:11, cursor:'pointer',
}}>✕ Clear Custom Skybox</button>
)}
</div>
)
}