mirror of
https://github.com/seemueller-io/yachtpit.git
synced 2025-09-08 22:46:45 +00:00
**Introduce Neumorphic UI design with new themes and styles**
- Added NeumorphicTheme implementation for light and dark modes. - Refactored `LayerSelector` and `MapNext` components to use the neumorphic style and color utilities. - Updated `menu.rs` with neumorphic-inspired button and background styling. - Enhanced GPS feed and vessel popups with neumorphic visuals, improving clarity and aesthetics. - Temporarily disabled base-map dependency in `yachtpit` for isolation testing.
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -9425,7 +9425,6 @@ checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7"
|
||||
name = "yachtpit"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"base-map",
|
||||
"bevy",
|
||||
"bevy_asset_loader",
|
||||
"bevy_flurx",
|
||||
|
@@ -1,18 +1,15 @@
|
||||
import 'mapbox-gl/dist/mapbox-gl.css';
|
||||
import {Box, Button, HStack, Input} from '@chakra-ui/react';
|
||||
import {Box, Button, HStack, Input, Text} from '@chakra-ui/react';
|
||||
import { useColorMode } from './components/ui/color-mode';
|
||||
import {useCallback, useEffect, useState} from "react";
|
||||
import MapNext, {type Geolocation} from "@/MapNext.tsx";
|
||||
import { getNeumorphicStyle, getNeumorphicColors } from './theme/neumorphic-theme';
|
||||
import {layers, LayerSelector} from "@/LayerSelector.tsx";
|
||||
|
||||
// public key
|
||||
const key =
|
||||
'cGsuZXlKMUlqb2laMlZ2Wm1aelpXVWlMQ0poSWpvaVkycDFOalo0YkdWNk1EUTRjRE41YjJnNFp6VjNNelp6YXlKOS56LUtzS1l0X3VGUGdCSDYwQUFBNFNn';
|
||||
|
||||
const layers = [
|
||||
{ name: 'OSM', value: 'mapbox://styles/mapbox/dark-v11' },
|
||||
{ name: 'Satellite', value: 'mapbox://styles/mapbox/satellite-v9' },
|
||||
];
|
||||
|
||||
|
||||
|
||||
// const vesselLayerStyle: CircleLayerSpecification = {
|
||||
// id: 'vessel',
|
||||
@@ -40,8 +37,6 @@ interface VesselStatus {
|
||||
speed: number;
|
||||
}
|
||||
|
||||
export type Layer = { name: string; value: string };
|
||||
export type Layers = Layer[];
|
||||
|
||||
class MyGeolocation implements Geolocation {
|
||||
constructor({clearWatch, getCurrentPosition, watchPosition}: {
|
||||
@@ -65,6 +60,155 @@ class MyGeolocation implements Geolocation {
|
||||
|
||||
}
|
||||
|
||||
|
||||
const custom_geolocation = new MyGeolocation({
|
||||
clearWatch: (watchId: number) => {
|
||||
if (typeof window !== 'undefined' && (window as any).geolocationWatches) {
|
||||
const interval = (window as any).geolocationWatches.get(watchId);
|
||||
if (interval) {
|
||||
clearInterval(interval);
|
||||
(window as any).geolocationWatches.delete(watchId);
|
||||
}
|
||||
}
|
||||
},
|
||||
watchPosition: (successCallback: PositionCallback, errorCallback?: PositionErrorCallback | null, options?: PositionOptions) => {
|
||||
if (typeof window === 'undefined') return 0;
|
||||
|
||||
// Initialize watches map if it doesn't exist
|
||||
if (!(window as any).geolocationWatches) {
|
||||
(window as any).geolocationWatches = new Map();
|
||||
}
|
||||
if (!(window as any).geolocationWatchId) {
|
||||
(window as any).geolocationWatchId = 0;
|
||||
}
|
||||
|
||||
const watchId = ++(window as any).geolocationWatchId;
|
||||
|
||||
const pollPosition = async () => {
|
||||
if ((window as any).__FLURX__) {
|
||||
try {
|
||||
const vesselStatus: VesselStatus = await (window as any).__FLURX__.invoke("get_vessel_status");
|
||||
const position: GeolocationPosition = {
|
||||
coords: {
|
||||
latitude: vesselStatus.latitude,
|
||||
longitude: vesselStatus.longitude,
|
||||
altitude: null,
|
||||
accuracy: 10, // Assume 10m accuracy
|
||||
altitudeAccuracy: null,
|
||||
heading: vesselStatus.heading,
|
||||
speed: vesselStatus.speed,
|
||||
toJSON: () => ({
|
||||
latitude: vesselStatus.latitude,
|
||||
longitude: vesselStatus.longitude,
|
||||
altitude: null,
|
||||
accuracy: 10,
|
||||
altitudeAccuracy: null,
|
||||
heading: vesselStatus.heading,
|
||||
speed: vesselStatus.speed
|
||||
})
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
toJSON: () => ({
|
||||
coords: {
|
||||
latitude: vesselStatus.latitude,
|
||||
longitude: vesselStatus.longitude,
|
||||
altitude: null,
|
||||
accuracy: 10,
|
||||
altitudeAccuracy: null,
|
||||
heading: vesselStatus.heading,
|
||||
speed: vesselStatus.speed
|
||||
},
|
||||
timestamp: Date.now()
|
||||
})
|
||||
};
|
||||
successCallback(position);
|
||||
} catch (error) {
|
||||
if (errorCallback) {
|
||||
const positionError: GeolocationPositionError = {
|
||||
code: 2, // POSITION_UNAVAILABLE
|
||||
message: 'Failed to get vessel status: ' + error,
|
||||
PERMISSION_DENIED: 1,
|
||||
POSITION_UNAVAILABLE: 2,
|
||||
TIMEOUT: 3
|
||||
};
|
||||
errorCallback(positionError);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Poll immediately and then at intervals
|
||||
pollPosition();
|
||||
const interval = setInterval(pollPosition, options?.timeout || 5000);
|
||||
(window as any).geolocationWatches.set(watchId, interval);
|
||||
|
||||
return watchId;
|
||||
},
|
||||
getCurrentPosition: (successCallback: PositionCallback, errorCallback?: PositionErrorCallback | null, _options?: PositionOptions) => {
|
||||
if (typeof window !== 'undefined' && (window as any).__FLURX__) {
|
||||
(async () => {
|
||||
try {
|
||||
const vesselStatus: VesselStatus = await (window as any).__FLURX__.invoke("get_vessel_status");
|
||||
const position: GeolocationPosition = {
|
||||
coords: {
|
||||
latitude: vesselStatus.latitude,
|
||||
longitude: vesselStatus.longitude,
|
||||
altitude: null,
|
||||
accuracy: 10, // Assume 10m accuracy
|
||||
altitudeAccuracy: null,
|
||||
heading: vesselStatus.heading,
|
||||
speed: vesselStatus.speed,
|
||||
toJSON: () => ({
|
||||
latitude: vesselStatus.latitude,
|
||||
longitude: vesselStatus.longitude,
|
||||
altitude: null,
|
||||
accuracy: 10,
|
||||
altitudeAccuracy: null,
|
||||
heading: vesselStatus.heading,
|
||||
speed: vesselStatus.speed
|
||||
})
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
toJSON: () => ({
|
||||
coords: {
|
||||
latitude: vesselStatus.latitude,
|
||||
longitude: vesselStatus.longitude,
|
||||
altitude: null,
|
||||
accuracy: 10,
|
||||
altitudeAccuracy: null,
|
||||
heading: vesselStatus.heading,
|
||||
speed: vesselStatus.speed
|
||||
},
|
||||
timestamp: Date.now()
|
||||
})
|
||||
};
|
||||
successCallback(position);
|
||||
} catch (error) {
|
||||
if (errorCallback) {
|
||||
const positionError: GeolocationPositionError = {
|
||||
code: 2, // POSITION_UNAVAILABLE
|
||||
message: 'Failed to get vessel status: ' + error,
|
||||
PERMISSION_DENIED: 1,
|
||||
POSITION_UNAVAILABLE: 2,
|
||||
TIMEOUT: 3
|
||||
};
|
||||
errorCallback(positionError);
|
||||
}
|
||||
}
|
||||
})();
|
||||
} else if (errorCallback) {
|
||||
const positionError: GeolocationPositionError = {
|
||||
code: 2, // POSITION_UNAVAILABLE
|
||||
message: '__FLURX__ not available',
|
||||
PERMISSION_DENIED: 1,
|
||||
POSITION_UNAVAILABLE: 2,
|
||||
TIMEOUT: 3
|
||||
};
|
||||
errorCallback(positionError);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// interface MapViewParams {
|
||||
// latitude: number;
|
||||
// longitude: number;
|
||||
@@ -76,49 +220,10 @@ class MyGeolocation implements Geolocation {
|
||||
// token: string | null;
|
||||
// }
|
||||
|
||||
function LayerSelector(props: { onClick: (e: any) => Promise<void> }) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Box position="relative">
|
||||
<Button colorScheme="blue" size="sm" variant="solid" onClick={() => setIsOpen(!isOpen)}>
|
||||
Layer
|
||||
</Button>
|
||||
|
||||
{isOpen && (
|
||||
<Box
|
||||
position="absolute"
|
||||
top="100%"
|
||||
left={0}
|
||||
w="200px"
|
||||
bg="rgba(0, 0, 0, 0.8)"
|
||||
boxShadow="md"
|
||||
zIndex={2}
|
||||
>
|
||||
{layers.map(layer => (
|
||||
<Box
|
||||
key={layer.value}
|
||||
id={layer.value}
|
||||
p={2}
|
||||
cursor="pointer"
|
||||
color="white"
|
||||
_hover={{ bg: 'whiteAlpha.200' }}
|
||||
onClick={async e => {
|
||||
setIsOpen(false);
|
||||
await props.onClick(e);
|
||||
}}
|
||||
>
|
||||
{layer.name}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
|
||||
const { colorMode } = useColorMode();
|
||||
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
||||
const [selectedLayer, setSelectedLayer] = useState(layers[0]);
|
||||
const [searchInput, setSearchInput] = useState('');
|
||||
@@ -215,12 +320,10 @@ function App() {
|
||||
}
|
||||
}, [isSearchOpen, searchInput]);
|
||||
|
||||
const handleLayerChange = useCallback(async (e: any) => {
|
||||
const newLayer = layers.find(layer => layer.value === e.target.id);
|
||||
if (newLayer) {
|
||||
setSelectedLayer(newLayer);
|
||||
console.log('Layer changed to:', newLayer.name);
|
||||
}
|
||||
const handleLayerChange = useCallback(async (layer: any) => {
|
||||
console.log('Layer change requested:', layer);
|
||||
setSelectedLayer(layer);
|
||||
console.log('Layer changed to:', layer.name);
|
||||
}, []);
|
||||
|
||||
// const handleMapViewChange = useCallback(async (evt: any) => {
|
||||
@@ -289,25 +392,24 @@ function App() {
|
||||
return (
|
||||
/* Full-screen wrapper — fills the viewport and becomes the positioning context */
|
||||
<Box w="100vw" h="100vh" position="relative" overflow="hidden">
|
||||
{/* GPS Feed Display — absolutely positioned at bottom-left */}
|
||||
{/* GPS Feed Display — absolutely positioned at top-right */}
|
||||
{vesselPosition && (
|
||||
<Box
|
||||
position="absolute"
|
||||
top={65}
|
||||
right={4}
|
||||
zIndex={1}
|
||||
bg="rgba(0, 0, 0, 0.8)"
|
||||
color="white"
|
||||
p={3}
|
||||
borderRadius="md"
|
||||
p={4}
|
||||
fontSize="sm"
|
||||
fontFamily="monospace"
|
||||
minW="200px"
|
||||
minW="220px"
|
||||
backdropFilter="blur(10px)"
|
||||
{...getNeumorphicStyle(colorMode as 'light' | 'dark')}
|
||||
>
|
||||
<Box fontWeight="bold" mb={2}>GPS Feed</Box>
|
||||
<Box>Lat: {vesselPosition.latitude.toFixed(6)}°</Box>
|
||||
<Box>Lon: {vesselPosition.longitude.toFixed(6)}°</Box>
|
||||
<Box>Heading: {vesselPosition.heading.toFixed(1)}°</Box>
|
||||
<Box fontWeight="bold" mb={3} fontSize="md">GPS Feed</Box>
|
||||
<Box mb={1}>Lat: {vesselPosition.latitude.toFixed(6)}°</Box>
|
||||
<Box mb={1}>Lon: {vesselPosition.longitude.toFixed(6)}°</Box>
|
||||
<Box mb={1}>Heading: {vesselPosition.heading.toFixed(1)}°</Box>
|
||||
<Box>Speed: {vesselPosition.speed.toFixed(1)} kts</Box>
|
||||
</Box>
|
||||
)}
|
||||
@@ -316,39 +418,31 @@ function App() {
|
||||
<Box
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
position="relative"
|
||||
>
|
||||
<Button
|
||||
colorScheme="teal"
|
||||
size="sm"
|
||||
variant="solid"
|
||||
variant="surface"
|
||||
onClick={handleSearchClick}
|
||||
mr={2}
|
||||
{...getNeumorphicStyle(colorMode as 'light' | 'dark')}
|
||||
>
|
||||
Search
|
||||
<Text>SEARCH</Text>
|
||||
</Button>
|
||||
{isSearchOpen && <Box
|
||||
w="200px"
|
||||
transition="all 0.3s"
|
||||
transform={`translateX(${isSearchOpen ? "0" : "100%"})`}
|
||||
background="rgba(0, 0, 0, 0.8)"
|
||||
opacity={isSearchOpen ? 1 : 0}
|
||||
color="white"
|
||||
backdropFilter="blur(10px)"
|
||||
{...getNeumorphicStyle(colorMode as 'light' | 'dark', 'pressed')}
|
||||
>
|
||||
<Input
|
||||
placeholder="Search..."
|
||||
size="sm"
|
||||
value={searchInput}
|
||||
onChange={e => setSearchInput(e.target.value)}
|
||||
color="white"
|
||||
bg="rgba(0, 0, 0, 0.8)"
|
||||
border="none"
|
||||
borderRadius="0"
|
||||
_focus={{
|
||||
outline: 'none',
|
||||
}}
|
||||
_placeholder={{
|
||||
color: "#d1cfcf"
|
||||
}}
|
||||
{...getNeumorphicStyle(colorMode as 'light' | 'dark', 'pressed')}
|
||||
/>
|
||||
{searchResults.length > 0 && (
|
||||
<Box
|
||||
@@ -356,180 +450,42 @@ function App() {
|
||||
top="100%"
|
||||
left={0}
|
||||
w="200px"
|
||||
bg="rgba(0, 0, 0, 0.8)"
|
||||
boxShadow="md"
|
||||
zIndex={2}
|
||||
mt={2}
|
||||
backdropFilter="blur(10px)"
|
||||
{...getNeumorphicStyle(colorMode as 'light' | 'dark')}
|
||||
>
|
||||
{searchResults.map((result, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
p={2}
|
||||
cursor="pointer"
|
||||
color="white"
|
||||
_hover={{ bg: 'whiteAlpha.200' }}
|
||||
onClick={async () => {
|
||||
console.log(`Selecting result ${result.lat}, ${result.lon}`);
|
||||
await selectSearchResult(result);
|
||||
setSearchResults([]);
|
||||
setIsSearchOpen(false);
|
||||
}}
|
||||
>
|
||||
{`${result.lat}, ${result.lon}`}
|
||||
</Box>
|
||||
))}
|
||||
{searchResults.map((result, index) => {
|
||||
const colors = getNeumorphicColors(colorMode as 'light' | 'dark');
|
||||
return (
|
||||
<Box
|
||||
key={index}
|
||||
p={3}
|
||||
cursor="pointer"
|
||||
borderRadius="8px"
|
||||
transition="all 0.2s ease-in-out"
|
||||
_hover={{
|
||||
bg: colors.accent + '20',
|
||||
transform: 'translateY(-1px)',
|
||||
}}
|
||||
onClick={async () => {
|
||||
console.log(`Selecting result ${result.lat}, ${result.lon}`);
|
||||
await selectSearchResult(result);
|
||||
setSearchResults([]);
|
||||
setIsSearchOpen(false);
|
||||
}}
|
||||
>
|
||||
{`${result.lat}, ${result.lon}`}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
</Box>}
|
||||
</Box>
|
||||
<LayerSelector onClick={handleLayerChange} />
|
||||
</HStack>
|
||||
<MapNext mapboxPublicKey={atob(key)} vesselPosition={vesselPosition} layer={selectedLayer} mapView={mapView} geolocation={new MyGeolocation({
|
||||
clearWatch: (watchId: number) => {
|
||||
if (typeof window !== 'undefined' && (window as any).geolocationWatches) {
|
||||
const interval = (window as any).geolocationWatches.get(watchId);
|
||||
if (interval) {
|
||||
clearInterval(interval);
|
||||
(window as any).geolocationWatches.delete(watchId);
|
||||
}
|
||||
}
|
||||
},
|
||||
watchPosition: (successCallback: PositionCallback, errorCallback?: PositionErrorCallback | null, options?: PositionOptions) => {
|
||||
if (typeof window === 'undefined') return 0;
|
||||
|
||||
// Initialize watches map if it doesn't exist
|
||||
if (!(window as any).geolocationWatches) {
|
||||
(window as any).geolocationWatches = new Map();
|
||||
}
|
||||
if (!(window as any).geolocationWatchId) {
|
||||
(window as any).geolocationWatchId = 0;
|
||||
}
|
||||
|
||||
const watchId = ++(window as any).geolocationWatchId;
|
||||
|
||||
const pollPosition = async () => {
|
||||
if ((window as any).__FLURX__) {
|
||||
try {
|
||||
const vesselStatus: VesselStatus = await (window as any).__FLURX__.invoke("get_vessel_status");
|
||||
const position: GeolocationPosition = {
|
||||
coords: {
|
||||
latitude: vesselStatus.latitude,
|
||||
longitude: vesselStatus.longitude,
|
||||
altitude: null,
|
||||
accuracy: 10, // Assume 10m accuracy
|
||||
altitudeAccuracy: null,
|
||||
heading: vesselStatus.heading,
|
||||
speed: vesselStatus.speed,
|
||||
toJSON: () => ({
|
||||
latitude: vesselStatus.latitude,
|
||||
longitude: vesselStatus.longitude,
|
||||
altitude: null,
|
||||
accuracy: 10,
|
||||
altitudeAccuracy: null,
|
||||
heading: vesselStatus.heading,
|
||||
speed: vesselStatus.speed
|
||||
})
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
toJSON: () => ({
|
||||
coords: {
|
||||
latitude: vesselStatus.latitude,
|
||||
longitude: vesselStatus.longitude,
|
||||
altitude: null,
|
||||
accuracy: 10,
|
||||
altitudeAccuracy: null,
|
||||
heading: vesselStatus.heading,
|
||||
speed: vesselStatus.speed
|
||||
},
|
||||
timestamp: Date.now()
|
||||
})
|
||||
};
|
||||
successCallback(position);
|
||||
} catch (error) {
|
||||
if (errorCallback) {
|
||||
const positionError: GeolocationPositionError = {
|
||||
code: 2, // POSITION_UNAVAILABLE
|
||||
message: 'Failed to get vessel status: ' + error,
|
||||
PERMISSION_DENIED: 1,
|
||||
POSITION_UNAVAILABLE: 2,
|
||||
TIMEOUT: 3
|
||||
};
|
||||
errorCallback(positionError);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Poll immediately and then at intervals
|
||||
pollPosition();
|
||||
const interval = setInterval(pollPosition, options?.timeout || 5000);
|
||||
(window as any).geolocationWatches.set(watchId, interval);
|
||||
|
||||
return watchId;
|
||||
},
|
||||
getCurrentPosition: (successCallback: PositionCallback, errorCallback?: PositionErrorCallback | null, _options?: PositionOptions) => {
|
||||
if (typeof window !== 'undefined' && (window as any).__FLURX__) {
|
||||
(async () => {
|
||||
try {
|
||||
const vesselStatus: VesselStatus = await (window as any).__FLURX__.invoke("get_vessel_status");
|
||||
const position: GeolocationPosition = {
|
||||
coords: {
|
||||
latitude: vesselStatus.latitude,
|
||||
longitude: vesselStatus.longitude,
|
||||
altitude: null,
|
||||
accuracy: 10, // Assume 10m accuracy
|
||||
altitudeAccuracy: null,
|
||||
heading: vesselStatus.heading,
|
||||
speed: vesselStatus.speed,
|
||||
toJSON: () => ({
|
||||
latitude: vesselStatus.latitude,
|
||||
longitude: vesselStatus.longitude,
|
||||
altitude: null,
|
||||
accuracy: 10,
|
||||
altitudeAccuracy: null,
|
||||
heading: vesselStatus.heading,
|
||||
speed: vesselStatus.speed
|
||||
})
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
toJSON: () => ({
|
||||
coords: {
|
||||
latitude: vesselStatus.latitude,
|
||||
longitude: vesselStatus.longitude,
|
||||
altitude: null,
|
||||
accuracy: 10,
|
||||
altitudeAccuracy: null,
|
||||
heading: vesselStatus.heading,
|
||||
speed: vesselStatus.speed
|
||||
},
|
||||
timestamp: Date.now()
|
||||
})
|
||||
};
|
||||
successCallback(position);
|
||||
} catch (error) {
|
||||
if (errorCallback) {
|
||||
const positionError: GeolocationPositionError = {
|
||||
code: 2, // POSITION_UNAVAILABLE
|
||||
message: 'Failed to get vessel status: ' + error,
|
||||
PERMISSION_DENIED: 1,
|
||||
POSITION_UNAVAILABLE: 2,
|
||||
TIMEOUT: 3
|
||||
};
|
||||
errorCallback(positionError);
|
||||
}
|
||||
}
|
||||
})();
|
||||
} else if (errorCallback) {
|
||||
const positionError: GeolocationPositionError = {
|
||||
code: 2, // POSITION_UNAVAILABLE
|
||||
message: '__FLURX__ not available',
|
||||
PERMISSION_DENIED: 1,
|
||||
POSITION_UNAVAILABLE: 2,
|
||||
TIMEOUT: 3
|
||||
};
|
||||
errorCallback(positionError);
|
||||
}
|
||||
},
|
||||
})}/>
|
||||
<MapNext mapboxPublicKey={atob(key)} vesselPosition={vesselPosition} layer={selectedLayer} mapView={mapView} geolocation={window.navigator.geolocation || custom_geolocation}/>
|
||||
{/*<Map*/}
|
||||
{/* mapboxAccessToken={atob(key)}*/}
|
||||
{/* initialViewState={mapView}*/}
|
||||
|
91
crates/base-map/map/src/LayerSelector.tsx
Normal file
91
crates/base-map/map/src/LayerSelector.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import 'mapbox-gl/dist/mapbox-gl.css';
|
||||
import {Button, Menu, Portal} from '@chakra-ui/react';
|
||||
import {useColorMode} from './components/ui/color-mode';
|
||||
import {useState} from "react";
|
||||
import {getNeumorphicColors, getNeumorphicStyle} from './theme/neumorphic-theme';
|
||||
|
||||
export const layers = [
|
||||
{ name: 'OSM', value: 'mapbox://styles/mapbox/dark-v11' },
|
||||
{ name: 'Satellite', value: 'mapbox://styles/mapbox/satellite-v9' },
|
||||
];
|
||||
|
||||
|
||||
|
||||
// const vesselLayerStyle: CircleLayerSpecification = {
|
||||
// id: 'vessel',
|
||||
// type: 'circle',
|
||||
// paint: {
|
||||
// 'circle-radius': 8,
|
||||
// 'circle-color': '#ff4444',
|
||||
// 'circle-stroke-width': 2,
|
||||
// 'circle-stroke-color': '#ffffff'
|
||||
// },
|
||||
// source: ''
|
||||
// };
|
||||
|
||||
|
||||
export type Layer = { name: string; value: string };
|
||||
export type Layers = Layer[];
|
||||
|
||||
// interface MapViewParams {
|
||||
// latitude: number;
|
||||
// longitude: number;
|
||||
// zoom: number;
|
||||
// }
|
||||
|
||||
// interface AuthParams {
|
||||
// authenticated: boolean;
|
||||
// token: string | null;
|
||||
// }
|
||||
|
||||
export function LayerSelector(props: { onClick: (layer: Layer) => Promise<void> }) {
|
||||
const { colorMode } = useColorMode();
|
||||
const [selectedLayer, setSelectedLayer] = useState(layers[0]);
|
||||
const neumorphicStyle = getNeumorphicStyle(colorMode as 'light' | 'dark');
|
||||
const colors = getNeumorphicColors(colorMode as 'light' | 'dark');
|
||||
|
||||
return (
|
||||
<Menu.Root>
|
||||
<Menu.Trigger asChild>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="surface"
|
||||
{...neumorphicStyle}
|
||||
>
|
||||
{selectedLayer?.name || 'Layer'}
|
||||
</Button>
|
||||
</Menu.Trigger>
|
||||
<Portal>
|
||||
<Menu.Positioner>
|
||||
<Menu.Content
|
||||
minW="200px"
|
||||
py={2}
|
||||
{...neumorphicStyle}
|
||||
>
|
||||
{layers.map(layer => (
|
||||
<Menu.Item
|
||||
key={layer.value}
|
||||
id={layer.value}
|
||||
value={layer.value}
|
||||
borderRadius={6}
|
||||
transition="all 0.2s ease-in-out"
|
||||
_hover={{
|
||||
bg: colors.accent + '20',
|
||||
transform: 'translateY(-1px)',
|
||||
}}
|
||||
onClick={(e) => {
|
||||
// @ts-ignore
|
||||
console.log(e.target.id)
|
||||
setSelectedLayer(layer);
|
||||
props.onClick(layer);
|
||||
}}
|
||||
>
|
||||
{layer.name}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu.Content>
|
||||
</Menu.Positioner>
|
||||
</Portal>
|
||||
</Menu.Root>
|
||||
);
|
||||
}
|
@@ -17,6 +17,8 @@ import { useRealAISProvider } from './real-ais-provider.tsx';
|
||||
|
||||
import PORTS from './test_data/nautical-base-data.json';
|
||||
import {Box} from "@chakra-ui/react";
|
||||
import { useColorMode } from './components/ui/color-mode';
|
||||
import { getNeumorphicStyle, getNeumorphicColors } from './theme/neumorphic-theme';
|
||||
|
||||
|
||||
export interface Geolocation {
|
||||
@@ -31,6 +33,7 @@ export interface Geolocation {
|
||||
|
||||
|
||||
export default function MapNext(props: any = {mapboxPublicKey: "", geolocation: Geolocation, vesselPosition: undefined, layer: undefined, mapView: undefined} as any) {
|
||||
const { colorMode } = useColorMode();
|
||||
const [popupInfo, setPopupInfo] = useState(null);
|
||||
const [vesselPopupInfo, setVesselPopupInfo] = useState<VesselData | null>(null);
|
||||
const [boundingBox, setBoundingBox] = useState<{sw_lat: number, sw_lon: number, ne_lat: number, ne_lon: number} | undefined>(undefined);
|
||||
@@ -230,28 +233,45 @@ export default function MapNext(props: any = {mapboxPublicKey: "", geolocation:
|
||||
latitude={vesselPopupInfo.latitude}
|
||||
onClose={() => setVesselPopupInfo(null)}
|
||||
>
|
||||
<div style={{ minWidth: '200px' }}>
|
||||
<h3 style={{ margin: '0 0 8px 0', fontSize: '16px', fontWeight: 'bold' }}>
|
||||
<Box
|
||||
minW="240px"
|
||||
p={4}
|
||||
backdropFilter="blur(10px)"
|
||||
{...getNeumorphicStyle(colorMode as 'light' | 'dark')}
|
||||
>
|
||||
<Box
|
||||
mb={3}
|
||||
fontSize="lg"
|
||||
fontWeight="bold"
|
||||
color={getNeumorphicColors(colorMode as 'light' | 'dark').text}
|
||||
>
|
||||
{vesselPopupInfo.name}
|
||||
</h3>
|
||||
<div style={{ fontSize: '14px', lineHeight: '1.4' }}>
|
||||
<div><strong>Type:</strong> {vesselPopupInfo.type}</div>
|
||||
<div><strong>MMSI:</strong> {vesselPopupInfo.mmsi}</div>
|
||||
<div><strong>Call Sign:</strong> {vesselPopupInfo.callSign}</div>
|
||||
<div><strong>Speed:</strong> {vesselPopupInfo.speed.toFixed(1)} knots</div>
|
||||
<div><strong>Heading:</strong> {vesselPopupInfo.heading.toFixed(0)}°</div>
|
||||
<div><strong>Length:</strong> {vesselPopupInfo.length.toFixed(0)}m</div>
|
||||
</Box>
|
||||
<Box fontSize="sm" lineHeight="1.6">
|
||||
<Box mb={1}><strong>Type:</strong> {vesselPopupInfo.type}</Box>
|
||||
<Box mb={1}><strong>MMSI:</strong> {vesselPopupInfo.mmsi}</Box>
|
||||
<Box mb={1}><strong>Call Sign:</strong> {vesselPopupInfo.callSign}</Box>
|
||||
<Box mb={1}><strong>Speed:</strong> {vesselPopupInfo.speed.toFixed(1)} knots</Box>
|
||||
<Box mb={1}><strong>Heading:</strong> {vesselPopupInfo.heading.toFixed(0)}°</Box>
|
||||
<Box mb={1}><strong>Length:</strong> {vesselPopupInfo.length.toFixed(0)}m</Box>
|
||||
{vesselPopupInfo.destination && (
|
||||
<div><strong>Destination:</strong> {vesselPopupInfo.destination}</div>
|
||||
<Box mb={1}><strong>Destination:</strong> {vesselPopupInfo.destination}</Box>
|
||||
)}
|
||||
{vesselPopupInfo.eta && (
|
||||
<div><strong>ETA:</strong> {vesselPopupInfo.eta}</div>
|
||||
<Box mb={1}><strong>ETA:</strong> {vesselPopupInfo.eta}</Box>
|
||||
)}
|
||||
<div style={{ fontSize: '12px', color: '#666', marginTop: '8px' }}>
|
||||
<Box
|
||||
fontSize="xs"
|
||||
color={getNeumorphicColors(colorMode as 'light' | 'dark').textSecondary}
|
||||
mt={3}
|
||||
pt={2}
|
||||
borderTop="1px solid"
|
||||
borderColor={getNeumorphicColors(colorMode as 'light' | 'dark').textSecondary + '30'}
|
||||
>
|
||||
Last Update: {vesselPopupInfo.lastUpdate.toLocaleTimeString()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Popup>
|
||||
)}
|
||||
|
||||
|
75
crates/base-map/map/src/theme/neumorphic-theme.ts
Normal file
75
crates/base-map/map/src/theme/neumorphic-theme.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { defineConfig } from "@chakra-ui/react";
|
||||
|
||||
// Neumorphic color palette
|
||||
const neumorphicColors = {
|
||||
light: {
|
||||
bg: '#e0e5ec',
|
||||
surface: '#e0e5ec',
|
||||
text: '#2d3748',
|
||||
textSecondary: '#4a5568',
|
||||
accent: '#3182ce',
|
||||
shadow: {
|
||||
dark: '#a3b1c6',
|
||||
light: '#ffffff',
|
||||
},
|
||||
},
|
||||
dark: {
|
||||
bg: '#2d3748',
|
||||
surface: '#ffffff',
|
||||
text: '#f7fafc',
|
||||
textSecondary: '#e2e8f0',
|
||||
accent: '#63b3ed',
|
||||
shadow: {
|
||||
dark: '#1a202c',
|
||||
light: '#4a5568',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Neumorphic shadow mixins
|
||||
const neumorphicShadows = {
|
||||
light: {
|
||||
raised: '1px 1px 2px #a3b1c6, -1px -1px 2px #ffffff',
|
||||
pressed: 'inset 2px 2px 4px #a3b1c6, inset -2px -2px 4px #ffffff',
|
||||
subtle: '1px 1px 2px #a3b1c6, -1px -1px 2px #ffffff',
|
||||
subtlePressed: 'inset 1px 1px 2px #a3b1c6, inset -1px -1px 2px #ffffff',
|
||||
floating: '6px 6px 12px #a3b1c6, -6px -6px 12px #ffffff',
|
||||
},
|
||||
dark: {
|
||||
raised: '2px 2px 4px #1a202c, -2px -2px 4px #4a5568',
|
||||
pressed: 'inset 2px 2px 4px #1a202c, inset -2px -2px 4px #4a5568',
|
||||
subtle: '2px 2px 2px #1a202c, -2px -2px 2px #4a5568',
|
||||
subtlePressed: 'inset 2px 2px 2px #1a202c, inset -2px -2px 2px #4a5568',
|
||||
floating: '6px 6px 12px #1a202c, -6px -6px 12px #4a5568',
|
||||
},
|
||||
};
|
||||
|
||||
// Simplified theme configuration to avoid TypeScript errors
|
||||
// The utility functions below provide the neumorphic styling functionality
|
||||
export const neumorphicTheme = defineConfig({
|
||||
theme: {
|
||||
// Theme configuration simplified to avoid type errors
|
||||
},
|
||||
});
|
||||
|
||||
// Utility functions for neumorphic styling
|
||||
export const getNeumorphicStyle = (colorMode: 'light' | 'dark', variant: 'raised' | 'pressed' | 'subtle' | 'floating' = 'raised') => {
|
||||
const colors = neumorphicColors[colorMode];
|
||||
const shadows = neumorphicShadows[colorMode];
|
||||
|
||||
return {
|
||||
bg: colors.surface,
|
||||
color: colors.text,
|
||||
borderRadius: 6,
|
||||
boxShadow: shadows[variant] || shadows.raised,
|
||||
transition: 'all 0.3s ease-in-out',
|
||||
};
|
||||
};
|
||||
|
||||
export const getNeumorphicColors = (colorMode: 'light' | 'dark') => {
|
||||
return neumorphicColors[colorMode];
|
||||
};
|
||||
|
||||
export const getNeumorphicShadows = (colorMode: 'light' | 'dark') => {
|
||||
return neumorphicShadows[colorMode];
|
||||
};
|
@@ -5,7 +5,7 @@ export default defineConfig({
|
||||
plugins: [react()],
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
setupFiles: ['./src/test-setup.ts'],
|
||||
setupFiles: ['./test/test-setup.ts'],
|
||||
globals: true,
|
||||
},
|
||||
});
|
@@ -100,4 +100,4 @@ console_error_panic_hook = "0.1"
|
||||
|
||||
[build-dependencies]
|
||||
embed-resource = "1"
|
||||
base-map = { path = "../base-map" }
|
||||
# base-map = { path = "../base-map" } # Temporarily disabled for testing
|
||||
|
@@ -13,17 +13,42 @@ impl Plugin for MenuPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
#[derive(Component, Clone)]
|
||||
struct ButtonColors {
|
||||
normal: Color,
|
||||
hovered: Color,
|
||||
pressed: Color,
|
||||
}
|
||||
|
||||
// Neumorphic color palette for luxury design
|
||||
struct NeumorphicColors;
|
||||
|
||||
impl NeumorphicColors {
|
||||
// Base surface color - soft gray with warm undertones
|
||||
const SURFACE: Color = Color::linear_rgb(0.88, 0.90, 0.92);
|
||||
|
||||
// Primary button colors with depth
|
||||
const PRIMARY_NORMAL: Color = Color::linear_rgb(0.85, 0.87, 0.90);
|
||||
const PRIMARY_HOVERED: Color = Color::linear_rgb(0.90, 0.92, 0.95);
|
||||
const PRIMARY_PRESSED: Color = Color::linear_rgb(0.80, 0.82, 0.85);
|
||||
|
||||
// Secondary button colors (more subtle)
|
||||
const SECONDARY_NORMAL: Color = Color::linear_rgb(0.86, 0.88, 0.91);
|
||||
const SECONDARY_HOVERED: Color = Color::linear_rgb(0.88, 0.90, 0.93);
|
||||
const SECONDARY_PRESSED: Color = Color::linear_rgb(0.82, 0.84, 0.87);
|
||||
|
||||
// Text colors for contrast
|
||||
const TEXT_PRIMARY: Color = Color::linear_rgb(0.25, 0.30, 0.35);
|
||||
const TEXT_SECONDARY: Color = Color::linear_rgb(0.45, 0.50, 0.55);
|
||||
const TEXT_ACCENT: Color = Color::linear_rgb(0.20, 0.45, 0.75);
|
||||
}
|
||||
|
||||
impl Default for ButtonColors {
|
||||
fn default() -> Self {
|
||||
ButtonColors {
|
||||
normal: Color::linear_rgb(0.15, 0.15, 0.15),
|
||||
hovered: Color::linear_rgb(0.25, 0.25, 0.25),
|
||||
normal: NeumorphicColors::PRIMARY_NORMAL,
|
||||
hovered: NeumorphicColors::PRIMARY_HOVERED,
|
||||
pressed: NeumorphicColors::PRIMARY_PRESSED,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,6 +59,18 @@ struct Menu;
|
||||
fn setup_menu(mut commands: Commands) {
|
||||
info!("menu");
|
||||
commands.spawn((Camera2d, Msaa::Off));
|
||||
|
||||
// Set neumorphic background
|
||||
commands.spawn((
|
||||
Node {
|
||||
width: Val::Percent(100.0),
|
||||
height: Val::Percent(100.0),
|
||||
position_type: PositionType::Absolute,
|
||||
..default()
|
||||
},
|
||||
BackgroundColor(NeumorphicColors::SURFACE),
|
||||
));
|
||||
|
||||
commands
|
||||
.spawn((
|
||||
Node {
|
||||
@@ -52,23 +89,27 @@ fn setup_menu(mut commands: Commands) {
|
||||
.spawn((
|
||||
Button,
|
||||
Node {
|
||||
width: Val::Px(140.0),
|
||||
height: Val::Px(50.0),
|
||||
width: Val::Px(180.0),
|
||||
height: Val::Px(65.0),
|
||||
justify_content: JustifyContent::Center,
|
||||
align_items: AlignItems::Center,
|
||||
border: UiRect::all(Val::Px(2.0)),
|
||||
margin: UiRect::all(Val::Px(8.0)),
|
||||
..Default::default()
|
||||
},
|
||||
BackgroundColor(button_colors.normal),
|
||||
BorderColor(Color::linear_rgb(0.82, 0.84, 0.87)),
|
||||
BorderRadius::all(Val::Px(16.0)),
|
||||
button_colors,
|
||||
ChangeState(GameState::Playing),
|
||||
))
|
||||
.with_child((
|
||||
Text::new("Play"),
|
||||
Text::new("▶ PLAY"),
|
||||
TextFont {
|
||||
font_size: 40.0,
|
||||
font_size: 28.0,
|
||||
..default()
|
||||
},
|
||||
TextColor(Color::linear_rgb(0.9, 0.9, 0.9)),
|
||||
TextColor(NeumorphicColors::TEXT_PRIMARY),
|
||||
));
|
||||
});
|
||||
commands
|
||||
@@ -85,74 +126,71 @@ fn setup_menu(mut commands: Commands) {
|
||||
Menu,
|
||||
))
|
||||
.with_children(|children| {
|
||||
let secondary_button_colors = ButtonColors {
|
||||
normal: NeumorphicColors::SECONDARY_NORMAL,
|
||||
hovered: NeumorphicColors::SECONDARY_HOVERED,
|
||||
pressed: NeumorphicColors::SECONDARY_PRESSED,
|
||||
};
|
||||
|
||||
children
|
||||
.spawn((
|
||||
Button,
|
||||
Node {
|
||||
width: Val::Px(170.0),
|
||||
height: Val::Px(50.0),
|
||||
justify_content: JustifyContent::SpaceAround,
|
||||
width: Val::Px(180.0),
|
||||
height: Val::Px(45.0),
|
||||
justify_content: JustifyContent::Center,
|
||||
align_items: AlignItems::Center,
|
||||
padding: UiRect::all(Val::Px(5.)),
|
||||
padding: UiRect::all(Val::Px(8.)),
|
||||
border: UiRect::all(Val::Px(1.0)),
|
||||
margin: UiRect::horizontal(Val::Px(8.0)),
|
||||
..Default::default()
|
||||
},
|
||||
BackgroundColor(Color::NONE),
|
||||
ButtonColors {
|
||||
normal: Color::NONE,
|
||||
..default()
|
||||
},
|
||||
BackgroundColor(secondary_button_colors.normal),
|
||||
BorderColor(Color::linear_rgb(0.80, 0.82, 0.85)),
|
||||
BorderRadius::all(Val::Px(12.0)),
|
||||
secondary_button_colors,
|
||||
OpenLink("https://bevyengine.org"),
|
||||
))
|
||||
.with_children(|parent| {
|
||||
parent.spawn((
|
||||
Text::new("Made with Bevy"),
|
||||
TextFont {
|
||||
font_size: 15.0,
|
||||
..default()
|
||||
},
|
||||
TextColor(Color::linear_rgb(0.9, 0.9, 0.9)),
|
||||
));
|
||||
parent.spawn((
|
||||
Node {
|
||||
width: Val::Px(32.),
|
||||
..default()
|
||||
},
|
||||
));
|
||||
});
|
||||
.with_child((
|
||||
Text::new("🚀 Made with Bevy"),
|
||||
TextFont {
|
||||
font_size: 14.0,
|
||||
..default()
|
||||
},
|
||||
TextColor(NeumorphicColors::TEXT_SECONDARY),
|
||||
));
|
||||
|
||||
children
|
||||
.spawn((
|
||||
Button,
|
||||
Node {
|
||||
width: Val::Px(170.0),
|
||||
height: Val::Px(50.0),
|
||||
justify_content: JustifyContent::SpaceAround,
|
||||
width: Val::Px(180.0),
|
||||
height: Val::Px(45.0),
|
||||
justify_content: JustifyContent::Center,
|
||||
align_items: AlignItems::Center,
|
||||
padding: UiRect::all(Val::Px(5.)),
|
||||
padding: UiRect::all(Val::Px(8.)),
|
||||
border: UiRect::all(Val::Px(1.0)),
|
||||
margin: UiRect::horizontal(Val::Px(8.0)),
|
||||
..default()
|
||||
},
|
||||
BackgroundColor(Color::NONE),
|
||||
BackgroundColor(secondary_button_colors.normal),
|
||||
BorderColor(Color::linear_rgb(0.80, 0.82, 0.85)),
|
||||
BorderRadius::all(Val::Px(12.0)),
|
||||
ButtonColors {
|
||||
normal: Color::NONE,
|
||||
hovered: Color::linear_rgb(0.25, 0.25, 0.25),
|
||||
normal: NeumorphicColors::SECONDARY_NORMAL,
|
||||
hovered: NeumorphicColors::SECONDARY_HOVERED,
|
||||
pressed: NeumorphicColors::SECONDARY_PRESSED,
|
||||
},
|
||||
OpenLink("https://github.com/NiklasEi/bevy_game_template"),
|
||||
))
|
||||
.with_children(|parent| {
|
||||
parent.spawn((
|
||||
Text::new("Open source"),
|
||||
TextFont {
|
||||
font_size: 15.0,
|
||||
..default()
|
||||
},
|
||||
TextColor(Color::linear_rgb(0.9, 0.9, 0.9)),
|
||||
));
|
||||
parent.spawn((
|
||||
Node {
|
||||
width: Val::Px(32.),
|
||||
..default()
|
||||
},
|
||||
));
|
||||
});
|
||||
.with_child((
|
||||
Text::new("📖 Open Source"),
|
||||
TextFont {
|
||||
font_size: 14.0,
|
||||
..default()
|
||||
},
|
||||
TextColor(NeumorphicColors::TEXT_SECONDARY),
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -178,6 +216,10 @@ fn click_play_button(
|
||||
for (interaction, mut color, button_colors, change_state, open_link) in &mut interaction_query {
|
||||
match *interaction {
|
||||
Interaction::Pressed => {
|
||||
// Apply pressed state visual feedback
|
||||
*color = button_colors.pressed.into();
|
||||
|
||||
// Handle button actions
|
||||
if let Some(state) = change_state {
|
||||
next_state.set(state.0.clone());
|
||||
} else if let Some(link) = open_link {
|
||||
@@ -187,9 +229,11 @@ fn click_play_button(
|
||||
}
|
||||
}
|
||||
Interaction::Hovered => {
|
||||
// Smooth transition to hovered state
|
||||
*color = button_colors.hovered.into();
|
||||
}
|
||||
Interaction::None => {
|
||||
// Return to normal state
|
||||
*color = button_colors.normal.into();
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user