71 changed files with 1697 additions and 1282 deletions
File diff suppressed because it is too large
@ -0,0 +1,21 @@ |
|||
import type React from 'react'; |
|||
|
|||
import { m } from 'framer-motion'; |
|||
|
|||
export interface CardProps { |
|||
className?: string; |
|||
children: React.ReactNode; |
|||
} |
|||
|
|||
export const Card = ({ className, children }: CardProps): JSX.Element => { |
|||
return ( |
|||
<m.div |
|||
className={`flex select-none rounded-md bg-white p-4 shadow-md dark:bg-primaryDark ${className}`} |
|||
initial={{ opacity: 0 }} |
|||
animate={{ opacity: 1 }} |
|||
exit={{ opacity: 0 }} |
|||
> |
|||
{children} |
|||
</m.div> |
|||
); |
|||
}; |
|||
@ -1,10 +0,0 @@ |
|||
import type React from 'react'; |
|||
|
|||
export interface CoverProps { |
|||
content: JSX.Element; |
|||
enabled: boolean; |
|||
} |
|||
|
|||
export const Cover = ({ content, enabled }: CoverProps): JSX.Element => { |
|||
return enabled ? <div className="m-4 ">{content}</div> : <></>; |
|||
}; |
|||
@ -1,46 +0,0 @@ |
|||
import type React from 'react'; |
|||
|
|||
import { IconButton } from '@meshtastic/components'; |
|||
|
|||
export interface ListItemProps { |
|||
selected: boolean; |
|||
selectedIcon: JSX.Element; |
|||
actions?: JSX.Element; |
|||
status: JSX.Element; |
|||
onClick?: () => void; |
|||
children: React.ReactNode; |
|||
} |
|||
|
|||
export const ListItem = ({ |
|||
selected, |
|||
selectedIcon, |
|||
actions, |
|||
status, |
|||
onClick, |
|||
children, |
|||
}: ListItemProps): JSX.Element => { |
|||
return ( |
|||
<div |
|||
onClick={(): void => { |
|||
onClick && onClick(); |
|||
}} |
|||
className={`flex select-none rounded-md border bg-gray-100 shadow-md dark:bg-primaryDark ${ |
|||
selected |
|||
? 'border-primary dark:border-primary' |
|||
: 'border-gray-100 dark:border-primaryDark' |
|||
}`}
|
|||
> |
|||
<div className="w-3 rounded-l-md bg-green-500" /> |
|||
<div className="flex justify-between p-2"> |
|||
<div className="my-auto flex space-x-2"> |
|||
{status} |
|||
<div className="flex gap-2">{children}</div> |
|||
</div> |
|||
<div className="flex gap-2"> |
|||
{actions} |
|||
<IconButton active={selected} icon={selectedIcon} /> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
); |
|||
}; |
|||
@ -0,0 +1,9 @@ |
|||
import type React from 'react'; |
|||
|
|||
export const Loading = (): JSX.Element => { |
|||
return ( |
|||
<div className="absolute top-0 bottom-0 left-0 right-0 z-10 flex rounded-md backdrop-blur-sm backdrop-filter"> |
|||
<div className="m-auto text-lg font-medium text-gray-400">Loading</div> |
|||
</div> |
|||
); |
|||
}; |
|||
@ -0,0 +1,19 @@ |
|||
import 'tippy.js/dist/tippy.css'; |
|||
|
|||
import type React from 'react'; |
|||
|
|||
import cuid from 'cuid'; |
|||
|
|||
import Tippy, { TippyProps } from '@tippyjs/react'; |
|||
|
|||
export const Tooltip = ({ |
|||
children, |
|||
content, |
|||
...props |
|||
}: TippyProps): JSX.Element => { |
|||
return ( |
|||
<Tippy content={content} {...props}> |
|||
<div key={cuid()}>{children}</div> |
|||
</Tippy> |
|||
); |
|||
}; |
|||
@ -0,0 +1,73 @@ |
|||
import React from 'react'; |
|||
|
|||
import { FiCheck } from 'react-icons/fi'; |
|||
|
|||
type DefaultButtonProps = JSX.IntrinsicElements['button']; |
|||
|
|||
export enum ButtonSize { |
|||
Small = 'small', |
|||
Medium = 'medium', |
|||
Large = 'large', |
|||
} |
|||
|
|||
export interface ButtonProps extends DefaultButtonProps { |
|||
icon?: JSX.Element; |
|||
active?: boolean; |
|||
border?: boolean; |
|||
size?: ButtonSize; |
|||
confirmAction?: () => void; |
|||
} |
|||
|
|||
export const Button = ({ |
|||
icon, |
|||
className, |
|||
active, |
|||
border, |
|||
size = ButtonSize.Medium, |
|||
confirmAction, |
|||
disabled, |
|||
children, |
|||
...props |
|||
}: ButtonProps): JSX.Element => { |
|||
const [hasConfirmed, setHasConfirmed] = React.useState(false); |
|||
|
|||
const handleConfirm = (): void => { |
|||
if (typeof confirmAction == 'function') { |
|||
if (hasConfirmed) { |
|||
void confirmAction(); |
|||
} |
|||
setHasConfirmed(true); |
|||
setTimeout(() => { |
|||
setHasConfirmed(false); |
|||
}, 3000); |
|||
} |
|||
}; |
|||
|
|||
return ( |
|||
<button |
|||
onClick={handleConfirm} |
|||
className={`flex select-none items-center space-x-3 rounded-md border border-transparent text-sm transition duration-200 ease-in-out focus-within:border-primary focus-within:shadow-border active:scale-95 dark:text-white dark:focus-within:border-primary
|
|||
${ |
|||
size === ButtonSize.Small |
|||
? 'p-0' |
|||
: size === ButtonSize.Medium |
|||
? 'p-2' |
|||
: 'p-4' |
|||
} |
|||
${ |
|||
disabled |
|||
? 'cursor-not-allowed bg-white dark:bg-primaryDark' |
|||
: 'cursor-pointer hover:bg-gray-100 hover:shadow-md dark:hover:bg-secondaryDark' |
|||
} ${border ? 'border-gray-400 dark:border-gray-200' : ''} ${className}`}
|
|||
{...props} |
|||
> |
|||
{icon && ( |
|||
<div className="text-gray-500 dark:text-gray-400"> |
|||
{hasConfirmed ? <FiCheck /> : icon} |
|||
</div> |
|||
)} |
|||
|
|||
<span>{children}</span> |
|||
</button> |
|||
); |
|||
}; |
|||
@ -0,0 +1,35 @@ |
|||
import type React from 'react'; |
|||
|
|||
type DefaulButtonProps = JSX.IntrinsicElements['button']; |
|||
|
|||
export interface IconButtonProps extends DefaulButtonProps { |
|||
icon: React.ReactNode; |
|||
active?: boolean; |
|||
} |
|||
|
|||
export const IconButton = ({ |
|||
icon, |
|||
active, |
|||
disabled, |
|||
...props |
|||
}: IconButtonProps): JSX.Element => { |
|||
return ( |
|||
<div className="my-auto text-gray-500 dark:text-gray-400"> |
|||
<button |
|||
type="button" |
|||
disabled={disabled} |
|||
className={`rounded-md p-2 transition duration-200 ease-in-out active:scale-95 ${ |
|||
active |
|||
? 'bg-gray-200 dark:bg-gray-600' |
|||
: 'hover:bg-gray-200 dark:hover:bg-gray-600' |
|||
} ${ |
|||
disabled ? 'cursor-not-allowed text-gray-400 dark:text-gray-700' : '' |
|||
}`}
|
|||
{...props} |
|||
> |
|||
{icon} |
|||
<span className="sr-only">Refresh</span> |
|||
</button> |
|||
</div> |
|||
); |
|||
}; |
|||
@ -0,0 +1,48 @@ |
|||
import React from 'react'; |
|||
|
|||
import { Label } from '@components/generic/form/Label'; |
|||
|
|||
type DefaultInputProps = JSX.IntrinsicElements['input']; |
|||
|
|||
export interface CheckboxProps extends DefaultInputProps { |
|||
action?: (enabled: boolean) => void; |
|||
label: string; |
|||
valid?: boolean; |
|||
validationMessage?: string; |
|||
error?: boolean; |
|||
} |
|||
|
|||
export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>( |
|||
function Input( |
|||
{ label, valid, validationMessage, id, error, ...props }: CheckboxProps, |
|||
ref, |
|||
) { |
|||
return ( |
|||
<div className="flex w-full flex-col"> |
|||
<Label label={label} /> |
|||
<div className="ml-auto"> |
|||
<input |
|||
ref={ref} |
|||
type="checkbox" |
|||
id={id} |
|||
className={`h-8 w-8 appearance-none rounded-md border border-gray-400 transition duration-200 ease-in-out checked:border-transparent checked:bg-primary focus-within:shadow-border focus:outline-none dark:border-gray-200 ${ |
|||
props.disabled |
|||
? 'border-gray-400 bg-gray-300 text-gray-500 dark:border-gray-700 dark:bg-secondaryDark dark:text-gray-400' |
|||
: '' |
|||
} ${ |
|||
error |
|||
? 'border-red-500' |
|||
: props.disabled |
|||
? 'border-gray-200' |
|||
: 'focus-within:border-primary hover:border-primary dark:focus-within:border-primary dark:hover:border-primary' |
|||
}`}
|
|||
{...props} |
|||
/> |
|||
</div> |
|||
{!valid && ( |
|||
<div className="text-sm text-gray-600">{validationMessage}</div> |
|||
)} |
|||
</div> |
|||
); |
|||
}, |
|||
); |
|||
@ -0,0 +1,37 @@ |
|||
import React from 'react'; |
|||
|
|||
import { InputWrapper } from '@components/generic/form/InputWrapper'; |
|||
import { Label } from '@components/generic/form/Label'; |
|||
|
|||
type DefaultInputProps = JSX.IntrinsicElements['input']; |
|||
|
|||
export interface InputProps extends DefaultInputProps { |
|||
label?: string; |
|||
error?: string; |
|||
action?: JSX.Element; |
|||
prefix?: string; |
|||
suffix?: string; |
|||
} |
|||
|
|||
export const Input = React.forwardRef<HTMLInputElement, InputProps>( |
|||
function Input({ label, error, action, suffix, ...props }: InputProps, ref) { |
|||
return ( |
|||
<div className="w-full"> |
|||
{label && <Label label={label} error={error} />} |
|||
<InputWrapper error={error} disabled={props.disabled}> |
|||
<input |
|||
ref={ref} |
|||
className="h-10 w-full bg-transparent px-3 py-2 focus:outline-none disabled:cursor-not-allowed dark:text-white" |
|||
{...props} |
|||
/> |
|||
{suffix && ( |
|||
<span className="my-auto mr-3 text-sm font-medium text-gray-500 dark:text-gray-400"> |
|||
{suffix} |
|||
</span> |
|||
)} |
|||
{action && <div className="mr-1 flex">{action}</div>} |
|||
</InputWrapper> |
|||
</div> |
|||
); |
|||
}, |
|||
); |
|||
@ -0,0 +1,29 @@ |
|||
import type React from 'react'; |
|||
|
|||
export interface LabelProps { |
|||
error?: string; |
|||
disabled?: boolean; |
|||
children: React.ReactNode; |
|||
} |
|||
|
|||
export const InputWrapper = ({ |
|||
error, |
|||
disabled, |
|||
children, |
|||
}: LabelProps): JSX.Element => ( |
|||
<div |
|||
className={`flex w-full rounded-md border border-gray-400 transition duration-200 ease-in-out dark:border-gray-200 ${ |
|||
disabled |
|||
? 'border-gray-400 bg-gray-300 text-gray-500 dark:border-gray-700 dark:bg-secondaryDark dark:text-gray-400' |
|||
: '' |
|||
} ${ |
|||
error |
|||
? 'border-red-500 dark:border-red-500' |
|||
: disabled |
|||
? '' |
|||
: ' focus-within:border-primary focus-within:shadow-border hover:border-primary dark:focus-within:border-primary dark:hover:border-primary' |
|||
}`}
|
|||
> |
|||
{children} |
|||
</div> |
|||
); |
|||
@ -0,0 +1,68 @@ |
|||
import React from 'react'; |
|||
|
|||
import { InputWrapper } from '@components/generic/form/InputWrapper'; |
|||
import { Label } from '@components/generic/form/Label'; |
|||
|
|||
type DefaultSelectProps = JSX.IntrinsicElements['select']; |
|||
|
|||
export interface SelectProps extends DefaultSelectProps { |
|||
options?: { |
|||
name: string | number; |
|||
value: string | number; |
|||
}[]; |
|||
optionsEnum?: { [s: string]: string | number }; |
|||
label?: string; |
|||
error?: string; |
|||
small?: boolean; |
|||
} |
|||
|
|||
export const Select = React.forwardRef<HTMLSelectElement, SelectProps>( |
|||
({ options, optionsEnum, label, error, small, ...props }, ref) => { |
|||
const optionsEnumValues = optionsEnum |
|||
? Object.entries(optionsEnum).filter( |
|||
(value) => typeof value[1] === 'number', |
|||
) |
|||
: []; |
|||
return ( |
|||
<div> |
|||
{label && <Label label={label} error={error} />} |
|||
<InputWrapper error={error} disabled={props.disabled}> |
|||
<select |
|||
ref={ref} |
|||
className={`w-full rounded-md bg-transparent focus:border-primary focus:outline-none disabled:cursor-not-allowed dark:text-white ${ |
|||
small ? 'm-1' : 'mx-2 h-10' |
|||
}`}
|
|||
disabled={ |
|||
props.disabled |
|||
? true |
|||
: !(optionsEnumValues.length || options?.length) |
|||
} |
|||
{...props} |
|||
> |
|||
{!(optionsEnumValues.length || options?.length) && ( |
|||
<option key="loading" className="dark:bg-gray-700"> |
|||
Loading |
|||
</option> |
|||
)} |
|||
{optionsEnumValues.length && |
|||
optionsEnumValues.map(([name, value], index) => ( |
|||
<option key={index} className="dark:bg-gray-700" value={value}> |
|||
{name} |
|||
</option> |
|||
))} |
|||
{options && |
|||
options.map((option, index) => ( |
|||
<option |
|||
key={index} |
|||
className="dark:bg-gray-700" |
|||
value={option.value} |
|||
> |
|||
{option.name} |
|||
</option> |
|||
))} |
|||
</select> |
|||
</InputWrapper> |
|||
</div> |
|||
); |
|||
}, |
|||
); |
|||
@ -1,31 +0,0 @@ |
|||
import type React from 'react'; |
|||
|
|||
import JSONPretty from 'react-json-pretty'; |
|||
|
|||
import { useAppSelector } from '@app/hooks/useAppSelector'; |
|||
import { CopyButton } from '@components/menu/buttons/CopyButton'; |
|||
|
|||
export const ExternalNotificationsDebugPanel = (): JSX.Element => { |
|||
const preferences = useAppSelector( |
|||
(state) => state.meshtastic.radio.preferences, |
|||
); |
|||
|
|||
const debugData = { |
|||
extNotificationPluginActive: preferences.extNotificationPluginActive, |
|||
extNotificationPluginAlertBell: preferences.extNotificationPluginAlertBell, |
|||
extNotificationPluginAlertMessage: |
|||
preferences.extNotificationPluginAlertMessage, |
|||
extNotificationPluginEnabled: preferences.extNotificationPluginEnabled, |
|||
extNotificationPluginOutput: preferences.extNotificationPluginOutput, |
|||
extNotificationPluginOutputMs: preferences.extNotificationPluginOutputMs, |
|||
}; |
|||
|
|||
return ( |
|||
<> |
|||
<div className="fixed right-0 m-2"> |
|||
<CopyButton data={JSON.stringify(debugData)} /> |
|||
</div> |
|||
<JSONPretty className="max-w-sm" data={debugData} /> |
|||
</> |
|||
); |
|||
}; |
|||
@ -1,27 +0,0 @@ |
|||
import type React from 'react'; |
|||
|
|||
import JSONPretty from 'react-json-pretty'; |
|||
|
|||
import { useAppSelector } from '@app/hooks/useAppSelector'; |
|||
import { CopyButton } from '@components/menu/buttons/CopyButton'; |
|||
|
|||
export const RangeTestDebugPanel = (): JSX.Element => { |
|||
const preferences = useAppSelector( |
|||
(state) => state.meshtastic.radio.preferences, |
|||
); |
|||
|
|||
const debugData = { |
|||
rangeTestPluginEnabled: preferences.rangeTestPluginEnabled, |
|||
rangeTestPluginSave: preferences.rangeTestPluginSave, |
|||
rangeTestPluginSender: preferences.rangeTestPluginSender, |
|||
}; |
|||
|
|||
return ( |
|||
<> |
|||
<div className="fixed right-0 m-2"> |
|||
<CopyButton data={JSON.stringify(debugData)} /> |
|||
</div> |
|||
<JSONPretty className="max-w-sm" data={debugData} /> |
|||
</> |
|||
); |
|||
}; |
|||
@ -1,30 +0,0 @@ |
|||
import type React from 'react'; |
|||
|
|||
import JSONPretty from 'react-json-pretty'; |
|||
|
|||
import { useAppSelector } from '@app/hooks/useAppSelector'; |
|||
import { CopyButton } from '@components/menu/buttons/CopyButton'; |
|||
|
|||
export const SerialDebugPanel = (): JSX.Element => { |
|||
const preferences = useAppSelector( |
|||
(state) => state.meshtastic.radio.preferences, |
|||
); |
|||
|
|||
const debugData = { |
|||
serialpluginEnabled: preferences.serialpluginEnabled, |
|||
serialpluginEcho: preferences.serialpluginEcho, |
|||
serialpluginMode: preferences.serialpluginMode, |
|||
serialpluginRxd: preferences.serialpluginRxd, |
|||
serialpluginTxd: preferences.serialpluginTxd, |
|||
serialpluginTimeout: preferences.serialpluginTimeout, |
|||
}; |
|||
|
|||
return ( |
|||
<> |
|||
<div className="fixed right-0 m-2"> |
|||
<CopyButton data={JSON.stringify(debugData)} /> |
|||
</div> |
|||
<JSONPretty className="max-w-sm" data={debugData} /> |
|||
</> |
|||
); |
|||
}; |
|||
@ -1,31 +0,0 @@ |
|||
import type React from 'react'; |
|||
|
|||
import JSONPretty from 'react-json-pretty'; |
|||
|
|||
import { useAppSelector } from '@app/hooks/useAppSelector'; |
|||
import { CopyButton } from '@components/menu/buttons/CopyButton'; |
|||
|
|||
export const StoreForwardDebugPanel = (): JSX.Element => { |
|||
const preferences = useAppSelector( |
|||
(state) => state.meshtastic.radio.preferences, |
|||
); |
|||
|
|||
const debugData = { |
|||
storeForwardPluginEnabled: preferences.storeForwardPluginEnabled, |
|||
storeForwardPluginHeartbeat: preferences.storeForwardPluginHeartbeat, |
|||
storeForwardPluginRecords: preferences.storeForwardPluginRecords, |
|||
storeForwardPluginHistoryReturnMax: |
|||
preferences.storeForwardPluginHistoryReturnMax, |
|||
storeForwardPluginHistoryReturnWindow: |
|||
preferences.storeForwardPluginHistoryReturnWindow, |
|||
}; |
|||
|
|||
return ( |
|||
<> |
|||
<div className="fixed right-0 m-2"> |
|||
<CopyButton data={JSON.stringify(debugData)} /> |
|||
</div> |
|||
<JSONPretty className="max-w-sm" data={debugData} /> |
|||
</> |
|||
); |
|||
}; |
|||
@ -1,21 +0,0 @@ |
|||
import type React from 'react'; |
|||
|
|||
import JSONPretty from 'react-json-pretty'; |
|||
|
|||
import { CopyButton } from '@components/menu/buttons/CopyButton'; |
|||
import type { Protobuf } from '@meshtastic/meshtasticjs'; |
|||
|
|||
export interface DebugPanelProps { |
|||
channel: Protobuf.Channel; |
|||
} |
|||
|
|||
export const DebugPanel = ({ channel }: DebugPanelProps): JSX.Element => { |
|||
return ( |
|||
<> |
|||
<div className="fixed right-0 m-2"> |
|||
<CopyButton data={JSON.stringify(channel)} /> |
|||
</div> |
|||
<JSONPretty className="max-w-sm" data={channel} /> |
|||
</> |
|||
); |
|||
}; |
|||
@ -1,20 +0,0 @@ |
|||
import type React from 'react'; |
|||
|
|||
import QRCode from 'react-qr-code'; |
|||
|
|||
import type { Protobuf } from '@meshtastic/meshtasticjs'; |
|||
|
|||
export interface QRCodePanelProps { |
|||
channel: Protobuf.Channel; |
|||
} |
|||
|
|||
export const QRCodePanel = ({ channel }: QRCodePanelProps): JSX.Element => { |
|||
return ( |
|||
<div className="m-auto"> |
|||
<QRCode |
|||
className="rounded-md" |
|||
value={`https://www.meshtastic.org/d/#${channel.index}`} |
|||
/> |
|||
</div> |
|||
); |
|||
}; |
|||
@ -1,56 +0,0 @@ |
|||
import type React from 'react'; |
|||
|
|||
import { FiBluetooth, FiCpu, FiWifi } from 'react-icons/fi'; |
|||
|
|||
import { connType, openConnectionModal } from '@core/slices/appSlice'; |
|||
import { useAppDispatch } from '@hooks/useAppDispatch'; |
|||
import { useAppSelector } from '@hooks/useAppSelector'; |
|||
import { Button } from '@meshtastic/components'; |
|||
import { Types } from '@meshtastic/meshtasticjs'; |
|||
|
|||
export const DeviceStatus = (): JSX.Element => { |
|||
const dispatch = useAppDispatch(); |
|||
const appState = useAppSelector((state) => state.app); |
|||
const state = useAppSelector((state) => state.meshtastic); |
|||
|
|||
return ( |
|||
<Button |
|||
active |
|||
onClick={(): void => { |
|||
dispatch(dispatch(openConnectionModal())); |
|||
}} |
|||
> |
|||
<div className="flex gap-2 px-2"> |
|||
<div |
|||
className={` |
|||
my-auto h-2 w-2 min-w-[2] rounded-full ${ |
|||
[ |
|||
Types.DeviceStatusEnum.DEVICE_CONNECTED, |
|||
Types.DeviceStatusEnum.DEVICE_CONFIGURED, |
|||
].includes(state.deviceStatus) |
|||
? 'bg-green-400' |
|||
: [ |
|||
Types.DeviceStatusEnum.DEVICE_CONNECTING, |
|||
Types.DeviceStatusEnum.DEVICE_RECONNECTING, |
|||
Types.DeviceStatusEnum.DEVICE_CONFIGURING, |
|||
].includes(state.deviceStatus) |
|||
? 'bg-yellow-400' |
|||
: 'bg-gray-400' |
|||
}`}
|
|||
></div> |
|||
<div className="my-auto"> |
|||
{state.nodes.find( |
|||
(node) => node.number === state.radio.hardware.myNodeNum, |
|||
)?.user?.longName ?? 'Disconnected'} |
|||
</div> |
|||
{appState.connType === connType.BLE ? ( |
|||
<FiBluetooth className="h-5 w-5" /> |
|||
) : appState.connType === connType.SERIAL ? ( |
|||
<FiCpu className="h-5 w-5" /> |
|||
) : ( |
|||
<FiWifi className="h-5 w-5" /> |
|||
)} |
|||
</div> |
|||
</Button> |
|||
); |
|||
}; |
|||
@ -1,21 +0,0 @@ |
|||
import type React from 'react'; |
|||
|
|||
import JSONPretty from 'react-json-pretty'; |
|||
|
|||
import { CopyButton } from '@components/menu/buttons/CopyButton'; |
|||
import type { Node } from '@core/slices/meshtasticSlice'; |
|||
|
|||
export interface DebugPanelProps { |
|||
node: Node; |
|||
} |
|||
|
|||
export const DebugPanel = ({ node }: DebugPanelProps): JSX.Element => { |
|||
return ( |
|||
<div className="relative"> |
|||
<div className="fixed right-0 m-2"> |
|||
<CopyButton data={JSON.stringify(node)} /> |
|||
</div> |
|||
<JSONPretty className="max-w-sm" data={node} /> |
|||
</div> |
|||
); |
|||
}; |
|||
@ -1,32 +0,0 @@ |
|||
import type React from 'react'; |
|||
|
|||
import { CopyButton } from '@components/menu/buttons/CopyButton'; |
|||
import type { Node } from '@core/slices/meshtasticSlice'; |
|||
|
|||
export interface PositionPanelProps { |
|||
node: Node; |
|||
} |
|||
|
|||
export const PositionPanel = ({ node }: PositionPanelProps): JSX.Element => { |
|||
return ( |
|||
<div className="p-2"> |
|||
{node.currentPosition && ( |
|||
<div className="flex h-10 select-none justify-between rounded-md border border-gray-300 bg-transparent bg-gray-200 px-1 text-gray-500 dark:border-gray-600 dark:bg-secondaryDark dark:text-gray-400 "> |
|||
<div className="my-auto px-1"> |
|||
{(node.currentPosition.latitudeI / 1e7).toPrecision(6)}, |
|||
{(node.currentPosition?.longitudeI / 1e7).toPrecision(6)} |
|||
</div> |
|||
<CopyButton |
|||
data={ |
|||
node.currentPosition |
|||
? `${node.currentPosition.latitudeI / 1e7},${ |
|||
node.currentPosition.longitudeI / 1e7 |
|||
}` |
|||
: '' |
|||
} |
|||
/> |
|||
</div> |
|||
)} |
|||
</div> |
|||
); |
|||
}; |
|||
Loading…
Reference in new issue