Multiple theme support & fixes
This commit is contained in:
+27
-24
@@ -14,37 +14,40 @@ import { NewDevice } from "@components/NewDevice.js";
|
||||
import { PageNav } from "@components/PageNav.js";
|
||||
import { Sidebar } from "@components/Sidebar.js";
|
||||
import { useDeviceStore } from "@core/stores/deviceStore.js";
|
||||
import { ThemeController } from "./components/generic/ThemeController.js";
|
||||
|
||||
export const App = (): JSX.Element => {
|
||||
const { getDevice } = useDeviceStore();
|
||||
const { selectedDevice } = useAppStore();
|
||||
const { selectedDevice, darkMode, accent } = useAppStore();
|
||||
|
||||
const device = getDevice(selectedDevice);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-full">
|
||||
<DeviceSelector />
|
||||
<ThemeController>
|
||||
<div className="flex h-screen w-full bg-backgroundSecondary">
|
||||
<DeviceSelector />
|
||||
|
||||
{device && (
|
||||
<DeviceWrapper device={device}>
|
||||
<CommandPalette />
|
||||
<Toaster
|
||||
toastOptions={{
|
||||
duration: 2000
|
||||
}}
|
||||
/>
|
||||
<DialogManager />
|
||||
<Sidebar />
|
||||
<PageNav />
|
||||
<MapProvider>
|
||||
<div className="flex h-full w-full flex-col overflow-y-auto">
|
||||
<PageRouter />
|
||||
<Drawer />
|
||||
</div>
|
||||
</MapProvider>
|
||||
</DeviceWrapper>
|
||||
)}
|
||||
{selectedDevice === 0 && <NewDevice />}
|
||||
</div>
|
||||
{device && (
|
||||
<DeviceWrapper device={device}>
|
||||
<CommandPalette />
|
||||
<Toaster
|
||||
toastOptions={{
|
||||
duration: 4000
|
||||
}}
|
||||
/>
|
||||
<DialogManager />
|
||||
<Sidebar />
|
||||
<PageNav />
|
||||
<MapProvider>
|
||||
<div className="flex h-full w-full flex-col overflow-y-auto">
|
||||
<PageRouter />
|
||||
<Drawer />
|
||||
</div>
|
||||
</MapProvider>
|
||||
</DeviceWrapper>
|
||||
)}
|
||||
{selectedDevice === 0 && <NewDevice />}
|
||||
</div>
|
||||
</ThemeController>
|
||||
);
|
||||
};
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ import { PeersPage } from "@pages/Peers.js";
|
||||
export const PageRouter = (): JSX.Element => {
|
||||
const { activePage } = useDevice();
|
||||
return (
|
||||
<div className="flex-grow border-b">
|
||||
<div className="flex-grow overflow-y-auto">
|
||||
{activePage === "messages" && <MessagesPage />}
|
||||
{activePage === "map" && <MapPage />}
|
||||
{activePage === "extensions" && <ExtensionsPage />}
|
||||
|
||||
@@ -14,19 +14,15 @@ export const GroupView = ({ group }: GroupViewProps): JSX.Element => {
|
||||
value={group.name}
|
||||
className={({ active }) =>
|
||||
`flex cursor-default select-none items-center rounded-md px-3 py-2 ${
|
||||
active ? "bg-gray-900 bg-opacity-5 text-gray-900" : ""
|
||||
active ? "bg-backgroundPrimary text-textPrimary" : ""
|
||||
}`
|
||||
}
|
||||
>
|
||||
{({ active }) => (
|
||||
<>
|
||||
<group.icon
|
||||
className={`h-6 w-6 flex-none text-gray-900 text-opacity-40 ${
|
||||
active ? "text-opacity-100" : ""
|
||||
}`}
|
||||
/>
|
||||
<group.icon className="h-6 w-6" />
|
||||
<span className="ml-3 flex-auto truncate">{group.name}</span>
|
||||
{active && <ChevronRightIcon className="h-5 text-gray-400" />}
|
||||
{active && <ChevronRightIcon className="h-5 text-textSecondary" />}
|
||||
</>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
|
||||
@@ -1,16 +1,3 @@
|
||||
/**
|
||||
* Contextual
|
||||
* - Reset nodedb
|
||||
* - Map commands
|
||||
* - Disconnect
|
||||
* Debug commands
|
||||
* - Re-configure
|
||||
* - clear parts of store (messages, positions, telemetry etc)
|
||||
*
|
||||
* Application
|
||||
* - Light/Dark mode
|
||||
*/
|
||||
|
||||
import type React from "react";
|
||||
import { Fragment, useEffect, useState } from "react";
|
||||
|
||||
@@ -47,11 +34,15 @@ import {
|
||||
QrCodeIcon,
|
||||
QueueListIcon,
|
||||
Square3Stack3DIcon,
|
||||
SwatchIcon,
|
||||
TrashIcon,
|
||||
UsersIcon,
|
||||
WindowIcon,
|
||||
XCircleIcon
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { ThemeController } from "../generic/ThemeController.js";
|
||||
import { Blur } from "../generic/Blur.js";
|
||||
import { accentColor } from "@core/stores/appStore.js";
|
||||
|
||||
export interface Group {
|
||||
name: string;
|
||||
@@ -79,7 +70,10 @@ export const CommandPalette = (): JSX.Element => {
|
||||
devices,
|
||||
setSelectedDevice,
|
||||
removeDevice,
|
||||
selectedDevice
|
||||
selectedDevice,
|
||||
darkMode,
|
||||
setDarkMode,
|
||||
setAccent
|
||||
} = useAppStore();
|
||||
const { getDevices } = useDeviceStore();
|
||||
|
||||
@@ -276,11 +270,108 @@ export const CommandPalette = (): JSX.Element => {
|
||||
icon: WindowIcon,
|
||||
commands: [
|
||||
{
|
||||
name: "[WIP] Toggle Dark Mode",
|
||||
name: "Toggle Dark Mode",
|
||||
icon: MoonIcon,
|
||||
action() {
|
||||
alert("This feature is not implemented");
|
||||
setDarkMode(!darkMode);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Accent Color",
|
||||
icon: SwatchIcon,
|
||||
subItems: [
|
||||
{
|
||||
name: "Red",
|
||||
icon: (
|
||||
<span
|
||||
className={`h-3 w-3 rounded-full ${
|
||||
darkMode ? "bg-[#f25555]" : "bg-[#f28585]"
|
||||
}`}
|
||||
/>
|
||||
),
|
||||
action() {
|
||||
setAccent("red");
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Orange",
|
||||
icon: (
|
||||
<span
|
||||
className={`h-3 w-3 rounded-full ${
|
||||
darkMode ? "bg-[#e1720b]" : "bg-[#edb17a]"
|
||||
}`}
|
||||
/>
|
||||
),
|
||||
action() {
|
||||
setAccent("orange");
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Yellow",
|
||||
icon: (
|
||||
<span
|
||||
className={`h-3 w-3 rounded-full ${
|
||||
darkMode ? "bg-[#ac8c1a]" : "bg-[#e0cc87]"
|
||||
}`}
|
||||
/>
|
||||
),
|
||||
action() {
|
||||
setAccent("yellow");
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Green",
|
||||
icon: (
|
||||
<span
|
||||
className={`h-3 w-3 rounded-full ${
|
||||
darkMode ? "bg-[#27a341]" : "bg-[#8bc9c5]"
|
||||
}`}
|
||||
/>
|
||||
),
|
||||
action() {
|
||||
setAccent("green");
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Blue",
|
||||
icon: (
|
||||
<span
|
||||
className={`h-3 w-3 rounded-full ${
|
||||
darkMode ? "bg-[#2093fe]" : "bg-[#70afea]"
|
||||
}`}
|
||||
/>
|
||||
),
|
||||
action() {
|
||||
setAccent("blue");
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Purple",
|
||||
icon: (
|
||||
<span
|
||||
className={`h-3 w-3 rounded-full ${
|
||||
darkMode ? "bg-[#926bff]" : "bg-[#a09eef]"
|
||||
}`}
|
||||
/>
|
||||
),
|
||||
action() {
|
||||
setAccent("purple");
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Pink",
|
||||
icon: (
|
||||
<span
|
||||
className={`h-3 w-3 rounded-full ${
|
||||
darkMode ? "bg-[#e454c4]" : "bg-[#dba0c7]"
|
||||
}`}
|
||||
/>
|
||||
),
|
||||
action() {
|
||||
setAccent("pink");
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -326,43 +417,46 @@ export const CommandPalette = (): JSX.Element => {
|
||||
className="relative z-10"
|
||||
onClose={setCommandPaletteOpen}
|
||||
>
|
||||
<PaletteTransition>
|
||||
<Dialog.Panel className="mx-auto max-w-2xl transform divide-y divide-gray-500 divide-opacity-10 overflow-hidden rounded-xl bg-white shadow-2xl ring-1 ring-black ring-opacity-5 transition-all">
|
||||
<Combobox<Command | string>
|
||||
onChange={(input) => {
|
||||
if (typeof input === "string") {
|
||||
setQuery(input);
|
||||
} else if (input.action) {
|
||||
setCommandPaletteOpen(false);
|
||||
input.action();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SearchBox setQuery={setQuery} />
|
||||
<ThemeController>
|
||||
<Blur />
|
||||
<PaletteTransition>
|
||||
<Dialog.Panel className="mx-auto max-w-2xl transform overflow-hidden rounded-md bg-backgroundPrimary transition-all">
|
||||
<Combobox<Command | string>
|
||||
onChange={(input) => {
|
||||
if (typeof input === "string") {
|
||||
setQuery(input);
|
||||
} else if (input.action) {
|
||||
setCommandPaletteOpen(false);
|
||||
input.action();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SearchBox setQuery={setQuery} />
|
||||
|
||||
{query === "" || filtered.length > 0 ? (
|
||||
<Combobox.Options
|
||||
static
|
||||
className="max-h-80 scroll-py-2 divide-y divide-gray-500 divide-opacity-10 overflow-y-auto"
|
||||
>
|
||||
<li className="p-2">
|
||||
<ul className="flex flex-col gap-2 text-sm text-gray-700">
|
||||
{filtered.map((group, index) => (
|
||||
<SearchResult key={index} group={group} />
|
||||
))}
|
||||
{query === "" &&
|
||||
groups.map((group, index) => (
|
||||
<GroupView key={index} group={group} />
|
||||
{query === "" || filtered.length > 0 ? (
|
||||
<Combobox.Options
|
||||
static
|
||||
className="max-h-80 scroll-py-2 divide-y divide-opacity-10 overflow-y-auto bg-backgroundSecondary"
|
||||
>
|
||||
<li className="p-2">
|
||||
<ul className="flex flex-col gap-2 text-sm text-textSecondary">
|
||||
{filtered.map((group, index) => (
|
||||
<SearchResult key={index} group={group} />
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
</Combobox.Options>
|
||||
) : (
|
||||
query !== "" && filtered.length === 0 && <NoResults />
|
||||
)}
|
||||
</Combobox>
|
||||
</Dialog.Panel>
|
||||
</PaletteTransition>
|
||||
{query === "" &&
|
||||
groups.map((group, index) => (
|
||||
<GroupView key={index} group={group} />
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
</Combobox.Options>
|
||||
) : (
|
||||
query !== "" && filtered.length === 0 && <NoResults />
|
||||
)}
|
||||
</Combobox>
|
||||
</Dialog.Panel>
|
||||
</PaletteTransition>
|
||||
</ThemeController>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { CommandLineIcon } from "@heroicons/react/24/outline";
|
||||
export const NoResults = (): JSX.Element => {
|
||||
return (
|
||||
<div className="py-14 px-14 text-center">
|
||||
<CommandLineIcon className="mx-auto h-6 text-slate-500" />
|
||||
<CommandLineIcon className="mx-auto h-6 text-textSecondary" />
|
||||
<Mono className="tracking-tighter">
|
||||
Query does not match any avaliable commands
|
||||
</Mono>
|
||||
|
||||
@@ -14,23 +14,23 @@ export const PaletteTransition = ({
|
||||
<>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enter="ease-out duration-200"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leave="ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-gray-500 bg-opacity-25 transition-opacity" />
|
||||
<div className="bg-gray-500 fixed inset-0 bg-opacity-25 transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-10 overflow-y-auto p-4 sm:p-6 md:p-20">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enter="ease-out duration-200"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leave="ease-in duration-100"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
|
||||
@@ -10,9 +10,9 @@ export interface SearchBoxProps {
|
||||
export const SearchBox = ({ setQuery }: SearchBoxProps): JSX.Element => {
|
||||
return (
|
||||
<div className="relative">
|
||||
<MagnifyingGlassIcon className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-gray-900 text-opacity-40" />
|
||||
<MagnifyingGlassIcon className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-textSecondary" />
|
||||
<Combobox.Input
|
||||
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-sm text-gray-900 placeholder-gray-500 focus:ring-0"
|
||||
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-sm text-textPrimary placeholder-textSecondary focus:ring-0"
|
||||
placeholder="Search..."
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
/>
|
||||
|
||||
@@ -10,9 +10,9 @@ export interface SearchResultProps {
|
||||
|
||||
export const SearchResult = ({ group }: SearchResultProps): JSX.Element => {
|
||||
return (
|
||||
<div className="rounded-md border border-gray-300 py-2 shadow-md">
|
||||
<div className="rounded-md border-2 border-backgroundPrimary py-2">
|
||||
<div className="flex items-center px-3 py-2">
|
||||
<group.icon className="h-6 w-6 flex-none text-gray-900 text-opacity-40" />
|
||||
<group.icon className="text-gray-900 h-6 w-6 flex-none text-opacity-40" />
|
||||
<span className="ml-3 flex-auto truncate">{group.name}</span>
|
||||
</div>
|
||||
{group.commands.map((command, index) => (
|
||||
@@ -21,20 +21,20 @@ export const SearchResult = ({ group }: SearchResultProps): JSX.Element => {
|
||||
value={command}
|
||||
className={({ active }) =>
|
||||
`mr-2 ml-4 flex cursor-pointer select-none items-center rounded-md px-3 py-1 ${
|
||||
active ? "bg-gray-900 bg-opacity-5 text-gray-900" : ""
|
||||
active ? "text-gray-900 bg-backgroundPrimary" : ""
|
||||
}`
|
||||
}
|
||||
>
|
||||
{({ active }) => (
|
||||
<>
|
||||
<command.icon
|
||||
className={`h-4 flex-none text-gray-900 text-opacity-40 ${
|
||||
className={`text-gray-900 h-4 flex-none text-opacity-40 ${
|
||||
active ? "text-opacity-100" : ""
|
||||
}`}
|
||||
/>
|
||||
<span className="ml-3">{command.name}</span>
|
||||
{active && (
|
||||
<ChevronRightIcon className="ml-auto h-4 text-gray-400" />
|
||||
<ChevronRightIcon className="text-gray-400 ml-auto h-4" />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
@@ -47,7 +47,9 @@ export const SearchResult = ({ group }: SearchResultProps): JSX.Element => {
|
||||
value={item}
|
||||
className={({ active }) =>
|
||||
`mx-2 flex cursor-pointer select-none items-center rounded-md px-3 py-1 ${
|
||||
active ? "bg-gray-900 bg-opacity-5 text-gray-900" : ""
|
||||
active
|
||||
? "text-gray-900 bg-backgroundPrimary bg-opacity-5"
|
||||
: ""
|
||||
}`
|
||||
}
|
||||
>
|
||||
@@ -56,7 +58,7 @@ export const SearchResult = ({ group }: SearchResultProps): JSX.Element => {
|
||||
{item.icon}
|
||||
<span className="ml-3">{item.name}</span>
|
||||
{active && (
|
||||
<ChevronRightIcon className="ml-auto h-4 text-gray-400" />
|
||||
<ChevronRightIcon className="text-gray-400 ml-auto h-4" />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -4,16 +4,17 @@ import { useAppStore } from "@app/core/stores/appStore.js";
|
||||
import { useDeviceStore } from "@app/core/stores/deviceStore.js";
|
||||
import { Mono } from "@components/generic/Mono.js";
|
||||
import { Hashicon } from "@emeraldpay/hashicon-react";
|
||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||
import { MoonIcon, PlusIcon } from "@heroicons/react/24/outline";
|
||||
import { IconButton } from "./form/IconButton.js";
|
||||
|
||||
export const DeviceSelector = (): JSX.Element => {
|
||||
const { getDevices } = useDeviceStore();
|
||||
const { selectedDevice, setSelectedDevice } = useAppStore();
|
||||
const { selectedDevice, setSelectedDevice, darkMode } = useAppStore();
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-16 items-center whitespace-nowrap bg-slate-50 pt-12 [writing-mode:vertical-rl]">
|
||||
<div className="flex h-full w-16 items-center whitespace-nowrap bg-backgroundPrimary pt-12 [writing-mode:vertical-rl]">
|
||||
<Mono>Connected Devices</Mono>
|
||||
<span className="mt-6 flex gap-4 font-bold text-slate-900">
|
||||
<span className="mt-6 flex gap-4 font-bold text-textPrimary">
|
||||
{getDevices().map((device) => (
|
||||
<div
|
||||
key={device.id}
|
||||
@@ -24,10 +25,8 @@ export const DeviceSelector = (): JSX.Element => {
|
||||
>
|
||||
<Hashicon size={32} value={device.hardware.myNodeNum.toString()} />
|
||||
<div
|
||||
className={`absolute -left-1.5 h-7 w-0.5 rounded-full group-hover:bg-orange-300 ${
|
||||
device.id === selectedDevice
|
||||
? "bg-orange-400"
|
||||
: "bg-transparent"
|
||||
className={`absolute -left-1.5 h-7 w-0.5 rounded-full group-hover:bg-accent ${
|
||||
device.id === selectedDevice ? "bg-accent" : "bg-transparent"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
@@ -36,14 +35,17 @@ export const DeviceSelector = (): JSX.Element => {
|
||||
onClick={() => {
|
||||
setSelectedDevice(0);
|
||||
}}
|
||||
className={`h-8 w-8 cursor-pointer rounded-md border-2 border-dashed p-2 hover:border-orange-300 ${
|
||||
selectedDevice === 0 ? "border-orange-400" : "border-slate-200"
|
||||
className={`h-8 w-8 cursor-pointer rounded-md border-2 border-dashed p-2 hover:border-accent ${
|
||||
selectedDevice === 0 ? "border-accent" : "border-textSecondary"
|
||||
}`}
|
||||
>
|
||||
<PlusIcon />
|
||||
</div>
|
||||
</span>
|
||||
<img src="Logo_Black.svg" className="mt-auto px-3 py-4" />
|
||||
<img
|
||||
src={darkMode ? "Logo_White.svg" : "Logo_Black.svg"}
|
||||
className="mt-auto px-3 py-4"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -116,7 +116,7 @@ export const ImportDialog = ({
|
||||
{renderOptions(Protobuf.Config_LoRaConfig_RegionCode)}
|
||||
</Select>
|
||||
|
||||
<span className="text-md block font-medium text-gray-700">
|
||||
<span className="text-md block font-medium text-textPrimary">
|
||||
Channels:
|
||||
</span>
|
||||
<div className="flex w-40 flex-col gap-1">
|
||||
|
||||
@@ -48,8 +48,7 @@ export const RebootDialog = ({
|
||||
/>
|
||||
<Button
|
||||
className="w-24"
|
||||
variant="secondary"
|
||||
iconAfter={<ArrowPathIcon className="w-4" />}
|
||||
iconBefore={<ArrowPathIcon className="w-4" />}
|
||||
onClick={() => {
|
||||
connection?.reboot({
|
||||
time: 0,
|
||||
|
||||
@@ -48,8 +48,7 @@ export const ShutdownDialog = ({
|
||||
/>
|
||||
<Button
|
||||
className="w-24"
|
||||
variant="secondary"
|
||||
iconAfter={<PowerIcon className="w-4" />}
|
||||
iconBefore={<PowerIcon className="w-4" />}
|
||||
onClick={() => {
|
||||
connection?.shutdown({
|
||||
time: 0,
|
||||
|
||||
@@ -18,7 +18,7 @@ export const Drawer = (): JSX.Element => {
|
||||
];
|
||||
return (
|
||||
<Tab.Group>
|
||||
<Tab.List className="flex">
|
||||
<Tab.List className="flex bg-backgroundPrimary">
|
||||
{tabs.map((tab, index) => (
|
||||
<Tab key={index}>
|
||||
{({ selected }) => (
|
||||
@@ -26,8 +26,10 @@ export const Drawer = (): JSX.Element => {
|
||||
onClick={() => {
|
||||
setDrawerOpen(true);
|
||||
}}
|
||||
className={`flex h-full cursor-pointer px-1 first:pl-2 last:pr-2 hover:bg-orange-200 hover:text-orange-700 ${
|
||||
selected ? "bg-orange-500 text-white" : "bg-white text-black"
|
||||
className={`flex h-full cursor-pointer border-b-2 px-1 first:pl-2 last:pr-2 hover:text-textPrimary ${
|
||||
selected
|
||||
? "border-accent text-textPrimary"
|
||||
: "border-backgroundPrimary text-textSecondary"
|
||||
}`}
|
||||
>
|
||||
<span className="m-auto select-none">{tab.name}</span>
|
||||
@@ -43,11 +45,11 @@ export const Drawer = (): JSX.Element => {
|
||||
}}
|
||||
className="flex cursor-pointer px-2"
|
||||
>
|
||||
<div className="m-auto">
|
||||
<div className="m-auto text-textSecondary">
|
||||
{drawerOpen ? (
|
||||
<ChevronDownIcon className="h-4 text-gray-700" />
|
||||
<ChevronDownIcon className="h-4" />
|
||||
) : (
|
||||
<ChevronUpIcon className="h-4 text-gray-700" />
|
||||
<ChevronUpIcon className="h-4" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -111,7 +111,6 @@ export const Map = (): JSX.Element => {
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
append({
|
||||
enabled: true,
|
||||
|
||||
@@ -84,46 +84,47 @@ export const LoRa = (): JSX.Element => {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Select
|
||||
label="Preset"
|
||||
description="Modem preset to use"
|
||||
disabled={!usePreset}
|
||||
{...register("modemPreset", { valueAsNumber: true })}
|
||||
>
|
||||
{renderOptions(Protobuf.Config_LoRaConfig_ModemPreset)}
|
||||
</Select>
|
||||
<Input
|
||||
label="Bandwidth"
|
||||
description="Channel bandwidth in MHz"
|
||||
type="number"
|
||||
suffix="MHz"
|
||||
error={errors.bandwidth?.message}
|
||||
{...register("bandwidth", {
|
||||
valueAsNumber: true
|
||||
})}
|
||||
disabled={usePreset}
|
||||
/>
|
||||
<Input
|
||||
label="Spread Factor"
|
||||
description="Indicates the number of chirps per symbol"
|
||||
type="number"
|
||||
suffix="CPS"
|
||||
error={errors.spreadFactor?.message}
|
||||
{...register("spreadFactor", {
|
||||
valueAsNumber: true
|
||||
})}
|
||||
disabled={usePreset}
|
||||
/>
|
||||
<Input
|
||||
label="Coding Rate"
|
||||
description="The denominator of the coding rate"
|
||||
type="number"
|
||||
error={errors.codingRate?.message}
|
||||
{...register("codingRate", {
|
||||
valueAsNumber: true
|
||||
})}
|
||||
disabled={usePreset}
|
||||
/>
|
||||
{usePreset ? (
|
||||
<Select
|
||||
label="Preset"
|
||||
description="Modem preset to use"
|
||||
{...register("modemPreset", { valueAsNumber: true })}
|
||||
>
|
||||
{renderOptions(Protobuf.Config_LoRaConfig_ModemPreset)}
|
||||
</Select>
|
||||
) : (
|
||||
<>
|
||||
<Input
|
||||
label="Bandwidth"
|
||||
description="Channel bandwidth in MHz"
|
||||
type="number"
|
||||
suffix="MHz"
|
||||
error={errors.bandwidth?.message}
|
||||
{...register("bandwidth", {
|
||||
valueAsNumber: true
|
||||
})}
|
||||
/>
|
||||
<Input
|
||||
label="Spread Factor"
|
||||
description="Indicates the number of chirps per symbol"
|
||||
type="number"
|
||||
suffix="CPS"
|
||||
error={errors.spreadFactor?.message}
|
||||
{...register("spreadFactor", {
|
||||
valueAsNumber: true
|
||||
})}
|
||||
/>
|
||||
<Input
|
||||
label="Coding Rate"
|
||||
description="The denominator of the coding rate"
|
||||
type="number"
|
||||
error={errors.codingRate?.message}
|
||||
{...register("codingRate", {
|
||||
valueAsNumber: true
|
||||
})}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</FormSection>
|
||||
<FormSection title="Radio Settings">
|
||||
<Controller
|
||||
|
||||
@@ -73,7 +73,7 @@ export const Position = (): JSX.Element => {
|
||||
}),
|
||||
{
|
||||
loading: "Saving...",
|
||||
success: "Saved Channel",
|
||||
success: "Saved Position Config, Restarting Node",
|
||||
error: "No response received"
|
||||
}
|
||||
);
|
||||
@@ -109,13 +109,17 @@ export const Position = (): JSX.Element => {
|
||||
dirty={isDirty}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<Input
|
||||
suffix="Seconds"
|
||||
label="Broadcast Interval"
|
||||
description="How often your position is sent out over the mesh"
|
||||
type="number"
|
||||
error={errors.positionBroadcastSecs?.message}
|
||||
{...register("positionBroadcastSecs", { valueAsNumber: true })}
|
||||
<Controller
|
||||
name="gpsEnabled"
|
||||
control={control}
|
||||
render={({ field: { value, ...rest } }) => (
|
||||
<Toggle
|
||||
label="GPS Enabled"
|
||||
description="Enable the internal GPS module"
|
||||
checked={value}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="positionBroadcastSmartEnabled"
|
||||
@@ -129,104 +133,13 @@ export const Position = (): JSX.Element => {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="fixedPosition"
|
||||
control={control}
|
||||
render={({ field: { value, ...rest } }) => (
|
||||
<Toggle
|
||||
label="Use Fixed Position"
|
||||
description="Don't report GPS position, but a manually-specified one"
|
||||
checked={value}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<FormSection title="Fixed Position">
|
||||
<Input
|
||||
suffix="m"
|
||||
label="Altitude"
|
||||
type="number"
|
||||
error={errors.fixedAlt?.message}
|
||||
disabled={!fixedPositionEnabled}
|
||||
{...register("fixedAlt", { valueAsNumber: true })}
|
||||
/>
|
||||
<Input
|
||||
suffix="°"
|
||||
label="Latitude"
|
||||
type="number"
|
||||
error={errors.fixedLat?.message}
|
||||
disabled={!fixedPositionEnabled}
|
||||
{...register("fixedLat", { valueAsNumber: true })}
|
||||
/>
|
||||
<Input
|
||||
suffix="°"
|
||||
label="Longitude"
|
||||
type="number"
|
||||
error={errors.fixedLng?.message}
|
||||
disabled={!fixedPositionEnabled}
|
||||
{...register("fixedLng", { valueAsNumber: true })}
|
||||
/>
|
||||
</FormSection>
|
||||
<Controller
|
||||
name="gpsEnabled"
|
||||
control={control}
|
||||
render={({ field: { value, ...rest } }) => (
|
||||
<Toggle
|
||||
label="GPS Enabled"
|
||||
description="Enable the internal GPS module"
|
||||
checked={value}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Input
|
||||
suffix="Seconds"
|
||||
label="GPS Update Interval"
|
||||
description="How often a GPS fix should be acquired"
|
||||
type="number"
|
||||
error={errors.gpsUpdateInterval?.message}
|
||||
{...register("gpsUpdateInterval", { valueAsNumber: true })}
|
||||
/>
|
||||
<Input
|
||||
label="Fix Attempt Duration"
|
||||
description="How long the device will try to get a fix for"
|
||||
type="number"
|
||||
error={errors.gpsAttemptTime?.message}
|
||||
{...register("gpsAttemptTime", { valueAsNumber: true })}
|
||||
/>
|
||||
<Controller
|
||||
name="positionFlags"
|
||||
control={control}
|
||||
render={({ field, fieldState }): JSX.Element => {
|
||||
const { value, onChange, ...rest } = field;
|
||||
const { value, onChange } = field;
|
||||
const { error } = fieldState;
|
||||
// const options = Object.entries(
|
||||
// Protobuf.Config_PositionConfig_PositionFlags
|
||||
// )
|
||||
// .filter((value) => typeof value[1] !== "number")
|
||||
// .filter(
|
||||
// (value) =>
|
||||
// parseInt(value[0]) !==
|
||||
// Protobuf.Config_PositionConfig_PositionFlags.UNSET
|
||||
// )
|
||||
// .map((value) => {
|
||||
// return {
|
||||
// value: parseInt(value[0]),
|
||||
// label: value[1].toString().replace("POS_", "").toLowerCase(),
|
||||
// };
|
||||
// });
|
||||
|
||||
// const selected = bitwiseDecode(
|
||||
// value,
|
||||
// Protobuf.Config_PositionConfig_PositionFlags
|
||||
// ).map((flag) =>
|
||||
// Protobuf.Config_PositionConfig_PositionFlags[flag]
|
||||
// .replace("POS_", "")
|
||||
// .toLowerCase()
|
||||
// );
|
||||
// onChange={(e: { value: number; label: string }[]): void =>
|
||||
// onChange(bitwiseEncode(e.map((v) => v.value)))
|
||||
// }
|
||||
return (
|
||||
<BitwiseSelect
|
||||
label="Position Flags"
|
||||
@@ -239,6 +152,74 @@ export const Position = (): JSX.Element => {
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<FormSection title="Fixed Position">
|
||||
<Controller
|
||||
name="fixedPosition"
|
||||
control={control}
|
||||
render={({ field: { value, ...rest } }) => (
|
||||
<Toggle
|
||||
label="Enabled"
|
||||
description="Don't report GPS position, but a manually-specified one"
|
||||
checked={value}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{fixedPositionEnabled && (
|
||||
<>
|
||||
<Input
|
||||
suffix="m"
|
||||
label="Altitude"
|
||||
type="number"
|
||||
error={errors.fixedAlt?.message}
|
||||
disabled={!fixedPositionEnabled}
|
||||
{...register("fixedAlt", { valueAsNumber: true })}
|
||||
/>
|
||||
<Input
|
||||
suffix="°"
|
||||
label="Latitude"
|
||||
type="number"
|
||||
error={errors.fixedLat?.message}
|
||||
disabled={!fixedPositionEnabled}
|
||||
{...register("fixedLat", { valueAsNumber: true })}
|
||||
/>
|
||||
<Input
|
||||
suffix="°"
|
||||
label="Longitude"
|
||||
type="number"
|
||||
error={errors.fixedLng?.message}
|
||||
disabled={!fixedPositionEnabled}
|
||||
{...register("fixedLng", { valueAsNumber: true })}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</FormSection>
|
||||
<FormSection title="Intervals">
|
||||
<Input
|
||||
suffix="Seconds"
|
||||
label="Broadcast Interval"
|
||||
description="How often your position is sent out over the mesh"
|
||||
type="number"
|
||||
error={errors.positionBroadcastSecs?.message}
|
||||
{...register("positionBroadcastSecs", { valueAsNumber: true })}
|
||||
/>
|
||||
<Input
|
||||
suffix="Seconds"
|
||||
label="GPS Update Interval"
|
||||
description="How often a GPS fix should be acquired"
|
||||
type="number"
|
||||
error={errors.gpsUpdateInterval?.message}
|
||||
{...register("gpsUpdateInterval", { valueAsNumber: true })}
|
||||
/>
|
||||
<Input
|
||||
suffix="Seconds"
|
||||
label="Fix Attempt Duration"
|
||||
description="How long the device will try to get a fix for"
|
||||
type="number"
|
||||
error={errors.gpsAttemptTime?.message}
|
||||
{...register("gpsAttemptTime", { valueAsNumber: true })}
|
||||
/>
|
||||
</FormSection>
|
||||
<Input
|
||||
label="RX Pin"
|
||||
description="GPS Module RX pin override"
|
||||
|
||||
@@ -74,10 +74,10 @@ export const User = (): JSX.Element => {
|
||||
>
|
||||
<Input
|
||||
label="Device ID"
|
||||
disabled
|
||||
description="Preset unique identifier for this device."
|
||||
error={errors.id?.message}
|
||||
{...register("id")}
|
||||
readOnly
|
||||
/>
|
||||
<Input
|
||||
label="Device Name"
|
||||
|
||||
@@ -41,7 +41,6 @@ export const BLE = (): JSX.Element => {
|
||||
{bleDevices.map((device, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
void onConnect(device);
|
||||
}}
|
||||
|
||||
@@ -51,7 +51,6 @@ export const Serial = (): JSX.Element => {
|
||||
{serialPorts.map((port, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
void onConnect(port);
|
||||
}}
|
||||
|
||||
@@ -62,9 +62,9 @@ export const MapControlls = (): JSX.Element => {
|
||||
|
||||
return (
|
||||
<div className="absolute right-0 top-0 z-10 m-2 ">
|
||||
<div className="divide-y divide-orange-300 overflow-hidden rounded-md bg-white shadow-md">
|
||||
<div className="divide-y-2 divide-backgroundSecondary overflow-hidden rounded-md bg-backgroundPrimary text-textSecondary">
|
||||
<div
|
||||
className="cursor-pointer p-3 hover:bg-orange-200 hover:text-orange-700"
|
||||
className="hover:bg-orange-200 cursor-pointer p-3 hover:text-accent"
|
||||
onClick={() => {
|
||||
map?.zoomIn();
|
||||
}}
|
||||
@@ -72,7 +72,7 @@ export const MapControlls = (): JSX.Element => {
|
||||
<MagnifyingGlassPlusIcon className="h-4 w-4" />
|
||||
</div>
|
||||
<div
|
||||
className="cursor-pointer p-3 hover:bg-orange-200 hover:text-orange-700"
|
||||
className="hover:bg-orange-200 cursor-pointer p-3 hover:text-accent"
|
||||
onClick={() => {
|
||||
map?.zoomOut();
|
||||
}}
|
||||
@@ -80,13 +80,13 @@ export const MapControlls = (): JSX.Element => {
|
||||
<MagnifyingGlassMinusIcon className="h-4 w-4" />
|
||||
</div>
|
||||
<div
|
||||
className="cursor-pointer p-3 hover:bg-orange-200 hover:text-orange-700"
|
||||
className="hover:bg-orange-200 cursor-pointer p-3 hover:text-accent"
|
||||
onClick={() => {}}
|
||||
>
|
||||
<FiCrosshair className="h-4 w-4" />
|
||||
</div>
|
||||
<div
|
||||
className="cursor-pointer p-3 hover:bg-orange-200 hover:text-orange-700"
|
||||
className="hover:bg-orange-200 cursor-pointer p-3 hover:text-accent"
|
||||
onClick={() => getBBox()}
|
||||
>
|
||||
<ShareIcon className="h-4 w-4" />
|
||||
|
||||
@@ -10,7 +10,7 @@ export interface ChannelChatProps {
|
||||
}
|
||||
|
||||
export const ChannelChat = ({ channel }: ChannelChatProps): JSX.Element => {
|
||||
const { nodes, connection, ackMessage } = useDevice();
|
||||
const { nodes } = useDevice();
|
||||
|
||||
return (
|
||||
<div className="flex flex-grow flex-col">
|
||||
@@ -31,7 +31,9 @@ export const ChannelChat = ({ channel }: ChannelChatProps): JSX.Element => {
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<MessageInput channel={channel} />
|
||||
<div className="p-3">
|
||||
<MessageInput channel={channel} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,7 +6,8 @@ import type { AllMessageTypes } from "@core/stores/deviceStore.js";
|
||||
import { Hashicon } from "@emeraldpay/hashicon-react";
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
EllipsisHorizontalCircleIcon
|
||||
EllipsisHorizontalCircleIcon,
|
||||
ExclamationCircleIcon
|
||||
} from "@heroicons/react/24/outline";
|
||||
import type { Protobuf } from "@meshtastic/meshtasticjs";
|
||||
|
||||
@@ -31,16 +32,16 @@ export const Message = ({
|
||||
return lastMsgSameUser ? (
|
||||
<div className="ml-4 flex">
|
||||
{message.ack ? (
|
||||
<CheckCircleIcon className="my-auto h-4 text-slate-400" />
|
||||
<CheckCircleIcon className="my-auto h-4 text-textSecondary" />
|
||||
) : (
|
||||
<EllipsisHorizontalCircleIcon className="my-auto h-4 text-slate-200" />
|
||||
<EllipsisHorizontalCircleIcon className="my-auto h-4 text-textSecondary" />
|
||||
)}
|
||||
{"waypointID" in message ? (
|
||||
<WaypointMessage waypointID={message.waypointID} />
|
||||
) : (
|
||||
<span
|
||||
className={`ml-4 border-l-2 border-l-slate-200 pl-2 ${
|
||||
message.ack ? "text-black" : "text-slate-500"
|
||||
className={`ml-4 border-l-2 border-l-backgroundPrimary pl-2 ${
|
||||
message.ack ? "text-textPrimary" : "text-textSecondary"
|
||||
}`}
|
||||
>
|
||||
{message.text}
|
||||
@@ -54,12 +55,12 @@ export const Message = ({
|
||||
<Hashicon value={(sender?.num ?? 0).toString()} size={32} />
|
||||
</div>
|
||||
<span
|
||||
className="cursor-pointer font-medium text-slate-700"
|
||||
className="cursor-pointer font-medium text-textPrimary"
|
||||
onClick={openPeer}
|
||||
>
|
||||
{sender?.user?.longName ?? "UNK"}
|
||||
</span>
|
||||
<span className="text-sm">
|
||||
<span className="mt-1 font-mono text-xs text-textSecondary">
|
||||
{new Date(message.packet.rxTime).toLocaleTimeString(undefined, {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit"
|
||||
@@ -67,17 +68,18 @@ export const Message = ({
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
{/* <ExclamationCircleIcon /> */}
|
||||
{message.ack ? (
|
||||
<CheckCircleIcon className="my-auto h-4 text-slate-400" />
|
||||
<CheckCircleIcon className="my-auto h-4 text-textSecondary" />
|
||||
) : (
|
||||
<EllipsisHorizontalCircleIcon className="my-auto h-4 text-slate-200" />
|
||||
<EllipsisHorizontalCircleIcon className="my-auto h-4 text-textSecondary" />
|
||||
)}
|
||||
{"waypointID" in message ? (
|
||||
<WaypointMessage waypointID={message.waypointID} />
|
||||
) : (
|
||||
<span
|
||||
className={`ml-4 border-l-2 border-l-slate-200 pl-2 ${
|
||||
message.ack ? "text-black" : "text-slate-500"
|
||||
className={`ml-4 border-l-2 border-l-backgroundPrimary pl-2 ${
|
||||
message.ack ? "text-textPrimary" : "text-textSecondary"
|
||||
}`}
|
||||
>
|
||||
{message.text}
|
||||
|
||||
@@ -50,15 +50,11 @@ export const MessageInput = ({ channel }: MessageInputProps): JSX.Element => {
|
||||
/>
|
||||
</span>
|
||||
<IconButton
|
||||
variant="secondary"
|
||||
icon={<PaperAirplaneIcon className="h-4 text-slate-500" />}
|
||||
icon={<PaperAirplaneIcon className="text-slate-500 h-4" />}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<IconButton
|
||||
variant="secondary"
|
||||
icon={<MapPinIcon className="h-4 text-slate-500" />}
|
||||
/>
|
||||
<IconButton icon={<MapPinIcon className="text-slate-500 h-4" />} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -15,13 +15,13 @@ export const WaypointMessage = ({
|
||||
const waypoint = waypoints.find((wp) => wp.id === waypointID);
|
||||
|
||||
return (
|
||||
<div className="ml-4 border-l-2 border-l-slate-200 pl-2">
|
||||
<div className="flex gap-2 rounded-md p-2 shadow-md shadow-orange-300">
|
||||
<MapPinIcon className="m-auto w-6 text-slate-600" />
|
||||
<div className="border-l-slate-200 ml-4 border-l-2 pl-2">
|
||||
<div className="flex gap-2 rounded-md p-2">
|
||||
<MapPinIcon className="text-slate-600 m-auto w-6" />
|
||||
<div>
|
||||
<div className="flex gap-2">
|
||||
<div className="font-bold">{waypoint?.name}</div>
|
||||
<span className="font-mono text-sm text-slate-500">
|
||||
<span className="text-slate-500 font-mono text-sm">
|
||||
{toMGRS(waypoint?.latitudeI, waypoint?.longitudeI)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -66,18 +66,18 @@ export const PageNav = (): JSX.Element => {
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="t-4 flex h-full w-12 flex-shrink-0 items-center whitespace-nowrap border-r border-slate-200 bg-slate-50 text-sm [writing-mode:vertical-rl]">
|
||||
<span className="mt-2 flex gap-4 font-bold text-slate-500">
|
||||
<div className="flex h-full flex-shrink-0 whitespace-nowrap bg-backgroundPrimary text-sm [writing-mode:vertical-rl]">
|
||||
<span className="mt-2 flex gap-2 font-bold">
|
||||
{pages.map((Link) => (
|
||||
<div
|
||||
key={Link.name}
|
||||
onClick={() => {
|
||||
setActivePage(Link.page);
|
||||
}}
|
||||
className={`h-9 w-9 cursor-pointer rounded-md border-2 p-1.5 hover:border-orange-300 ${
|
||||
className={`hover:border-orange-300 h-9 w-9 cursor-pointer border-l-2 p-1.5 ${
|
||||
Link.page === activePage
|
||||
? "border-orange-400"
|
||||
: "border-slate-200"
|
||||
? "border-accent text-textPrimary"
|
||||
: "border-backgroundPrimary text-textSecondary hover:text-textPrimary"
|
||||
}`}
|
||||
>
|
||||
{Link.icon}
|
||||
|
||||
@@ -21,7 +21,7 @@ export const Sidebar = (): JSX.Element => {
|
||||
const myNode = nodes.find((n) => n.data.num === hardware.myNodeNum);
|
||||
|
||||
return (
|
||||
<div className="relative flex w-72 flex-shrink-0 flex-col gap-2 border-x border-slate-200 bg-slate-50 p-2">
|
||||
<div className="bg-slate-50 relative flex w-72 flex-shrink-0 flex-col gap-2 p-2">
|
||||
<DeviceWidget
|
||||
name={
|
||||
nodes.find((n) => n.data.num === hardware.myNodeNum)?.data.user
|
||||
|
||||
@@ -58,19 +58,17 @@ export const BatteryWidget = ({
|
||||
}, [hardware.myNodeNum, nodes]);
|
||||
|
||||
return (
|
||||
<div className="flex gap-3 overflow-hidden rounded-lg bg-white p-3 shadow">
|
||||
<div className="rounded-md bg-indigo-500 p-3">
|
||||
<Battery100Icon className="h-6 text-white" />
|
||||
<div className="flex gap-3 overflow-hidden rounded-lg bg-backgroundPrimary p-3 text-textSecondary">
|
||||
<div className="rounded-md bg-accent p-3 text-textPrimary">
|
||||
<Battery100Icon className="h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="truncate text-sm font-medium text-gray-500">
|
||||
Battery State
|
||||
</p>
|
||||
<p className="truncate text-sm font-medium">Battery State</p>
|
||||
<div className="flex gap-1">
|
||||
<p className="text-xl font-semibold text-gray-900">{batteryLevel}%</p>
|
||||
<div className={`flex text-sm font-semibold text-orange-600`}>
|
||||
<p className="text-xl font-semibold">{batteryLevel}%</p>
|
||||
<div className="flex text-sm font-semibold">
|
||||
<ClockIcon
|
||||
className="text-Orange-500 h-5 w-5 flex-shrink-0 self-center"
|
||||
className="h-5 w-5 flex-shrink-0 self-center"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="my-auto">{timeRemaining}</span>
|
||||
|
||||
@@ -20,24 +20,23 @@ export const DeviceWidget = ({
|
||||
reconnect
|
||||
}: DeviceWidgetProps): JSX.Element => {
|
||||
return (
|
||||
<div className="relative flex shrink-0 flex-col overflow-hidden rounded-md bg-white text-sm text-black shadow-md">
|
||||
<div className="relative flex shrink-0 flex-col overflow-hidden rounded-md text-sm text-textPrimary">
|
||||
<div className="absolute bottom-20 h-full w-full">
|
||||
<Hashicon size={350} value={nodeNum} />
|
||||
</div>
|
||||
<div className="flex p-3 backdrop-blur-md backdrop-brightness-50 backdrop-hue-rotate-30">
|
||||
<div className="drop-shadow-md">
|
||||
<div className="backdrop-brightness-50 flex p-3 backdrop-blur-md backdrop-hue-rotate-30">
|
||||
<div>
|
||||
<Hashicon size={96} value={nodeNum} />
|
||||
</div>
|
||||
<div className="flex w-full flex-col">
|
||||
<span className="ml-auto whitespace-nowrap text-xl font-bold text-slate-200">
|
||||
<span className="ml-auto whitespace-nowrap text-xl font-bold">
|
||||
{name}
|
||||
</span>
|
||||
<div className="my-auto ml-auto">
|
||||
<Button
|
||||
onClick={disconnected ? reconnect : disconnect}
|
||||
variant={disconnected ? "secondary" : "primary"}
|
||||
size="sm"
|
||||
iconAfter={<XCircleIcon className="h-4" />}
|
||||
iconBefore={<XCircleIcon className="h-4" />}
|
||||
>
|
||||
{disconnected ? "Reconnect" : "Disconnect"}
|
||||
</Button>
|
||||
|
||||
@@ -17,15 +17,15 @@ export const PeersWidget = ({ peers }: PeersWidgetProps): JSX.Element => {
|
||||
const { setActivePage } = useDevice();
|
||||
|
||||
return (
|
||||
<div className="flex gap-3 overflow-hidden rounded-lg bg-white p-3 shadow">
|
||||
<div className="rounded-md bg-emerald-500 p-3">
|
||||
<UserGroupIcon className="h-6 text-white" />
|
||||
<div className="flex gap-3 overflow-hidden rounded-lg bg-backgroundPrimary p-3 text-textSecondary">
|
||||
<div className="rounded-md bg-accent p-3">
|
||||
<UserGroupIcon className="h-6 text-textPrimary" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="truncate text-sm font-medium text-gray-500">Peers</p>
|
||||
<p className="truncate text-sm font-medium">Peers</p>
|
||||
<div className="flex gap-1">
|
||||
{peers.length > 0 ? (
|
||||
<p className="text-lg font-semibold text-gray-900">
|
||||
<p className="text-lg font-semibold">
|
||||
{`${peers.length} ${peers.length > 1 ? "Peers" : "Peer"}`}
|
||||
</p>
|
||||
) : (
|
||||
@@ -35,7 +35,6 @@ export const PeersWidget = ({ peers }: PeersWidgetProps): JSX.Element => {
|
||||
</div>
|
||||
<IconButton
|
||||
className="my-auto ml-auto"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setActivePage("peers");
|
||||
|
||||
@@ -8,16 +8,14 @@ export interface PositionWidgetProps {
|
||||
|
||||
export const PositionWidget = ({ grid }: PositionWidgetProps): JSX.Element => {
|
||||
return (
|
||||
<div className="flex gap-3 overflow-hidden rounded-lg bg-white p-3 shadow">
|
||||
<div className="rounded-md bg-rose-500 p-3">
|
||||
<MapPinIcon className="h-6 text-white" />
|
||||
<div className="flex gap-3 overflow-hidden rounded-lg bg-backgroundPrimary p-3 text-textSecondary">
|
||||
<div className="rounded-md bg-accent p-3 text-textPrimary">
|
||||
<MapPinIcon className="text-white h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="truncate text-sm font-medium text-gray-500">
|
||||
Current Location
|
||||
</p>
|
||||
<p className="truncate text-sm font-medium">Current Location</p>
|
||||
<div className="flex gap-1">
|
||||
<p className="text-lg font-semibold text-gray-900">{grid}</p>
|
||||
<p className="text-lg font-semibold">{grid}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -62,10 +62,10 @@ export const BitwiseSelect = ({
|
||||
multiple
|
||||
>
|
||||
<Listbox.Button
|
||||
className={`flex h-10 w-full items-center gap-2 rounded-md border-transparent bg-orange-100 px-3 text-sm focus:border-transparent focus:outline-none focus:ring-2 focus:ring-orange-500`}
|
||||
className={`bg-orange-100 focus:ring-orange-500 flex h-10 w-full items-center gap-2 rounded-md border-transparent px-3 text-sm focus:border-transparent focus:outline-none focus:ring-2`}
|
||||
>
|
||||
{decodedSelected.map((option) => (
|
||||
<span className="rounded-md bg-orange-300 p-1">{option}</span>
|
||||
<span className="bg-orange-300 rounded-md p-1">{option}</span>
|
||||
))}
|
||||
</Listbox.Button>
|
||||
<Listbox.Options>
|
||||
|
||||
@@ -3,16 +3,12 @@ import type { ButtonHTMLAttributes } from "react";
|
||||
|
||||
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
size?: "sm" | "md" | "lg";
|
||||
variant?: "primary" | "secondary";
|
||||
iconBefore?: JSX.Element;
|
||||
iconAfter?: JSX.Element;
|
||||
}
|
||||
|
||||
export const Button = ({
|
||||
size = "md",
|
||||
variant = "primary",
|
||||
iconBefore,
|
||||
iconAfter,
|
||||
children,
|
||||
disabled,
|
||||
className,
|
||||
@@ -20,11 +16,7 @@ export const Button = ({
|
||||
}: ButtonProps): JSX.Element => {
|
||||
return (
|
||||
<button
|
||||
className={`flex w-full rounded-md border border-transparent px-3 focus:outline-none focus:ring-2 focus:ring-orange-500 ${
|
||||
variant === "primary"
|
||||
? "bg-orange-600 text-white shadow-sm hover:bg-orange-700"
|
||||
: "bg-orange-200 text-orange-700 hover:bg-orange-300"
|
||||
} ${
|
||||
className={`flex w-full rounded-md bg-button px-3 text-textPrimary hover:brightness-hover focus:outline-none active:brightness-press ${
|
||||
size === "sm"
|
||||
? "h-8 text-sm"
|
||||
: size === "md"
|
||||
@@ -32,7 +24,7 @@ export const Button = ({
|
||||
: "h-10 text-base"
|
||||
} ${
|
||||
disabled
|
||||
? "cursor-not-allowed bg-gray-400 hover:bg-gray-400 focus:ring-gray-500"
|
||||
? "cursor-not-allowed text-textSecondary brightness-disabled hover:brightness-disabled"
|
||||
: ""
|
||||
} ${className}`}
|
||||
disabled={disabled}
|
||||
@@ -41,7 +33,6 @@ export const Button = ({
|
||||
<div className="m-auto flex shrink-0 items-center gap-2 font-medium">
|
||||
{iconBefore}
|
||||
{children}
|
||||
{iconAfter}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -13,8 +13,10 @@ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
|
||||
<input
|
||||
ref={ref}
|
||||
type="checkbox"
|
||||
className={`h-4 w-4 rounded border-transparent bg-orange-100 text-orange-500 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-orange-500 ${
|
||||
disabled ? "cursor-not-allowed bg-orange-50 text-orange-200" : ""
|
||||
className={`h-4 w-4 rounded border-none bg-backgroundPrimary text-accent focus:outline-none focus:ring-2 focus:ring-accent ${
|
||||
disabled
|
||||
? "bg-orange-50 cursor-not-allowed text-accent brightness-disabled"
|
||||
: ""
|
||||
}`}
|
||||
disabled={disabled}
|
||||
{...rest}
|
||||
@@ -23,7 +25,7 @@ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
|
||||
<div className="ml-3 text-sm">
|
||||
<label
|
||||
className={`font-medium ${
|
||||
disabled ? "text-gray-500" : "text-gray-700"
|
||||
disabled ? "text-textSecondary" : "text-textPrimary"
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
|
||||
@@ -29,23 +29,23 @@ export const Form = ({
|
||||
}: FormProps): JSX.Element => {
|
||||
return (
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
<form className="w-full" onSubmit={onSubmit} {...props}>
|
||||
<div className="select-none rounded-md bg-gray-700 p-4">
|
||||
<ol className="flex gap-4">
|
||||
<li className="cursor-pointer text-gray-400 hover:text-gray-200">
|
||||
<HomeIcon className="h-5 w-5 flex-shrink-0 text-gray-400" />
|
||||
<form className="w-full px-2" onSubmit={onSubmit} {...props}>
|
||||
<div className="select-none rounded-md bg-backgroundPrimary p-4">
|
||||
<ol className="flex gap-4 text-textSecondary">
|
||||
<li className="cursor-pointer hover:brightness-disabled">
|
||||
<HomeIcon className="h-5 w-5 flex-shrink-0" />
|
||||
</li>
|
||||
{breadcrumbs.map((breadcrumb, index) => (
|
||||
<li key={index} className="flex gap-4">
|
||||
<ChevronRightIcon className="h-5 w-5 flex-shrink-0 text-gray-500" />
|
||||
<span className="cursor-pointer text-sm font-medium text-gray-400 hover:text-gray-200">
|
||||
<ChevronRightIcon className="h-5 w-5 flex-shrink-0 brightness-disabled" />
|
||||
<span className="cursor-pointer text-sm font-medium hover:brightness-disabled">
|
||||
{breadcrumb}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
<div className="mt-2 flex items-center">
|
||||
<h2 className="truncate text-3xl font-bold tracking-tight text-white">
|
||||
<h2 className="text-3xl font-bold tracking-tight text-textPrimary">
|
||||
{title}
|
||||
</h2>
|
||||
<div className="ml-auto flex gap-2">
|
||||
@@ -54,7 +54,6 @@ export const Form = ({
|
||||
onClick={() => {
|
||||
reset();
|
||||
}}
|
||||
variant="secondary"
|
||||
iconBefore={<ArrowUturnLeftIcon className="w-4" />}
|
||||
>
|
||||
Reset
|
||||
|
||||
@@ -11,10 +11,10 @@ export const FormSection = ({
|
||||
}: FormSectionProps): JSX.Element => {
|
||||
return (
|
||||
<div className="relative">
|
||||
<h3 className="absolute left-2 -top-2 bg-white px-1 text-lg font-medium">
|
||||
<h3 className="absolute left-2 -top-2 bg-backgroundSecondary px-1 text-lg font-medium text-textPrimary">
|
||||
{title}
|
||||
</h3>
|
||||
<div className="mt-2 rounded-md border-2 border-orange-200 p-2">
|
||||
<div className="mt-2 rounded-md border-2 border-backgroundPrimary p-2">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,13 +4,11 @@ import type { ButtonHTMLAttributes } from "react";
|
||||
export interface IconButtonProps
|
||||
extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
size?: "sm" | "md" | "lg";
|
||||
variant?: "primary" | "secondary";
|
||||
icon?: JSX.Element;
|
||||
}
|
||||
|
||||
export const IconButton = ({
|
||||
size = "md",
|
||||
variant = "primary",
|
||||
icon,
|
||||
disabled,
|
||||
className,
|
||||
@@ -18,15 +16,13 @@ export const IconButton = ({
|
||||
}: IconButtonProps): JSX.Element => {
|
||||
return (
|
||||
<button
|
||||
className={`flex rounded-md border border-transparent focus:outline-none focus:ring-2 focus:ring-orange-500 ${
|
||||
variant === "primary"
|
||||
? "bg-orange-600 text-white shadow-sm hover:bg-orange-700"
|
||||
: "bg-orange-200 text-orange-700 hover:bg-orange-300"
|
||||
} ${
|
||||
className={`flex rounded-md bg-button text-textPrimary hover:text-accent hover:brightness-hover focus:outline-none active:brightness-press ${
|
||||
size === "sm" ? "h-8 w-8" : size === "md" ? "h-10 w-10" : "h-12 w-12"
|
||||
} ${disabled ? "cursor-not-allowed bg-red-400 focus:ring-red-500" : ""} ${
|
||||
className ?? ""
|
||||
}`}
|
||||
} ${
|
||||
disabled
|
||||
? "cursor-not-allowed text-textSecondary brightness-disabled hover:brightness-disabled"
|
||||
: ""
|
||||
} ${className ?? ""}`}
|
||||
disabled={disabled}
|
||||
{...rest}
|
||||
>
|
||||
|
||||
@@ -19,7 +19,7 @@ export const InfoWrapper = ({
|
||||
<div className="w-full">
|
||||
{/* Label */}
|
||||
{label && (
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
<label className="block text-sm font-medium text-textPrimary">
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
@@ -27,13 +27,13 @@ export const InfoWrapper = ({
|
||||
{children}
|
||||
{error && (
|
||||
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
|
||||
<ExclamationCircleIcon className="h-5 w-5 text-red-500" />
|
||||
<ExclamationCircleIcon className="text-red-500 h-5 w-5" />
|
||||
</div>
|
||||
)}
|
||||
{description && (
|
||||
<p className="mt-2 text-sm text-gray-500">{description}</p>
|
||||
<p className="mt-2 text-sm text-textSecondary">{description}</p>
|
||||
)}
|
||||
{error && <p className="mt-2 text-sm text-red-600">{error}</p>}
|
||||
{error && <p className="text-red-600 mt-2 text-sm">{error}</p>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -30,25 +30,27 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
|
||||
) {
|
||||
return (
|
||||
<InfoWrapper label={label} description={description} error={error}>
|
||||
<div className="relative flex rounded-md shadow-sm">
|
||||
<div className="relative flex rounded-md">
|
||||
{prefix && (
|
||||
<span className="inline-flex items-center rounded-l-md border-gray-300 bg-orange-200 px-3 font-mono text-sm">
|
||||
<span className="inline-flex items-center rounded-l-md bg-backgroundPrimary px-3 font-mono text-sm text-textSecondary brightness-hover">
|
||||
{prefix}
|
||||
</span>
|
||||
)}
|
||||
<input
|
||||
ref={ref}
|
||||
className={`flex h-10 w-full rounded-md border-transparent bg-orange-100 px-3 text-sm focus:border-transparent focus:outline-none focus:ring-2 focus:ring-orange-500 ${
|
||||
className={`flex h-10 w-full rounded-md border-none bg-backgroundPrimary px-3 text-sm text-textPrimary focus:outline-none focus:ring-2 focus:ring-accent ${
|
||||
prefix ? "rounded-l-none" : ""
|
||||
} ${action ? "rounded-r-none" : ""} ${
|
||||
disabled ? "cursor-not-allowed bg-orange-50 text-orange-200" : ""
|
||||
disabled
|
||||
? "cursor-not-allowed text-textSecondary brightness-disabled hover:brightness-disabled"
|
||||
: ""
|
||||
}`}
|
||||
disabled={disabled}
|
||||
step="any"
|
||||
{...rest}
|
||||
/>
|
||||
{suffix && (
|
||||
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3 font-mono">
|
||||
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3 font-mono text-textSecondary">
|
||||
<span className="text-gray-500 sm:text-sm">{suffix}</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -56,14 +58,14 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
|
||||
<button
|
||||
type="button"
|
||||
onClick={action.action}
|
||||
className="relative -ml-px inline-flex items-center space-x-2 rounded-r-md bg-orange-200 px-4 py-2 text-sm font-medium text-orange-700 hover:bg-orange-300 focus:outline-none focus:ring-2 focus:ring-orange-500"
|
||||
className="relative -ml-px inline-flex items-center space-x-2 rounded-r-md bg-backgroundPrimary px-4 py-2 text-sm font-medium text-textSecondary brightness-hover hover:text-accent hover:brightness-hover focus:outline-none focus:ring-2 focus:ring-accent active:brightness-press"
|
||||
>
|
||||
{action.icon}
|
||||
</button>
|
||||
)}
|
||||
{error && (
|
||||
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
|
||||
<ExclamationCircleIcon className="h-5 w-5 text-red-500" />
|
||||
<ExclamationCircleIcon className="text-red-500 h-5 w-5" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -28,13 +28,15 @@ export const Select = forwardRef<HTMLSelectElement, SelectProps>(function Input(
|
||||
) {
|
||||
return (
|
||||
<InfoWrapper label={label} description={description} error={error}>
|
||||
<div className="flex rounded-md shadow-sm">
|
||||
<div className="flex rounded-md">
|
||||
<select
|
||||
ref={ref}
|
||||
className={`flex h-10 w-full rounded-md border-transparent bg-orange-100 px-3 text-sm focus:border-transparent focus:outline-none focus:ring-2 focus:ring-orange-500 ${
|
||||
className={`flex h-10 w-full rounded-md border-transparent bg-backgroundPrimary px-3 text-sm text-textPrimary focus:border-transparent focus:outline-none focus:ring-2 focus:ring-accent ${
|
||||
action ? "rounded-r-none" : ""
|
||||
} ${
|
||||
disabled ? "cursor-not-allowed bg-orange-50 text-orange-200" : ""
|
||||
disabled
|
||||
? "cursor-not-allowed text-textSecondary brightness-disabled hover:brightness-disabled"
|
||||
: ""
|
||||
}`}
|
||||
disabled={disabled}
|
||||
{...rest}
|
||||
@@ -49,7 +51,7 @@ export const Select = forwardRef<HTMLSelectElement, SelectProps>(function Input(
|
||||
<button
|
||||
type="button"
|
||||
onClick={action.action}
|
||||
className="relative -ml-px inline-flex items-center space-x-2 rounded-r-md bg-orange-200 px-4 py-2 text-sm font-medium hover:bg-orange-300 focus:outline-none focus:ring-2 focus:ring-orange-500"
|
||||
className="relative -ml-px inline-flex items-center space-x-2 rounded-r-md bg-backgroundPrimary px-4 py-2 text-sm font-medium text-textSecondary brightness-hover hover:brightness-hover focus:outline-none focus:ring-2 focus:ring-accent active:brightness-press"
|
||||
>
|
||||
{action.icon}
|
||||
</button>
|
||||
|
||||
@@ -28,14 +28,14 @@ export const Toggle = ({
|
||||
{label && (
|
||||
<Switch.Label
|
||||
as="span"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
className="block text-sm font-medium text-textPrimary"
|
||||
passive
|
||||
>
|
||||
{label}
|
||||
</Switch.Label>
|
||||
)}
|
||||
{description && (
|
||||
<Switch.Description as="span" className="text-sm text-gray-500">
|
||||
<Switch.Description as="span" className="text-sm text-textSecondary">
|
||||
{description}
|
||||
</Switch.Description>
|
||||
)}
|
||||
@@ -44,12 +44,12 @@ export const Toggle = ({
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
onChange={onChange}
|
||||
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-orange-500 focus:ring-offset-2 ${
|
||||
checked ? "bg-orange-600" : "bg-orange-100"
|
||||
} ${disabled ? "cursor-not-allowed bg-orange-200" : ""}`}
|
||||
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-accent ${
|
||||
checked ? "bg-accent" : "bg-backgroundPrimary"
|
||||
} ${disabled ? "bg-orange-200 cursor-not-allowed" : ""}`}
|
||||
>
|
||||
<span
|
||||
className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
|
||||
className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-backgroundSecondary ring-0 transition duration-200 ease-in-out ${
|
||||
checked ? "translate-x-5" : "translate-x-0"
|
||||
}`}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export const Blur = (): JSX.Element => {
|
||||
return <div className="fixed inset-0 backdrop-blur-md" aria-hidden="true" />;
|
||||
};
|
||||
@@ -3,6 +3,8 @@ import type React from "react";
|
||||
import { IconButton } from "@components/form/IconButton.js";
|
||||
import { Dialog as DialogUI } from "@headlessui/react";
|
||||
import { XMarkIcon } from "@heroicons/react/24/outline";
|
||||
import { ThemeController } from "./ThemeController.js";
|
||||
import { Blur } from "./Blur.js";
|
||||
|
||||
export interface DialogProps {
|
||||
title: string;
|
||||
@@ -21,28 +23,27 @@ export const Dialog = ({
|
||||
}: DialogProps): JSX.Element => {
|
||||
return (
|
||||
<DialogUI open={isOpen} onClose={close}>
|
||||
<div className="fixed inset-0 bg-black/30" aria-hidden="true" />
|
||||
<div className="fixed inset-0 flex items-center justify-center p-4">
|
||||
<DialogUI.Panel>
|
||||
<div className="divide-y divide-gray-200 overflow-hidden rounded-lg bg-white shadow">
|
||||
<div className="flex px-4 py-5 sm:px-6">
|
||||
<ThemeController>
|
||||
<Blur />
|
||||
<div className="fixed inset-0 flex items-center justify-center p-4">
|
||||
<DialogUI.Panel>
|
||||
<div className="flex bg-backgroundPrimary px-4 py-5 sm:px-6">
|
||||
<div>
|
||||
<h1 className="text-lg font-bold">{title}</h1>
|
||||
<h5 className="text-sm text-slate-600">{description}</h5>
|
||||
<h1 className="text-lg font-bold text-textPrimary">{title}</h1>
|
||||
<h5 className="text-sm text-textSecondary">{description}</h5>
|
||||
</div>
|
||||
<IconButton
|
||||
onClick={close}
|
||||
className="my-auto ml-auto"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
icon={<XMarkIcon className="h-4" />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="p-4">{children}</div>
|
||||
</div>
|
||||
</DialogUI.Panel>
|
||||
</div>
|
||||
<div className="bg-backgroundSecondary p-4">{children}</div>
|
||||
</DialogUI.Panel>
|
||||
</div>
|
||||
</ThemeController>
|
||||
</DialogUI>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ export const Mono = ({
|
||||
}: JSX.IntrinsicElements["span"]): JSX.Element => {
|
||||
return (
|
||||
<span
|
||||
className={`font-mono text-sm text-slate-500 ${className ?? ""}`}
|
||||
className={`font-mono text-sm text-textSecondary ${className ?? ""}`}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -23,21 +23,25 @@ export const TabbedContent = ({
|
||||
actions
|
||||
}: TabbedContentProps): JSX.Element => {
|
||||
return (
|
||||
<Tab.Group as="div" className="flex flex-grow flex-col gap-2 p-4">
|
||||
<Tab.List className="flex gap-4 border-b pb-3">
|
||||
<Tab.Group as="div" className="flex flex-grow flex-col gap-2">
|
||||
<Tab.List className="flex bg-backgroundPrimary">
|
||||
{tabs.map((entry, index) => (
|
||||
<Tab key={index}>
|
||||
<Tab key={index} disabled={entry.disabled}>
|
||||
{({ selected }) => (
|
||||
<div
|
||||
className={`flex h-10 cursor-pointer gap-3 rounded-md px-3 text-sm font-medium ${
|
||||
className={`flex h-10 gap-3 truncate border-b-2 px-3 text-sm font-medium ${
|
||||
selected
|
||||
? "bg-gray-100 text-gray-700"
|
||||
: "text-gray-500 hover:text-gray-700"
|
||||
? "border-accent text-textPrimary"
|
||||
: "border-backgroundPrimary text-textSecondary hover:text-textPrimary"
|
||||
} ${
|
||||
entry.disabled
|
||||
? "cursor-not-allowed hover:text-textSecondary"
|
||||
: "cursor-pointer"
|
||||
}
|
||||
`}
|
||||
>
|
||||
{entry.icon && (
|
||||
<div className="m-auto text-slate-500">{entry.icon}</div>
|
||||
<div className="text-slate-500 m-auto">{entry.icon}</div>
|
||||
)}
|
||||
<span className="m-auto">{entry.name}</span>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { useAppStore } from "@app/core/stores/appStore.js";
|
||||
import type React from "react";
|
||||
|
||||
export interface ThemeControllerProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const ThemeController = ({
|
||||
children
|
||||
}: ThemeControllerProps): JSX.Element => {
|
||||
const { darkMode, accent } = useAppStore();
|
||||
|
||||
return (
|
||||
<div data-theme={darkMode ? "dark" : "light"} data-accent={accent}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -8,6 +8,15 @@ export interface RasterSource {
|
||||
tileSize: number;
|
||||
}
|
||||
|
||||
export type accentColor =
|
||||
| "red"
|
||||
| "orange"
|
||||
| "yellow"
|
||||
| "green"
|
||||
| "blue"
|
||||
| "purple"
|
||||
| "pink";
|
||||
|
||||
interface AppState {
|
||||
selectedDevice: number;
|
||||
devices: {
|
||||
@@ -16,6 +25,8 @@ interface AppState {
|
||||
}[];
|
||||
rasterSources: RasterSource[];
|
||||
commandPaletteOpen: boolean;
|
||||
darkMode: boolean;
|
||||
accent: accentColor;
|
||||
|
||||
setRasterSources: (sources: RasterSource[]) => void;
|
||||
addRasterSource: (source: RasterSource) => void;
|
||||
@@ -25,6 +36,8 @@ interface AppState {
|
||||
addDevice: (device: { id: number; num: number }) => void;
|
||||
removeDevice: (deviceId: number) => void;
|
||||
setCommandPaletteOpen: (open: boolean) => void;
|
||||
setDarkMode: (enabled: boolean) => void;
|
||||
setAccent: (color: accentColor) => void;
|
||||
}
|
||||
|
||||
export const useAppStore = create<AppState>()((set) => ({
|
||||
@@ -33,6 +46,8 @@ export const useAppStore = create<AppState>()((set) => ({
|
||||
currentPage: "messages",
|
||||
rasterSources: [],
|
||||
commandPaletteOpen: false,
|
||||
darkMode: true,
|
||||
accent: "orange",
|
||||
|
||||
setRasterSources: (sources: RasterSource[]) => {
|
||||
set(
|
||||
@@ -73,5 +88,19 @@ export const useAppStore = create<AppState>()((set) => ({
|
||||
draft.commandPaletteOpen = open;
|
||||
})
|
||||
);
|
||||
},
|
||||
setDarkMode: (enabled: boolean) => {
|
||||
set(
|
||||
produce<AppState>((draft) => {
|
||||
draft.darkMode = enabled;
|
||||
})
|
||||
);
|
||||
},
|
||||
setAccent(color) {
|
||||
set(
|
||||
produce<AppState>((draft) => {
|
||||
draft.accent = color;
|
||||
})
|
||||
);
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -518,23 +518,18 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
|
||||
);
|
||||
},
|
||||
ackMessage: (channelIndex: number, messageId: number) => {
|
||||
console.log("ack called");
|
||||
|
||||
set(
|
||||
produce<DeviceState>((draft) => {
|
||||
const device = draft.devices.get(id);
|
||||
if (device) {
|
||||
const channel = device.channels.find(
|
||||
(ch) => ch.config.index === channelIndex
|
||||
);
|
||||
if (channel) {
|
||||
const message = channel.messages.find(
|
||||
(msg) => msg.packet.id === messageId
|
||||
);
|
||||
if (message) {
|
||||
message.ack = true;
|
||||
}
|
||||
}
|
||||
|
||||
const channel = device?.channels.find(
|
||||
(ch) => ch.config.index === channelIndex
|
||||
);
|
||||
const message = channel?.messages.find(
|
||||
(msg) => msg.packet.id === messageId
|
||||
);
|
||||
if (message) {
|
||||
message.ack = true;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { toast } from "react-hot-toast";
|
||||
|
||||
import type { Device } from "@core/stores/deviceStore.js";
|
||||
import { Types } from "@meshtastic/meshtasticjs";
|
||||
import { Protobuf, Types } from "@meshtastic/meshtasticjs";
|
||||
|
||||
export const subscribeAll = (
|
||||
device: Device,
|
||||
@@ -17,7 +17,33 @@ export const subscribeAll = (
|
||||
});
|
||||
|
||||
connection.onRoutingPacket.subscribe((routingPacket) => {
|
||||
console.log(routingPacket);
|
||||
switch (routingPacket.data.variant.oneofKind) {
|
||||
case "errorReason":
|
||||
if (
|
||||
routingPacket.data.variant.errorReason === Protobuf.Routing_Error.NONE
|
||||
) {
|
||||
return;
|
||||
}
|
||||
toast.error(
|
||||
`Routing error: ${
|
||||
Protobuf.Routing_Error[routingPacket.data.variant.errorReason]
|
||||
}`,
|
||||
{
|
||||
icon: "❌"
|
||||
}
|
||||
);
|
||||
break;
|
||||
case "routeReply":
|
||||
toast(`Route Reply: ${routingPacket.data.variant.routeReply}`, {
|
||||
icon: "✅"
|
||||
});
|
||||
break;
|
||||
case "routeRequest":
|
||||
toast(`Route Request: ${routingPacket.data.variant.routeRequest}`, {
|
||||
icon: "✅"
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
connection.onTelemetryPacket.subscribe((telemetryPacket) => {
|
||||
|
||||
@@ -1,3 +1,87 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--backgroundPrimary: #f5f5f6;
|
||||
--backgroundSecondary: #e6e9ed;
|
||||
--accent: #70afea;
|
||||
--button: #cfd5dd;
|
||||
--textPrimary: #111132;
|
||||
--textSecondary: #64748b;
|
||||
--link: #0b69bf;
|
||||
|
||||
--brighnessHover: "0.95";
|
||||
--brightnessPress: "1.05";
|
||||
--brightnessDisabled: "0.75";
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--backgroundPrimary: #2d2d30;
|
||||
--backgroundSecondary: #363638;
|
||||
--accent: #2093fe;
|
||||
--button: #595959;
|
||||
--textPrimary: #ebebeb;
|
||||
--textSecondary: #bdbdbd;
|
||||
--link: #8ec9ff;
|
||||
|
||||
--brighnessHover: 1.1;
|
||||
--brightnessPress: 0.9;
|
||||
--brightnessDisabled: 0.75;
|
||||
}
|
||||
|
||||
[data-accent="red"] {
|
||||
--accent: #f28585;
|
||||
}
|
||||
|
||||
[data-accent="red"][data-theme="dark"] {
|
||||
--accent: #f25555;
|
||||
}
|
||||
|
||||
[data-accent="orange"] {
|
||||
--accent: #edb17a;
|
||||
}
|
||||
|
||||
[data-accent="orange"][data-theme="dark"] {
|
||||
--accent: #e1720b;
|
||||
}
|
||||
|
||||
[data-accent="yellow"] {
|
||||
--accent: #e0cc87;
|
||||
}
|
||||
|
||||
[data-accent="yellow"][data-theme="dark"] {
|
||||
--accent: #ac8c1a;
|
||||
}
|
||||
|
||||
[data-accent="green"] {
|
||||
--accent: #8bc9c5;
|
||||
}
|
||||
|
||||
[data-accent="green"][data-theme="dark"] {
|
||||
--accent: #27a341;
|
||||
}
|
||||
|
||||
[data-accent="blue"] {
|
||||
--accent: #70afea;
|
||||
}
|
||||
|
||||
[data-accent="blue"][data-theme="dark"] {
|
||||
--accent: #2093fe;
|
||||
}
|
||||
|
||||
[data-accent="purple"] {
|
||||
--accent: #a09eef;
|
||||
}
|
||||
|
||||
[data-accent="purple"][data-theme="dark"] {
|
||||
--accent: #926bff;
|
||||
}
|
||||
|
||||
[data-accent="pink"] {
|
||||
--accent: #dba0c7;
|
||||
}
|
||||
|
||||
[data-accent="pink"][data-theme="dark"] {
|
||||
--accent: #e454c4;
|
||||
}
|
||||
@@ -30,7 +30,6 @@ export const ChannelsPage = (): JSX.Element => {
|
||||
actions={[
|
||||
() => (
|
||||
<Button
|
||||
variant="secondary"
|
||||
iconBefore={<ArrowDownOnSquareStackIcon className="w-4" />}
|
||||
onClick={() => {
|
||||
setImportDialogOpen(true);
|
||||
@@ -41,7 +40,6 @@ export const ChannelsPage = (): JSX.Element => {
|
||||
),
|
||||
() => (
|
||||
<Button
|
||||
variant="secondary"
|
||||
iconBefore={<QrCodeIcon className="w-4" />}
|
||||
onClick={() => {
|
||||
setQRDialogOpen(true);
|
||||
|
||||
@@ -41,7 +41,6 @@ export const ConfigPage = (): JSX.Element => {
|
||||
<Button
|
||||
disabled={!pendingSettingsChanges}
|
||||
onClick={connection?.commitEditSettings}
|
||||
variant="primary"
|
||||
>
|
||||
Commit Changes
|
||||
</Button>
|
||||
|
||||
+11
-11
@@ -17,31 +17,31 @@ export const LogsPage = (): JSX.Element => {
|
||||
|
||||
return (
|
||||
<div className="w-full overflow-y-auto">
|
||||
<div className="overflow-hidden ring-1 ring-black ring-opacity-5">
|
||||
<table className="min-w-full divide-y divide-gray-300">
|
||||
<div className="ring-black overflow-hidden ring-1 ring-opacity-5">
|
||||
<table className="divide-gray-300 min-w-full divide-y">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
className="py-3.5 pr-3 pl-6 text-left text-sm font-semibold text-gray-900"
|
||||
className="text-gray-900 py-3.5 pr-3 pl-6 text-left text-sm font-semibold"
|
||||
>
|
||||
Emitter
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="py-3.5 text-left text-sm font-semibold text-gray-900"
|
||||
className="text-gray-900 py-3.5 text-left text-sm font-semibold"
|
||||
>
|
||||
Level
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="py-3.5 text-left text-sm font-semibold text-gray-900"
|
||||
className="text-gray-900 py-3.5 text-left text-sm font-semibold"
|
||||
>
|
||||
Message
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="py-3.5 text-left text-sm font-semibold text-gray-900"
|
||||
className="text-gray-900 py-3.5 text-left text-sm font-semibold"
|
||||
>
|
||||
Scope
|
||||
</th>
|
||||
@@ -53,18 +53,18 @@ export const LogsPage = (): JSX.Element => {
|
||||
key={index}
|
||||
className={index % 2 === 0 ? undefined : "bg-gray-50"}
|
||||
>
|
||||
<td className="whitespace-nowrap py-2 pl-6 text-sm text-gray-500">
|
||||
<td className="text-gray-500 whitespace-nowrap py-2 pl-6 text-sm">
|
||||
<span className="my-auto">{Types.Emitter[log.emitter]}</span>
|
||||
</td>
|
||||
<td className="whitespace-nowrap py-2 text-sm text-gray-500">
|
||||
<span className="rounded-md bg-slate-200 p-1">
|
||||
<td className="text-gray-500 whitespace-nowrap py-2 text-sm">
|
||||
<span className="bg-slate-200 rounded-md p-1">
|
||||
<Mono>{[Protobuf.LogRecord_Level[log.level]]}</Mono>
|
||||
</span>
|
||||
</td>
|
||||
<td className="whitespace-nowrap py-2 text-sm text-gray-500">
|
||||
<td className="text-gray-500 whitespace-nowrap py-2 text-sm">
|
||||
<Mono>{log.message}</Mono>
|
||||
</td>
|
||||
<td className="whitespace-nowrap py-2 text-sm text-gray-500">
|
||||
<td className="text-gray-500 whitespace-nowrap py-2 text-sm">
|
||||
{Types.EmitterScope[log.scope]}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -32,7 +32,6 @@ export const MessagesPage = (): JSX.Element => {
|
||||
actions={[
|
||||
() => (
|
||||
<IconButton
|
||||
variant="secondary"
|
||||
icon={<PencilIcon className="h-4" />}
|
||||
onClick={() => {
|
||||
setActivePage("channels");
|
||||
|
||||
+90
-115
@@ -18,126 +18,101 @@ export const PeersPage = (): JSX.Element => {
|
||||
|
||||
return (
|
||||
<div className="w-full overflow-y-auto">
|
||||
<div className="overflow-hidden ring-1 ring-black ring-opacity-5">
|
||||
<table className="min-w-full divide-y divide-gray-300">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
className="py-3.5 pr-3 pl-6 text-left text-sm font-semibold text-gray-900"
|
||||
>
|
||||
Name
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="py-3.5 text-left text-sm font-semibold text-gray-900"
|
||||
>
|
||||
Model
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="py-3.5 text-left text-sm font-semibold text-gray-900"
|
||||
>
|
||||
MAC Address
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="py-3.5 text-left text-sm font-semibold text-gray-900"
|
||||
>
|
||||
Versions
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="py-3.5 text-left text-sm font-semibold text-gray-900"
|
||||
>
|
||||
Last Heard
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="py-3.5 text-left text-sm font-semibold text-gray-900"
|
||||
>
|
||||
SNR
|
||||
</th>
|
||||
<th scope="col" className="relative py-3.5 pl-3 pr-4 sm:pr-6">
|
||||
<span className="sr-only">Edit</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white">
|
||||
{nodes.map((node, index) => (
|
||||
<tr
|
||||
key={index}
|
||||
className={index % 2 === 0 ? undefined : "bg-gray-50"}
|
||||
>
|
||||
<td className="flex gap-2 whitespace-nowrap py-2 pr-3 pl-6 text-sm font-medium text-gray-900">
|
||||
<Hashicon size={24} value={node.data.num.toString()} />
|
||||
<span className="my-auto">
|
||||
{node.data.user?.longName ??
|
||||
`Meshtastic_${base16
|
||||
.stringify(node.data.user?.macaddr.subarray(4, 6) ?? [])
|
||||
.toLowerCase()}`}
|
||||
</span>
|
||||
</td>
|
||||
<td className="whitespace-nowrap py-2 text-sm text-gray-500">
|
||||
<span className="rounded-md bg-slate-200 p-1">
|
||||
<Mono>
|
||||
{Protobuf.HardwareModel[node.data.user?.hwModel ?? 0]}
|
||||
</Mono>
|
||||
</span>
|
||||
</td>
|
||||
<td className="whitespace-nowrap py-2 text-sm text-gray-500">
|
||||
<table className="min-w-full">
|
||||
<thead className="bg-backgroundPrimary text-sm font-semibold text-textPrimary">
|
||||
<tr>
|
||||
<th scope="col" className="py-2 pr-3 pl-6 text-left">
|
||||
Name
|
||||
</th>
|
||||
<th scope="col" className="py-2 text-left">
|
||||
Model
|
||||
</th>
|
||||
<th scope="col" className="py-2 text-left">
|
||||
MAC Address
|
||||
</th>
|
||||
<th scope="col" className="py-2 text-left">
|
||||
Versions
|
||||
</th>
|
||||
<th scope="col" className="py-2 text-left">
|
||||
Last Heard
|
||||
</th>
|
||||
<th scope="col" className="py-2 text-left">
|
||||
SNR
|
||||
</th>
|
||||
<th scope="col" className="relative py-2 pl-3 pr-4 sm:pr-6">
|
||||
<span className="sr-only">Edit</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{nodes.map((node, index) => (
|
||||
<tr key={index}>
|
||||
<td className="flex gap-2 whitespace-nowrap py-2 pr-3 pl-6 text-sm font-medium text-textPrimary">
|
||||
<Hashicon size={24} value={node.data.num.toString()} />
|
||||
<span className="my-auto">
|
||||
{node.data.user?.longName ??
|
||||
`Meshtastic_${base16
|
||||
.stringify(node.data.user?.macaddr.subarray(4, 6) ?? [])
|
||||
.toLowerCase()}`}
|
||||
</span>
|
||||
</td>
|
||||
<td className="whitespace-nowrap py-2 text-sm text-textSecondary">
|
||||
<span className="bg-slate-200 rounded-md p-1">
|
||||
<Mono>
|
||||
{base16
|
||||
.stringify(node.data.user?.macaddr ?? [])
|
||||
.match(/.{1,2}/g)
|
||||
?.join(":") ?? ""}
|
||||
{Protobuf.HardwareModel[node.data.user?.hwModel ?? 0]}
|
||||
</Mono>
|
||||
</td>
|
||||
<td className="whitespace-nowrap py-2 text-sm text-gray-500">
|
||||
{node.metadata ? (
|
||||
<>
|
||||
<Mono>{node.metadata.firmwareVersion}</Mono>
|
||||
<span className="text-black">/</span>
|
||||
<Mono>{node.metadata.deviceStateVersion}</Mono>
|
||||
</>
|
||||
) : (
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
if (connection) {
|
||||
void toast.promise(
|
||||
connection.getMetadata({ nodeNum: node.data.num }),
|
||||
{
|
||||
loading: "Requesting Metadata...",
|
||||
success: "Recieved Metadata",
|
||||
error: "No response received"
|
||||
}
|
||||
);
|
||||
}
|
||||
}}
|
||||
icon={<ArrowPathRoundedSquareIcon className="h-4" />}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td className="whitespace-nowrap py-2 text-sm text-gray-500">
|
||||
{new Date(node.data.lastHeard).toLocaleTimeString()}
|
||||
</td>
|
||||
<td className="whitespace-nowrap py-2 text-sm text-gray-500">
|
||||
<Mono>{node.data.snr}db</Mono>
|
||||
</td>
|
||||
<td className="relative whitespace-nowrap pl-3 pr-4 text-right text-sm font-medium">
|
||||
</span>
|
||||
</td>
|
||||
<td className="whitespace-nowrap py-2 text-sm text-textSecondary">
|
||||
<Mono>
|
||||
{base16
|
||||
.stringify(node.data.user?.macaddr ?? [])
|
||||
.match(/.{1,2}/g)
|
||||
?.join(":") ?? ""}
|
||||
</Mono>
|
||||
</td>
|
||||
<td className="whitespace-nowrap py-2 text-sm text-textSecondary">
|
||||
{node.metadata ? (
|
||||
<>
|
||||
<Mono>{node.metadata.firmwareVersion}</Mono>
|
||||
<span className="text-black">/</span>
|
||||
<Mono>{node.metadata.deviceStateVersion}</Mono>
|
||||
</>
|
||||
) : (
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
icon={<EllipsisHorizontalIcon className="h-4" />}
|
||||
onClick={() => {
|
||||
if (connection) {
|
||||
void toast.promise(
|
||||
connection.getMetadata({ nodeNum: node.data.num }),
|
||||
{
|
||||
loading: "Requesting Metadata...",
|
||||
success: "Recieved Metadata",
|
||||
error: "No response received"
|
||||
}
|
||||
);
|
||||
}
|
||||
}}
|
||||
icon={<ArrowPathRoundedSquareIcon className="h-4" />}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="whitespace-nowrap py-2 text-sm text-textSecondary">
|
||||
{new Date(node.data.lastHeard).toLocaleTimeString()}
|
||||
</td>
|
||||
<td className="whitespace-nowrap py-2 text-sm text-textSecondary">
|
||||
<Mono>{node.data.snr}db</Mono>
|
||||
</td>
|
||||
<td className="relative whitespace-nowrap pl-3 pr-4 text-right text-sm font-medium">
|
||||
<IconButton
|
||||
size="sm"
|
||||
icon={<EllipsisHorizontalIcon className="h-4" />}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user