Spaces:
Sleeping
Sleeping
| import { useTwinStore } from '../../lib/twin/store' | |
| import type { LayerKey } from '../../lib/twin/store' | |
| import { BASEMAPS } from '../../lib/twin/basemaps' | |
| import { | |
| Eye, | |
| EyeOff, | |
| Type, | |
| Route, | |
| Square, | |
| Building2, | |
| Droplets, | |
| Trees, | |
| Box, | |
| Layers, | |
| } from 'lucide-react' | |
| // Layer toggle rows shown under the basemap list. 3D rows sit at the bottom. | |
| const LAYER_ROWS: Array<{ key: LayerKey; label: string; icon: React.ReactNode }> = [ | |
| { key: 'label', label: 'Label', icon: <Type className="h-4 w-4" /> }, | |
| { key: 'road', label: 'Road', icon: <Route className="h-4 w-4" /> }, | |
| { key: 'border', label: 'Border', icon: <Square className="h-4 w-4" /> }, | |
| { key: 'building', label: 'Building', icon: <Building2 className="h-4 w-4" /> }, | |
| { key: 'water', label: 'Water', icon: <Droplets className="h-4 w-4" /> }, | |
| { key: 'land', label: 'Land', icon: <Trees className="h-4 w-4" /> }, | |
| { key: 'building3d', label: '3D Building', icon: <Box className="h-4 w-4" /> }, | |
| { key: 'road3d', label: '3D Road', icon: <Layers className="h-4 w-4" /> }, | |
| ] | |
| /** | |
| * Map view of the left panel: choose the basemap style (kepler-style list with | |
| * swatches) and toggle which basemap layers are visible, including extruded 3D | |
| * buildings/roads. All wired to the store; the map applies the changes live. | |
| */ | |
| export function MapPanel() { | |
| const basemapId = useTwinStore((s) => s.basemapId) | |
| const setBasemap = useTwinStore((s) => s.setBasemap) | |
| const layers = useTwinStore((s) => s.layers) | |
| const toggleLayer = useTwinStore((s) => s.toggleLayer) | |
| return ( | |
| <div className="twin-scroll min-h-0 flex-1 overflow-y-auto px-3 pb-4 pt-3"> | |
| {/* Base map styles */} | |
| <SectionLabel>Base map</SectionLabel> | |
| <div className="mt-2 flex flex-col gap-1"> | |
| {BASEMAPS.map((b) => { | |
| const active = b.id === basemapId | |
| return ( | |
| <button | |
| key={b.id} | |
| onClick={() => setBasemap(b.id)} | |
| className="flex items-center gap-3 rounded-lg px-2.5 py-2 text-left text-[13px] transition-colors" | |
| style={{ | |
| background: active ? 'var(--tw-hover)' : 'transparent', | |
| border: `1px solid ${active ? 'var(--tw-border)' : 'transparent'}`, | |
| color: active ? 'var(--tw-text)' : 'var(--tw-muted)', | |
| fontFamily: 'var(--font-display)', | |
| cursor: 'pointer', | |
| }} | |
| onMouseEnter={(e) => { if (!active) e.currentTarget.style.background = 'var(--tw-hover)' }} | |
| onMouseLeave={(e) => { if (!active) e.currentTarget.style.background = 'transparent' }} | |
| > | |
| <span | |
| className="relative inline-block h-9 w-12 flex-none overflow-hidden rounded" | |
| style={{ background: b.swatch, border: '1px solid var(--tw-border)' }} | |
| > | |
| {b.thumb && ( | |
| <img | |
| src={b.thumb} | |
| alt="" | |
| loading="lazy" | |
| className="absolute inset-0 h-full w-full object-cover" | |
| onError={(e) => { (e.currentTarget as HTMLImageElement).style.display = 'none' }} | |
| /> | |
| )} | |
| </span> | |
| {b.label} | |
| </button> | |
| ) | |
| })} | |
| </div> | |
| {/* Layer toggles */} | |
| <SectionLabel className="mt-5">Map layers</SectionLabel> | |
| <div className="mt-2 flex flex-col gap-0.5"> | |
| {LAYER_ROWS.map((row) => { | |
| const on = layers[row.key] | |
| return ( | |
| <button | |
| key={row.key} | |
| onClick={() => toggleLayer(row.key)} | |
| className="flex items-center gap-3 rounded-lg px-2.5 py-2 text-left text-[13px] transition-colors" | |
| style={{ | |
| background: 'transparent', | |
| color: on ? 'var(--tw-text)' : 'var(--tw-muted)', | |
| fontFamily: 'var(--font-display)', | |
| cursor: 'pointer', | |
| }} | |
| onMouseEnter={(e) => { e.currentTarget.style.background = 'var(--tw-hover)' }} | |
| onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent' }} | |
| > | |
| <span className="flex-none" style={{ opacity: on ? 0.9 : 0.4 }}>{row.icon}</span> | |
| <span className="flex-1">{row.label}</span> | |
| <span className="flex-none" style={{ opacity: on ? 0.9 : 0.4 }}> | |
| {on ? <Eye className="h-4 w-4" /> : <EyeOff className="h-4 w-4" />} | |
| </span> | |
| </button> | |
| ) | |
| })} | |
| </div> | |
| </div> | |
| ) | |
| } | |
| function SectionLabel({ children, className }: { children: React.ReactNode; className?: string }) { | |
| return ( | |
| <div | |
| className={`px-2 text-[10px] uppercase ${className ?? ''}`} | |
| style={{ fontFamily: 'var(--font-mono)', letterSpacing: '0.16em', color: 'var(--tw-muted)' }} | |
| > | |
| {children} | |
| </div> | |
| ) | |
| } | |