chore: fixes from deno linting/formatting
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"imports": {
|
||||
"@meshtastic/core": "jsr:@meshtastic/core@^2.6.0",
|
||||
"@meshtastic/js": "jsr:@meshtastic/js@^2.3.4",
|
||||
"@meshtastic/transport-http": "jsr:@meshtastic/transport-http@^0.2.1",
|
||||
"@meshtastic/transport-web-serial": "jsr:@meshtastic/transport-web-serial@^0.2.1",
|
||||
"@app/": "./src/",
|
||||
"@pages/": "./src/pages/",
|
||||
"@components/": "./src/components/",
|
||||
"@core/": "./src/core/",
|
||||
"@layouts/": "./src/layouts/"
|
||||
},
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"DOM",
|
||||
"DOM.Iterable",
|
||||
"ESNext"
|
||||
],
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"strictNullChecks": true,
|
||||
"types": [
|
||||
"vite/client",
|
||||
"node",
|
||||
"@types/web-bluetooth",
|
||||
"@types/w3c-web-serial"
|
||||
],
|
||||
"strictPropertyInitialization": false
|
||||
},
|
||||
"unstable": [
|
||||
"sloppy-imports"
|
||||
]
|
||||
}
|
||||
+11
-3
@@ -8,8 +8,8 @@
|
||||
"build": "deno run -A npm:vite build",
|
||||
"build:analyze": "BUNDLE_ANALYZE=true deno task build",
|
||||
"lint": "deno lint src/",
|
||||
"lint:fix": "deno lint --write src/",
|
||||
"format": "deno fmt --write src/",
|
||||
"lint:fix": "deno lint --fix src/",
|
||||
"format": "deno fmt src/",
|
||||
"dev": "deno task dev:ui",
|
||||
"dev:ui": "deno run -A npm:vite dev",
|
||||
"dev:scan": "VITE_DEBUG_SCAN=true deno task dev:ui",
|
||||
@@ -18,7 +18,6 @@
|
||||
"preview": "deno run --allow-net npm:vite preview",
|
||||
"package": "deno run -A npm:gzipper c -i html,js,css,png,ico,svg,webmanifest,txt dist dist/output && deno run -A --allow-run tar -cvf dist/build.tar -C ./dist/output/ ."
|
||||
},
|
||||
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/meshtastic/web.git"
|
||||
@@ -26,6 +25,15 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/meshtastic/web/issues"
|
||||
},
|
||||
"simple-git-hooks": {
|
||||
"pre-commit": "deno task lint:fix && deno task format"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,tsx}": [
|
||||
"deno task lint:fix",
|
||||
"deno task format"
|
||||
]
|
||||
},
|
||||
"homepage": "https://meshtastic.org",
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "^2.2.3",
|
||||
|
||||
+19
-18
@@ -1,20 +1,19 @@
|
||||
import { DeviceWrapper } from "@app/DeviceWrapper.tsx";
|
||||
import { PageRouter } from "@app/PageRouter.tsx";
|
||||
import { ThemeProvider } from "@app/components/generic/ThemeProvider";
|
||||
import { CommandPalette } from "@components/CommandPalette.tsx";
|
||||
import { DeviceSelector } from "@components/DeviceSelector.tsx";
|
||||
import { DialogManager } from "@components/Dialog/DialogManager";
|
||||
import { DialogManager } from "@components/Dialog/DialogManager.tsx";
|
||||
import { NewDeviceDialog } from "@components/Dialog/NewDeviceDialog.tsx";
|
||||
import { KeyBackupReminder } from "@components/KeyBackupReminder";
|
||||
import { KeyBackupReminder } from "@components/KeyBackupReminder.tsx";
|
||||
import { Toaster } from "@components/Toaster.tsx";
|
||||
import Footer from "@components/UI/Footer.tsx";
|
||||
import { useAppStore } from "@core/stores/appStore.ts";
|
||||
import { useDeviceStore } from "@core/stores/deviceStore.ts";
|
||||
import { Dashboard } from "@pages/Dashboard/index.tsx";
|
||||
import type { JSX } from "react";
|
||||
|
||||
import { MapProvider } from "react-map-gl/maplibre";
|
||||
|
||||
export const App = (): JSX.Element => {
|
||||
export const App = () => {
|
||||
const { getDevice } = useDeviceStore();
|
||||
const { selectedDevice, setConnectDialogOpen, connectDialogOpen } =
|
||||
useAppStore();
|
||||
@@ -36,19 +35,21 @@ export const App = (): JSX.Element => {
|
||||
<div className="flex grow">
|
||||
<DeviceSelector />
|
||||
<div className="flex grow flex-col">
|
||||
{device ? (
|
||||
<div className="flex h-screen">
|
||||
<DialogManager />
|
||||
<KeyBackupReminder />
|
||||
<CommandPalette />
|
||||
<PageRouter />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Dashboard />
|
||||
<Footer />
|
||||
</>
|
||||
)}
|
||||
{device
|
||||
? (
|
||||
<div className="flex h-screen">
|
||||
<DialogManager />
|
||||
<KeyBackupReminder />
|
||||
<CommandPalette />
|
||||
<PageRouter />
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<Dashboard />
|
||||
<Footer />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Avatar } from "@components/UI/Avatar";
|
||||
import { Avatar } from "./UI/Avatar.tsx";
|
||||
import {
|
||||
CommandDialog,
|
||||
CommandEmpty,
|
||||
@@ -117,13 +117,11 @@ export const CommandPalette = () => {
|
||||
return {
|
||||
label:
|
||||
device.nodes.get(device.hardware.myNodeNum)?.user?.longName ??
|
||||
device.hardware.myNodeNum.toString(),
|
||||
device.hardware.myNodeNum.toString(),
|
||||
icon: (
|
||||
<Avatar
|
||||
text={
|
||||
device.nodes.get(device.hardware.myNodeNum)?.user
|
||||
?.shortName ?? device.hardware.myNodeNum.toString()
|
||||
}
|
||||
text={device.nodes.get(device.hardware.myNodeNum)?.user
|
||||
?.shortName ?? device.hardware.myNodeNum.toString()}
|
||||
/>
|
||||
),
|
||||
action() {
|
||||
@@ -241,8 +239,8 @@ export const CommandPalette = () => {
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeydown);
|
||||
return () => window.removeEventListener("keydown", handleKeydown);
|
||||
globalThis.addEventListener("keydown", handleKeydown);
|
||||
return () => globalThis.removeEventListener("keydown", handleKeydown);
|
||||
}, [setCommandPaletteOpen]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -5,10 +5,11 @@ import { Code } from "@components/UI/Typography/Code.tsx";
|
||||
import { useAppStore } from "@core/stores/appStore.ts";
|
||||
import { useDeviceStore } from "@core/stores/deviceStore.ts";
|
||||
import { HomeIcon, PlusIcon, SearchIcon } from "lucide-react";
|
||||
import type { JSX } from "react";
|
||||
import { Avatar } from "./UI/Avatar";
|
||||
|
||||
export const DeviceSelector = (): JSX.Element => {
|
||||
import { Avatar } from "./UI/Avatar.tsx";
|
||||
import process from "node:process";
|
||||
|
||||
export const DeviceSelector = () => {
|
||||
const { getDevices } = useDeviceStore();
|
||||
const {
|
||||
selectedDevice,
|
||||
@@ -38,11 +39,9 @@ export const DeviceSelector = (): JSX.Element => {
|
||||
active={selectedDevice === device.id}
|
||||
>
|
||||
<Avatar
|
||||
text={
|
||||
device.nodes
|
||||
.get(device.hardware.myNodeNum)
|
||||
?.user?.shortName.toString() ?? "UNK"
|
||||
}
|
||||
text={device.nodes
|
||||
.get(device.hardware.myNodeNum)
|
||||
?.user?.shortName.toString() ?? "UNK"}
|
||||
/>
|
||||
</DeviceSelectorButton>
|
||||
))}
|
||||
@@ -66,9 +65,11 @@ export const DeviceSelector = (): JSX.Element => {
|
||||
<SearchIcon />
|
||||
</button>
|
||||
{/* TODO: This is being commented out until its fixed */}
|
||||
{/* <button type="button" className="transition-all hover:text-accent">
|
||||
{
|
||||
/* <button type="button" className="transition-all hover:text-accent">
|
||||
<LanguagesIcon />
|
||||
</button> */}
|
||||
</button> */
|
||||
}
|
||||
<Separator />
|
||||
<Code>{process.env.COMMIT_HASH}</Code>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,6 @@ export interface DeviceSelectorButtonProps {
|
||||
}
|
||||
|
||||
export const DeviceSelectorButton = ({
|
||||
active,
|
||||
onClick,
|
||||
children,
|
||||
}: DeviceSelectorButtonProps) => (
|
||||
@@ -14,9 +13,11 @@ export const DeviceSelectorButton = ({
|
||||
onClick={onClick}
|
||||
onKeyDown={onClick}
|
||||
>
|
||||
{/* {active && (
|
||||
{
|
||||
/* {active && (
|
||||
<div className="absolute -left-2 h-10 w-1.5 rounded-full bg-accent" />
|
||||
)} */}
|
||||
)} */
|
||||
}
|
||||
<div className="flex aspect-square cursor-pointer flex-col items-center justify-center">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { RemoveNodeDialog } from "@app/components/Dialog/RemoveNodeDialog.tsx";
|
||||
import { DeviceNameDialog } from "@components/Dialog/DeviceNameDialog.tsx";
|
||||
import { ImportDialog } from "@components/Dialog/ImportDialog.tsx";
|
||||
import { PkiBackupDialog } from "@components/Dialog/PKIBackupDialog";
|
||||
import { PkiBackupDialog } from "./PKIBackupDialog.tsx";
|
||||
import { QRDialog } from "@components/Dialog/QRDialog.tsx";
|
||||
import { RebootDialog } from "@components/Dialog/RebootDialog.tsx";
|
||||
import { ShutdownDialog } from "@components/Dialog/ShutdownDialog.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import type { JSX } from "react";
|
||||
import { NodeDetailsDialog } from "./NodeDetailsDialog";
|
||||
|
||||
export const DialogManager = (): JSX.Element => {
|
||||
import { NodeDetailsDialog } from "./NodeDetailsDialog.tsx";
|
||||
|
||||
export const DialogManager = () => {
|
||||
const { channels, config, dialog, setDialogOpen } = useDevice();
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -15,7 +15,7 @@ import { Switch } from "@components/UI/Switch.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
import { toByteArray } from "base64-js";
|
||||
import { type JSX, useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export interface ImportDialogProps {
|
||||
open: boolean;
|
||||
@@ -26,7 +26,7 @@ export interface ImportDialogProps {
|
||||
export const ImportDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: ImportDialogProps): JSX.Element => {
|
||||
}: ImportDialogProps) => {
|
||||
const [importDialogInput, setImportDialogInput] = useState<string>("");
|
||||
const [channelSet, setChannelSet] = useState<Protobuf.AppOnly.ChannelSet>();
|
||||
const [validUrl, setValidUrl] = useState<boolean>(false);
|
||||
@@ -62,7 +62,7 @@ export const ImportDialog = ({
|
||||
),
|
||||
);
|
||||
setValidUrl(true);
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
setValidUrl(false);
|
||||
setChannelSet(undefined);
|
||||
}
|
||||
@@ -73,10 +73,9 @@ export const ImportDialog = ({
|
||||
connection?.setChannel(
|
||||
create(Protobuf.Channel.ChannelSchema, {
|
||||
index,
|
||||
role:
|
||||
index === 0
|
||||
? Protobuf.Channel.Channel_Role.PRIMARY
|
||||
: Protobuf.Channel.Channel_Role.SECONDARY,
|
||||
role: index === 0
|
||||
? Protobuf.Channel.Channel_Role.PRIMARY
|
||||
: Protobuf.Channel.Channel_Role.SECONDARY,
|
||||
settings: ch,
|
||||
}),
|
||||
);
|
||||
@@ -119,25 +118,29 @@ export const ImportDialog = ({
|
||||
<div className="w-36">
|
||||
<Label>Use Preset?</Label>
|
||||
<Switch
|
||||
disabled={true}
|
||||
disabled
|
||||
checked={channelSet?.loraConfig?.usePreset ?? true}
|
||||
/>
|
||||
</div>
|
||||
{/* <Select
|
||||
{
|
||||
/* <Select
|
||||
label="Modem Preset"
|
||||
disabled
|
||||
value={channelSet?.loraConfig?.modemPreset}
|
||||
>
|
||||
{renderOptions(Protobuf.Config_LoRaConfig_ModemPreset)}
|
||||
</Select> */}
|
||||
</Select> */
|
||||
}
|
||||
</div>
|
||||
{/* <Select
|
||||
{
|
||||
/* <Select
|
||||
label="Region"
|
||||
disabled
|
||||
value={channelSet?.loraConfig?.region}
|
||||
>
|
||||
{renderOptions(Protobuf.Config_LoRaConfig_RegionCode)}
|
||||
</Select> */}
|
||||
</Select> */
|
||||
}
|
||||
|
||||
<span className="text-md block font-medium text-text-primary">
|
||||
Channels:
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { useDevice } from "@app/core/stores/deviceStore";
|
||||
import { useDevice } from "../../core/stores/deviceStore.ts";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@components/UI/Dialog";
|
||||
} from "../UI/Dialog.tsx";
|
||||
import type { Protobuf, Types } from "@meshtastic/core";
|
||||
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
||||
import type { JSX } from "react";
|
||||
|
||||
export interface LocationResponseDialogProps {
|
||||
location: Types.PacketMetadata<Protobuf.Mesh.location> | undefined;
|
||||
@@ -20,15 +19,13 @@ export const LocationResponseDialog = ({
|
||||
location,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: LocationResponseDialogProps): JSX.Element => {
|
||||
}: LocationResponseDialogProps) => {
|
||||
const { nodes } = useDevice();
|
||||
|
||||
const from = nodes.get(location?.from ?? 0);
|
||||
const longName =
|
||||
from?.user?.longName ??
|
||||
const longName = from?.user?.longName ??
|
||||
(from ? `!${numberToHexUnpadded(from?.num)}` : "Unknown");
|
||||
const shortName =
|
||||
from?.user?.shortName ??
|
||||
const shortName = from?.user?.shortName ??
|
||||
(from ? `${numberToHexUnpadded(from?.num).substring(0, 4)}` : "UNK");
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
type BrowserFeature,
|
||||
useBrowserFeatureDetection,
|
||||
} from "@app/core/hooks/useBrowserFeatureDetection";
|
||||
} from "../../core/hooks/useBrowserFeatureDetection.ts";
|
||||
import { BLE } from "@components/PageComponents/Connect/BLE.tsx";
|
||||
import { HTTP } from "@components/PageComponents/Connect/HTTP.tsx";
|
||||
import { Serial } from "@components/PageComponents/Connect/Serial.tsx";
|
||||
@@ -18,9 +18,9 @@ import {
|
||||
TabsTrigger,
|
||||
} from "@components/UI/Tabs.tsx";
|
||||
import { Subtle } from "@components/UI/Typography/Subtle.tsx";
|
||||
import { AlertCircle, InfoIcon } from "lucide-react";
|
||||
import { Fragment, type JSX } from "react/jsx-runtime";
|
||||
import { Link } from "../UI/Typography/Link";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import { Link } from "../UI/Typography/Link.tsx";
|
||||
import { Fragment } from "react/jsx-runtime";
|
||||
|
||||
export interface TabElementProps {
|
||||
closeDialog: () => void;
|
||||
@@ -85,14 +85,16 @@ const ErrorMessage = ({ missingFeatures }: FeatureErrorProps) => {
|
||||
<p className="text-sm">
|
||||
{browserFeatures.length > 0 && (
|
||||
<>
|
||||
This application requires {formatFeatureList(browserFeatures)}.
|
||||
Please use a Chromium-based browser like Chrome or Edge.
|
||||
This application requires{" "}
|
||||
{formatFeatureList(browserFeatures)}. Please use a
|
||||
Chromium-based browser like Chrome or Edge.
|
||||
</>
|
||||
)}
|
||||
{needsSecureContext && (
|
||||
<>
|
||||
{browserFeatures.length > 0 && " Additionally, it"}
|
||||
{browserFeatures.length === 0 && "This application"} requires a{" "}
|
||||
{browserFeatures.length === 0 && "This application"} requires a
|
||||
{" "}
|
||||
<Link href={links["Secure Context"]}>secure context</Link>.
|
||||
Please connect using HTTPS or localhost.
|
||||
</>
|
||||
@@ -107,7 +109,7 @@ const ErrorMessage = ({ missingFeatures }: FeatureErrorProps) => {
|
||||
export const NewDeviceDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: NewDeviceProps): JSX.Element => {
|
||||
}: NewDeviceProps) => {
|
||||
const { unsupported } = useBrowserFeatureDetection();
|
||||
|
||||
const tabs: TabManifest[] = [
|
||||
@@ -119,15 +121,13 @@ export const NewDeviceDialog = ({
|
||||
{
|
||||
label: "Bluetooth",
|
||||
element: BLE,
|
||||
isDisabled:
|
||||
unsupported.includes("Web Bluetooth") ||
|
||||
isDisabled: unsupported.includes("Web Bluetooth") ||
|
||||
unsupported.includes("Secure Context"),
|
||||
},
|
||||
{
|
||||
label: "Serial",
|
||||
element: Serial,
|
||||
isDisabled:
|
||||
unsupported.includes("Web Serial") ||
|
||||
isDisabled: unsupported.includes("Web Serial") ||
|
||||
unsupported.includes("Secure Context"),
|
||||
},
|
||||
];
|
||||
@@ -149,9 +149,9 @@ export const NewDeviceDialog = ({
|
||||
{tabs.map((tab) => (
|
||||
<TabsContent key={tab.label} value={tab.label}>
|
||||
<fieldset disabled={tab.isDisabled}>
|
||||
{tab.isDisabled ? (
|
||||
<ErrorMessage missingFeatures={unsupported} />
|
||||
) : null}
|
||||
{tab.isDisabled
|
||||
? <ErrorMessage missingFeatures={unsupported} />
|
||||
: null}
|
||||
<tab.element closeDialog={() => onOpenChange(false)} />
|
||||
</fieldset>
|
||||
</TabsContent>
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import { useAppStore } from "@app/core/stores/appStore";
|
||||
import { useDevice } from "@app/core/stores/deviceStore";
|
||||
import { useAppStore } from "../../core/stores/appStore.ts";
|
||||
import { useDevice } from "../../core/stores/deviceStore.ts";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@components/UI/Accordion";
|
||||
} from "../UI/Accordion.tsx";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@components/UI/Dialog";
|
||||
} from "../UI/Dialog.tsx";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
||||
import { DeviceImage } from "../generic/DeviceImage";
|
||||
import { TimeAgo } from "../generic/TimeAgo";
|
||||
import { Uptime } from "../generic/Uptime";
|
||||
import { DeviceImage } from "../generic/DeviceImage.tsx";
|
||||
import { TimeAgo } from "../generic/TimeAgo.tsx";
|
||||
import { Uptime } from "../generic/Uptime.tsx";
|
||||
|
||||
export interface NodeDetailsDialogProps {
|
||||
open: boolean;
|
||||
@@ -32,134 +32,159 @@ export const NodeDetailsDialog = ({
|
||||
const { nodeNumDetails } = useAppStore();
|
||||
const device: Protobuf.Mesh.NodeInfo = nodes.get(nodeNumDetails);
|
||||
|
||||
return device ? (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
Node Details for {device.user?.longName ?? "UNKNOWN"} (
|
||||
{device.user?.shortName ?? "UNK"})
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<div className="w-full">
|
||||
<DeviceImage
|
||||
className="w-32 h-32 mx-auto rounded-lg border-4 border-slate-200 dark:border-slate-800"
|
||||
deviceType={
|
||||
Protobuf.Mesh.HardwareModel[device.user?.hwModel ?? 0]
|
||||
}
|
||||
/>
|
||||
<div className="bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
|
||||
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50">
|
||||
Details:
|
||||
</p>
|
||||
<p>
|
||||
Hardware:{" "}
|
||||
{Protobuf.Mesh.HardwareModel[device.user?.hwModel ?? 0]}
|
||||
</p>
|
||||
<p>Node Number: {device.num}</p>
|
||||
<p>Node HEX: !{numberToHexUnpadded(device.num)}</p>
|
||||
<p>
|
||||
Role:{" "}
|
||||
{
|
||||
Protobuf.Config.Config_DeviceConfig_Role[
|
||||
return device
|
||||
? (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
Node Details for {device.user?.longName ?? "UNKNOWN"} (
|
||||
{device.user?.shortName ?? "UNK"})
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<div className="w-full">
|
||||
<DeviceImage
|
||||
className="w-32 h-32 mx-auto rounded-lg border-4 border-slate-200 dark:border-slate-800"
|
||||
deviceType={Protobuf.Mesh
|
||||
.HardwareModel[device.user?.hwModel ?? 0]}
|
||||
/>
|
||||
<div className="bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
|
||||
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50">
|
||||
Details:
|
||||
</p>
|
||||
<p>
|
||||
Hardware:{" "}
|
||||
{Protobuf.Mesh.HardwareModel[device.user?.hwModel ?? 0]}
|
||||
</p>
|
||||
<p>Node Number: {device.num}</p>
|
||||
<p>Node HEX: !{numberToHexUnpadded(device.num)}</p>
|
||||
<p>
|
||||
Role: {Protobuf.Config.Config_DeviceConfig_Role[
|
||||
device.user?.role ?? 0
|
||||
]
|
||||
}
|
||||
</p>
|
||||
<p>
|
||||
Last Heard:{" "}
|
||||
{device.lastHeard === 0 ? (
|
||||
"Never"
|
||||
) : (
|
||||
<TimeAgo timestamp={device.lastHeard * 1000} />
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{device.position ? (
|
||||
<div className="mt-5 bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
|
||||
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50">
|
||||
Position:
|
||||
]}
|
||||
</p>
|
||||
{device.position.latitudeI && device.position.longitudeI ? (
|
||||
<p>
|
||||
Coordinates:{" "}
|
||||
<a
|
||||
className="text-blue-500 dark:text-blue-400"
|
||||
href={`https://www.openstreetmap.org/?mlat=${
|
||||
device.position.latitudeI / 1e7
|
||||
}&mlon=${device.position.longitudeI / 1e7}&layers=N`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
<p>
|
||||
Last Heard: {device.lastHeard === 0
|
||||
? (
|
||||
"Never"
|
||||
)
|
||||
: <TimeAgo timestamp={device.lastHeard * 1000} />}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{device.position
|
||||
? (
|
||||
<div className="mt-5 bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
|
||||
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50">
|
||||
Position:
|
||||
</p>
|
||||
{device.position.latitudeI && device.position.longitudeI
|
||||
? (
|
||||
<p>
|
||||
Coordinates:{" "}
|
||||
<a
|
||||
className="text-blue-500 dark:text-blue-400"
|
||||
href={`https://www.openstreetmap.org/?mlat=${
|
||||
device.position.latitudeI / 1e7
|
||||
}&mlon=${
|
||||
device.position.longitudeI / 1e7
|
||||
}&layers=N`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{device.position.latitudeI / 1e7},{" "}
|
||||
{device.position.longitudeI / 1e7}
|
||||
</a>
|
||||
</p>
|
||||
)
|
||||
: null}
|
||||
{device.position.altitude
|
||||
? <p>Altitude: {device.position.altitude}m</p>
|
||||
: null}
|
||||
</div>
|
||||
)
|
||||
: null}
|
||||
|
||||
{device.deviceMetrics
|
||||
? (
|
||||
<div className="mt-5 bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
|
||||
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50">
|
||||
Device Metrics:
|
||||
</p>
|
||||
{device.deviceMetrics.airUtilTx
|
||||
? (
|
||||
<p>
|
||||
Air TX utilization:{" "}
|
||||
{device.deviceMetrics.airUtilTx.toFixed(2)}%
|
||||
</p>
|
||||
)
|
||||
: null}
|
||||
{device.deviceMetrics.channelUtilization
|
||||
? (
|
||||
<p>
|
||||
Channel utilization:{" "}
|
||||
{device.deviceMetrics.channelUtilization.toFixed(2)}%
|
||||
</p>
|
||||
)
|
||||
: null}
|
||||
{device.deviceMetrics.batteryLevel
|
||||
? (
|
||||
<p>
|
||||
Battery level:{" "}
|
||||
{device.deviceMetrics.batteryLevel.toFixed(2)}%
|
||||
</p>
|
||||
)
|
||||
: null}
|
||||
{device.deviceMetrics.voltage
|
||||
? (
|
||||
<p>
|
||||
Voltage: {device.deviceMetrics.voltage.toFixed(2)}V
|
||||
</p>
|
||||
)
|
||||
: null}
|
||||
{device.deviceMetrics.uptimeSeconds
|
||||
? (
|
||||
<p>
|
||||
Uptime:{" "}
|
||||
<Uptime
|
||||
seconds={device.deviceMetrics.uptimeSeconds}
|
||||
/>
|
||||
</p>
|
||||
)
|
||||
: null}
|
||||
</div>
|
||||
)
|
||||
: null}
|
||||
|
||||
{device
|
||||
? (
|
||||
<div className="mt-5 w-full max-w-[464px] bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
|
||||
<Accordion
|
||||
className="AccordionRoot"
|
||||
type="single"
|
||||
collapsible
|
||||
>
|
||||
{device.position.latitudeI / 1e7},{" "}
|
||||
{device.position.longitudeI / 1e7}
|
||||
</a>
|
||||
</p>
|
||||
) : null}
|
||||
{device.position.altitude ? (
|
||||
<p>Altitude: {device.position.altitude}m</p>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{device.deviceMetrics ? (
|
||||
<div className="mt-5 bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
|
||||
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50">
|
||||
Device Metrics:
|
||||
</p>
|
||||
{device.deviceMetrics.airUtilTx ? (
|
||||
<p>
|
||||
Air TX utilization:{" "}
|
||||
{device.deviceMetrics.airUtilTx.toFixed(2)}%
|
||||
</p>
|
||||
) : null}
|
||||
{device.deviceMetrics.channelUtilization ? (
|
||||
<p>
|
||||
Channel utilization:{" "}
|
||||
{device.deviceMetrics.channelUtilization.toFixed(2)}%
|
||||
</p>
|
||||
) : null}
|
||||
{device.deviceMetrics.batteryLevel ? (
|
||||
<p>
|
||||
Battery level:{" "}
|
||||
{device.deviceMetrics.batteryLevel.toFixed(2)}%
|
||||
</p>
|
||||
) : null}
|
||||
{device.deviceMetrics.voltage ? (
|
||||
<p>Voltage: {device.deviceMetrics.voltage.toFixed(2)}V</p>
|
||||
) : null}
|
||||
{device.deviceMetrics.uptimeSeconds ? (
|
||||
<p>
|
||||
Uptime:{" "}
|
||||
<Uptime seconds={device.deviceMetrics.uptimeSeconds} />
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{device ? (
|
||||
<div className="mt-5 w-full max-w-[464px] bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
|
||||
<Accordion className="AccordionRoot" type="single" collapsible>
|
||||
<AccordionItem className="AccordionItem" value="item-1">
|
||||
<AccordionTrigger>
|
||||
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50">
|
||||
All Raw Metrics:
|
||||
</p>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="overflow-x-scroll">
|
||||
<pre className="text-xs w-full">
|
||||
<AccordionItem className="AccordionItem" value="item-1">
|
||||
<AccordionTrigger>
|
||||
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50">
|
||||
All Raw Metrics:
|
||||
</p>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="overflow-x-scroll">
|
||||
<pre className="text-xs w-full">
|
||||
{JSON.stringify(device, null, 2)}
|
||||
</pre>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
) : null;
|
||||
</pre>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
)
|
||||
: null}
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
: null;
|
||||
};
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { toast } from "@app/core/hooks/useToast";
|
||||
import { useAppStore } from "@app/core/stores/appStore";
|
||||
import { useDevice } from "@app/core/stores/deviceStore";
|
||||
import { toast } from "../../core/hooks/useToast.ts";
|
||||
import { useAppStore } from "../../core/stores/appStore.ts";
|
||||
import { useDevice } from "../../core/stores/deviceStore.ts";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@components/UI/Dialog";
|
||||
} from "../UI/Dialog.tsx";
|
||||
import type { Protobuf } from "@meshtastic/core";
|
||||
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
||||
import { TrashIcon } from "lucide-react";
|
||||
import type { JSX } from "react";
|
||||
import { Button } from "../UI/Button";
|
||||
|
||||
import { Button } from "../UI/Button.tsx";
|
||||
|
||||
export interface NodeOptionsDialogProps {
|
||||
node: Protobuf.Mesh.NodeInfo | undefined;
|
||||
@@ -23,7 +23,7 @@ export const NodeOptionsDialog = ({
|
||||
node,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: NodeOptionsDialogProps): JSX.Element => {
|
||||
}: NodeOptionsDialogProps) => {
|
||||
const { setDialogOpen, connection, setActivePage } = useDevice();
|
||||
const {
|
||||
setNodeNumToBeRemoved,
|
||||
@@ -31,11 +31,9 @@ export const NodeOptionsDialog = ({
|
||||
setChatType,
|
||||
setActiveChat,
|
||||
} = useAppStore();
|
||||
const longName =
|
||||
node?.user?.longName ??
|
||||
const longName = node?.user?.longName ??
|
||||
(node ? `!${numberToHexUnpadded(node?.num)}` : "Unknown");
|
||||
const shortName =
|
||||
node?.user?.shortName ??
|
||||
const shortName = node?.user?.shortName ??
|
||||
(node ? `${numberToHexUnpadded(node?.num).substring(0, 4)}` : "UNK");
|
||||
|
||||
function handleDirectMessage() {
|
||||
@@ -53,7 +51,7 @@ export const NodeOptionsDialog = ({
|
||||
connection?.requestPosition(node.num).then(() =>
|
||||
toast({
|
||||
title: "Position request sent.",
|
||||
}),
|
||||
})
|
||||
);
|
||||
onOpenChange();
|
||||
}
|
||||
@@ -66,7 +64,7 @@ export const NodeOptionsDialog = ({
|
||||
connection?.traceRoute(node.num).then(() =>
|
||||
toast({
|
||||
title: "Traceroute sent.",
|
||||
}),
|
||||
})
|
||||
);
|
||||
onOpenChange();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useDevice } from "@app/core/stores/deviceStore";
|
||||
import { Button } from "@components/UI/Button";
|
||||
import { useDevice } from "../../core/stores/deviceStore.ts";
|
||||
import { Button } from "../UI/Button.tsx";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -40,7 +40,7 @@ export const PkiBackupDialog = ({
|
||||
const renderPrintWindow = React.useCallback(() => {
|
||||
if (!privateKey || !publicKey) return;
|
||||
|
||||
const printWindow = window.open("", "_blank");
|
||||
const printWindow = globalThis.open("", "_blank");
|
||||
if (printWindow) {
|
||||
printWindow.document.write(`
|
||||
<html>
|
||||
@@ -116,14 +116,14 @@ export const PkiBackupDialog = ({
|
||||
</DialogHeader>
|
||||
<DialogFooter className="mt-6">
|
||||
<Button
|
||||
variant={"default"}
|
||||
variant="default"
|
||||
onClick={() => createDownloadKeyFile()}
|
||||
className=""
|
||||
>
|
||||
<DownloadIcon size={20} className="mr-2" />
|
||||
Download
|
||||
</Button>
|
||||
<Button variant={"default"} onClick={() => renderPrintWindow()}>
|
||||
<Button variant="default" onClick={() => renderPrintWindow()}>
|
||||
<PrinterIcon size={20} className="mr-2" />
|
||||
Print
|
||||
</Button>
|
||||
|
||||
@@ -18,7 +18,7 @@ export const PkiRegenerateDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
onSubmit,
|
||||
}: PkiRegenerateDialogProps): JSX.Element => {
|
||||
}: PkiRegenerateDialogProps) => {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
|
||||
@@ -28,7 +28,7 @@ export const QRDialog = ({
|
||||
onOpenChange,
|
||||
loraConfig,
|
||||
channels,
|
||||
}: QRDialogProps): JSX.Element => {
|
||||
}: QRDialogProps) => {
|
||||
const [selectedChannels, setSelectedChannels] = useState<number[]>([0]);
|
||||
const [qrCodeUrl, setQrCodeUrl] = useState<string>("");
|
||||
const [qrCodeAdd, setQrCodeAdd] = useState<boolean>();
|
||||
@@ -77,8 +77,8 @@ export const QRDialog = ({
|
||||
{channel.settings?.name.length
|
||||
? channel.settings.name
|
||||
: channel.role === Protobuf.Channel.Channel_Role.PRIMARY
|
||||
? "Primary"
|
||||
: `Channel: ${channel.index}`}
|
||||
? "Primary"
|
||||
: `Channel: ${channel.index}`}
|
||||
</Label>
|
||||
<Checkbox
|
||||
key={channel.index}
|
||||
@@ -86,7 +86,9 @@ export const QRDialog = ({
|
||||
onCheckedChange={() => {
|
||||
if (selectedChannels.includes(channel.index)) {
|
||||
setSelectedChannels(
|
||||
selectedChannels.filter((c) => c !== channel.index),
|
||||
selectedChannels.filter((c) =>
|
||||
c !== channel.index
|
||||
),
|
||||
);
|
||||
} else {
|
||||
setSelectedChannels([
|
||||
@@ -130,7 +132,7 @@ export const QRDialog = ({
|
||||
<Label>Sharable URL</Label>
|
||||
<Input
|
||||
value={qrCodeUrl}
|
||||
disabled={true}
|
||||
disabled
|
||||
className="dark:text-slate-900"
|
||||
action={{
|
||||
icon: ClipboardIcon,
|
||||
|
||||
@@ -19,7 +19,7 @@ export interface RebootDialogProps {
|
||||
export const RebootDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: RebootDialogProps): JSX.Element => {
|
||||
}: RebootDialogProps) => {
|
||||
const { connection } = useDevice();
|
||||
|
||||
const [time, setTime] = useState<number>(5);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useAppStore } from "@app/core/stores/appStore";
|
||||
import { useAppStore } from "../../core/stores/appStore.ts";
|
||||
import { useDevice } from "@app/core/stores/deviceStore.ts";
|
||||
import { Button } from "@components/UI/Button.tsx";
|
||||
import {
|
||||
@@ -19,7 +19,7 @@ export interface RemoveNodeDialogProps {
|
||||
export const RemoveNodeDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: RemoveNodeDialogProps): JSX.Element => {
|
||||
}: RemoveNodeDialogProps) => {
|
||||
const { connection, nodes, removeNode } = useDevice();
|
||||
const { nodeNumToBeRemoved } = useAppStore();
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ export interface ShutdownDialogProps {
|
||||
export const ShutdownDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: ShutdownDialogProps): JSX.Element => {
|
||||
}: ShutdownDialogProps) => {
|
||||
const { connection } = useDevice();
|
||||
|
||||
const [time, setTime] = useState<number>(5);
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { useDevice } from "@app/core/stores/deviceStore";
|
||||
import { useDevice } from "../../core/stores/deviceStore.ts";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@components/UI/Dialog";
|
||||
} from "../UI/Dialog.tsx";
|
||||
import type { Protobuf, Types } from "@meshtastic/core";
|
||||
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
||||
import type { JSX } from "react";
|
||||
import { TraceRoute } from "../PageComponents/Messages/TraceRoute";
|
||||
|
||||
import { TraceRoute } from "../PageComponents/Messages/TraceRoute.tsx";
|
||||
|
||||
export interface TracerouteResponseDialogProps {
|
||||
traceroute: Types.PacketMetadata<Protobuf.Mesh.RouteDiscovery> | undefined;
|
||||
@@ -21,18 +21,16 @@ export const TracerouteResponseDialog = ({
|
||||
traceroute,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: TracerouteResponseDialogProps): JSX.Element => {
|
||||
}: TracerouteResponseDialogProps) => {
|
||||
const { nodes } = useDevice();
|
||||
const route: number[] = traceroute?.data.route ?? [];
|
||||
const routeBack: number[] = traceroute?.data.routeBack ?? [];
|
||||
const snrTowards = traceroute?.data.snrTowards ?? [];
|
||||
const snrBack = traceroute?.data.snrBack ?? [];
|
||||
const from = nodes.get(traceroute?.from ?? 0);
|
||||
const longName =
|
||||
from?.user?.longName ??
|
||||
const longName = from?.user?.longName ??
|
||||
(from ? `!${numberToHexUnpadded(from?.num)}` : "Unknown");
|
||||
const shortName =
|
||||
from?.user?.shortName ??
|
||||
const shortName = from?.user?.shortName ??
|
||||
(from ? `${numberToHexUnpadded(from?.num).substring(0, 4)}` : "UNK");
|
||||
const to = nodes.get(traceroute?.to ?? 0);
|
||||
return (
|
||||
|
||||
@@ -74,10 +74,11 @@ export function DynamicForm<T extends FieldValues>({
|
||||
const value = getValues(field.fieldName);
|
||||
if (value === "always") return true;
|
||||
if (typeof value === "boolean") return field.invert ? value : !value;
|
||||
if (typeof value === "number")
|
||||
if (typeof value === "number") {
|
||||
return field.invert
|
||||
? field.selector !== value
|
||||
: field.selector === value;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
};
|
||||
@@ -85,11 +86,9 @@ export function DynamicForm<T extends FieldValues>({
|
||||
return (
|
||||
<form
|
||||
className="space-y-8"
|
||||
{...(submitType === "onSubmit"
|
||||
? { onSubmit: handleSubmit(onSubmit) }
|
||||
: {
|
||||
onChange: handleSubmit(onSubmit),
|
||||
})}
|
||||
{...(submitType === "onSubmit" ? { onSubmit: handleSubmit(onSubmit) } : {
|
||||
onChange: handleSubmit(onSubmit),
|
||||
})}
|
||||
>
|
||||
{fieldGroups.map((fieldGroup) => (
|
||||
<div key={fieldGroup.label} className="space-y-8 sm:space-y-5">
|
||||
@@ -105,10 +104,8 @@ export function DynamicForm<T extends FieldValues>({
|
||||
label={field.label}
|
||||
fieldName={field.name}
|
||||
description={field.description}
|
||||
valid={
|
||||
field.validationText === undefined ||
|
||||
field.validationText === ""
|
||||
}
|
||||
valid={field.validationText === undefined ||
|
||||
field.validationText === ""}
|
||||
validationText={field.validationText}
|
||||
>
|
||||
<DynamicFormField
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
type MultiSelectFieldProps,
|
||||
MultiSelectInput,
|
||||
} from "@app/components/Form/FormMultiSelect";
|
||||
} from "./FormMultiSelect.tsx";
|
||||
import {
|
||||
GenericInput,
|
||||
type InputFieldProps,
|
||||
@@ -48,11 +48,19 @@ export function DynamicFormField<T extends FieldValues>({
|
||||
|
||||
case "toggle":
|
||||
return (
|
||||
<ToggleInput field={field} control={control} disabled={disabled} />
|
||||
<ToggleInput
|
||||
field={field}
|
||||
control={control}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
case "select":
|
||||
return (
|
||||
<SelectInput field={field} control={control} disabled={disabled} />
|
||||
<SelectInput
|
||||
field={field}
|
||||
control={control}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
case "passwordGenerator":
|
||||
return (
|
||||
|
||||
@@ -40,17 +40,15 @@ export function GenericInput<T extends FieldValues>({
|
||||
control={control}
|
||||
render={({ field: { value, onChange, ...rest } }) => (
|
||||
<Input
|
||||
type={
|
||||
field.type === "password" && passwordShown ? "text" : field.type
|
||||
}
|
||||
action={
|
||||
field.type === "password"
|
||||
? {
|
||||
icon: passwordShown ? EyeOff : Eye,
|
||||
onClick: togglePasswordVisiblity,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
type={field.type === "password" && passwordShown
|
||||
? "text"
|
||||
: field.type}
|
||||
action={field.type === "password"
|
||||
? {
|
||||
icon: passwordShown ? EyeOff : Eye,
|
||||
onClick: togglePasswordVisiblity,
|
||||
}
|
||||
: undefined}
|
||||
step={field.properties?.step}
|
||||
value={field.type === "number" ? Number.parseFloat(value) : value}
|
||||
id={field.name}
|
||||
|
||||
@@ -3,7 +3,7 @@ import type {
|
||||
GenericFormElementProps,
|
||||
} from "@components/Form/DynamicForm.tsx";
|
||||
import type { FieldValues } from "react-hook-form";
|
||||
import { MultiSelect, MultiSelectItem } from "../UI/MultiSelect";
|
||||
import { MultiSelect, MultiSelectItem } from "../UI/MultiSelect.tsx";
|
||||
|
||||
export interface MultiSelectFieldProps<T> extends BaseFormBuilderProps<T> {
|
||||
type: "multiSelect";
|
||||
@@ -28,8 +28,8 @@ export function MultiSelectInput<T extends FieldValues>({
|
||||
// Make sure to filter out the UNSET value, as it shouldn't be shown in the UI
|
||||
const optionsEnumValues = enumValue
|
||||
? Object.entries(enumValue)
|
||||
.filter((value) => typeof value[1] === "number")
|
||||
.filter((value) => value[0] !== "UNSET")
|
||||
.filter((value) => typeof value[1] === "number")
|
||||
.filter((value) => value[0] !== "UNSET")
|
||||
: [];
|
||||
|
||||
const formatName = (name: string) => {
|
||||
|
||||
@@ -2,10 +2,10 @@ import type {
|
||||
BaseFormBuilderProps,
|
||||
GenericFormElementProps,
|
||||
} from "@components/Form/DynamicForm.tsx";
|
||||
import type { ButtonVariant } from "@components/UI/Button";
|
||||
import type { ButtonVariant } from "../UI/Button.tsx";
|
||||
import { Generator } from "@components/UI/Generator.tsx";
|
||||
import { Eye, EyeOff } from "lucide-react";
|
||||
import type { ChangeEventHandler, MouseEventHandler } from "react";
|
||||
import type { ChangeEventHandler } from "react";
|
||||
import { useState } from "react";
|
||||
import { Controller, type FieldValues } from "react-hook-form";
|
||||
|
||||
@@ -43,14 +43,12 @@ export function PasswordGenerator<T extends FieldValues>({
|
||||
<Generator
|
||||
type={field.hide && !passwordShown ? "password" : "text"}
|
||||
id={field.id}
|
||||
action={
|
||||
field.hide
|
||||
? {
|
||||
icon: passwordShown ? EyeOff : Eye,
|
||||
onClick: togglePasswordVisiblity,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
action={field.hide
|
||||
? {
|
||||
icon: passwordShown ? EyeOff : Eye,
|
||||
onClick: togglePasswordVisiblity,
|
||||
}
|
||||
: undefined}
|
||||
devicePSKBitCount={field.devicePSKBitCount}
|
||||
bits={field.bits}
|
||||
inputChange={field.inputChange}
|
||||
|
||||
@@ -36,8 +36,8 @@ export function SelectInput<T extends FieldValues>({
|
||||
field.properties;
|
||||
const optionsEnumValues = enumValue
|
||||
? Object.entries(enumValue).filter(
|
||||
(value) => typeof value[1] === "number",
|
||||
)
|
||||
(value) => typeof value[1] === "number",
|
||||
)
|
||||
: [];
|
||||
return (
|
||||
<Select
|
||||
@@ -58,11 +58,13 @@ export function SelectInput<T extends FieldValues>({
|
||||
<SelectItem key={name} value={value.toString()}>
|
||||
{formatEnumName
|
||||
? name
|
||||
.replace(/_/g, " ")
|
||||
.toLowerCase()
|
||||
.split(" ")
|
||||
.map((s) => s.charAt(0).toUpperCase() + s.substring(1))
|
||||
.join(" ")
|
||||
.replace(/_/g, " ")
|
||||
.toLowerCase()
|
||||
.split(" ")
|
||||
.map((s) =>
|
||||
s.charAt(0).toUpperCase() + s.substring(1)
|
||||
)
|
||||
.join(" ")
|
||||
: name}
|
||||
</SelectItem>
|
||||
))}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useBackupReminder } from "@app/core/hooks/useKeyBackupReminder";
|
||||
import { useDevice } from "@app/core/stores/deviceStore";
|
||||
import { useBackupReminder } from "@core/hooks/useKeyBackupReminder.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
|
||||
export const KeyBackupReminder = (): JSX.Element => {
|
||||
export const KeyBackupReminder = () => {
|
||||
const { setDialogOpen } = useDevice();
|
||||
|
||||
useBackupReminder({
|
||||
@@ -15,5 +15,6 @@ export const KeyBackupReminder = (): JSX.Element => {
|
||||
sameSite: "strict",
|
||||
},
|
||||
});
|
||||
// deno-lint-ignore jsx-no-useless-fragment
|
||||
return <></>;
|
||||
};
|
||||
|
||||
@@ -7,13 +7,13 @@ import { Protobuf } from "@meshtastic/core";
|
||||
import { fromByteArray, toByteArray } from "base64-js";
|
||||
import cryptoRandomString from "crypto-random-string";
|
||||
import { useState } from "react";
|
||||
import { PkiRegenerateDialog } from "../Dialog/PkiRegenerateDialog";
|
||||
import { PkiRegenerateDialog } from "../Dialog/PkiRegenerateDialog.tsx";
|
||||
|
||||
export interface SettingsPanelProps {
|
||||
channel: Protobuf.Channel.Channel;
|
||||
}
|
||||
|
||||
export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
|
||||
export const Channel = ({ channel }: SettingsPanelProps) => {
|
||||
const { config, connection, addChannel } = useDevice();
|
||||
const { toast } = useToast();
|
||||
|
||||
@@ -24,8 +24,9 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
|
||||
channel?.settings?.psk.length ?? 16,
|
||||
);
|
||||
const [validationText, setValidationText] = useState<string>();
|
||||
const [preSharedDialogOpen, setPreSharedDialogOpen] =
|
||||
useState<boolean>(false);
|
||||
const [preSharedDialogOpen, setPreSharedDialogOpen] = useState<boolean>(
|
||||
false,
|
||||
);
|
||||
|
||||
const onSubmit = (data: ChannelValidation) => {
|
||||
const channel = create(Protobuf.Channel.ChannelSchema, {
|
||||
@@ -92,7 +93,7 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
|
||||
<DynamicForm<ChannelValidation>
|
||||
onSubmit={onSubmit}
|
||||
submitType="onSubmit"
|
||||
hasSubmitButton={true}
|
||||
hasSubmitButton
|
||||
defaultValues={{
|
||||
...channel,
|
||||
...{
|
||||
@@ -107,7 +108,7 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
|
||||
channel?.settings?.moduleSettings?.positionPrecision === 32,
|
||||
positionPrecision:
|
||||
channel?.settings?.moduleSettings?.positionPrecision ===
|
||||
undefined
|
||||
undefined
|
||||
? 10
|
||||
: channel?.settings?.moduleSettings?.positionPrecision,
|
||||
},
|
||||
@@ -126,10 +127,9 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
|
||||
description:
|
||||
"Device telemetry is sent over PRIMARY. Only one PRIMARY allowed",
|
||||
properties: {
|
||||
enumValue:
|
||||
channel.index === 0
|
||||
? { PRIMARY: 1 }
|
||||
: { DISABLED: 0, SECONDARY: 2 },
|
||||
enumValue: channel.index === 0
|
||||
? { PRIMARY: 1 }
|
||||
: { DISABLED: 0, SECONDARY: 2 },
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -192,32 +192,31 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
|
||||
description:
|
||||
"If not sharing precise location, position shared on channel will be accurate within this distance",
|
||||
properties: {
|
||||
enumValue:
|
||||
config.display?.units === 0
|
||||
? {
|
||||
"Within 23 km": 10,
|
||||
"Within 12 km": 11,
|
||||
"Within 5.8 km": 12,
|
||||
"Within 2.9 km": 13,
|
||||
"Within 1.5 km": 14,
|
||||
"Within 700 m": 15,
|
||||
"Within 350 m": 16,
|
||||
"Within 200 m": 17,
|
||||
"Within 90 m": 18,
|
||||
"Within 50 m": 19,
|
||||
}
|
||||
: {
|
||||
"Within 15 miles": 10,
|
||||
"Within 7.3 miles": 11,
|
||||
"Within 3.6 miles": 12,
|
||||
"Within 1.8 miles": 13,
|
||||
"Within 0.9 miles": 14,
|
||||
"Within 0.5 miles": 15,
|
||||
"Within 0.2 miles": 16,
|
||||
"Within 600 feet": 17,
|
||||
"Within 300 feet": 18,
|
||||
"Within 150 feet": 19,
|
||||
},
|
||||
enumValue: config.display?.units === 0
|
||||
? {
|
||||
"Within 23 km": 10,
|
||||
"Within 12 km": 11,
|
||||
"Within 5.8 km": 12,
|
||||
"Within 2.9 km": 13,
|
||||
"Within 1.5 km": 14,
|
||||
"Within 700 m": 15,
|
||||
"Within 350 m": 16,
|
||||
"Within 200 m": 17,
|
||||
"Within 90 m": 18,
|
||||
"Within 50 m": 19,
|
||||
}
|
||||
: {
|
||||
"Within 15 miles": 10,
|
||||
"Within 7.3 miles": 11,
|
||||
"Within 3.6 miles": 12,
|
||||
"Within 1.8 miles": 13,
|
||||
"Within 0.9 miles": 14,
|
||||
"Within 0.5 miles": 15,
|
||||
"Within 0.2 miles": 16,
|
||||
"Within 600 feet": 17,
|
||||
"Within 300 feet": 18,
|
||||
"Within 150 feet": 19,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useAppStore } from "@app/core/stores/appStore";
|
||||
import { useAppStore } from "../../../core/stores/appStore.ts";
|
||||
import type { BluetoothValidation } from "@app/validation/config/bluetooth.tsx";
|
||||
import { create } from "@bufbuild/protobuf";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||
@@ -111,9 +111,8 @@ export const Bluetooth = () => {
|
||||
disabledBy: [
|
||||
{
|
||||
fieldName: "mode",
|
||||
selector:
|
||||
Protobuf.Config.Config_BluetoothConfig_PairingMode
|
||||
.FIXED_PIN,
|
||||
selector: Protobuf.Config.Config_BluetoothConfig_PairingMode
|
||||
.FIXED_PIN,
|
||||
invert: true,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -4,7 +4,7 @@ import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
|
||||
export const Device = (): JSX.Element => {
|
||||
export const Device = () => {
|
||||
const { config, setWorkingConfig } = useDevice();
|
||||
|
||||
const onSubmit = (data: DeviceValidation) => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
|
||||
export const Display = (): JSX.Element => {
|
||||
export const Display = () => {
|
||||
const { config, setWorkingConfig } = useDevice();
|
||||
|
||||
const onSubmit = (data: DisplayValidation) => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
|
||||
export const LoRa = (): JSX.Element => {
|
||||
export const LoRa = () => {
|
||||
const { config, setWorkingConfig } = useDevice();
|
||||
|
||||
const onSubmit = (data: LoRaValidation) => {
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from "@core/utils/ip.ts";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
|
||||
export const Network = (): JSX.Element => {
|
||||
export const Network = () => {
|
||||
const { config, setWorkingConfig } = useDevice();
|
||||
|
||||
const onSubmit = (data: NetworkValidation) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
type FlagName,
|
||||
usePositionFlags,
|
||||
} from "@app/core/hooks/usePositionFlags";
|
||||
} from "../../../core/hooks/usePositionFlags.ts";
|
||||
import type { PositionValidation } from "@app/validation/config/position.tsx";
|
||||
import { create } from "@bufbuild/protobuf";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||
|
||||
@@ -4,7 +4,7 @@ import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
|
||||
export const Power = (): JSX.Element => {
|
||||
export const Power = () => {
|
||||
const { config, setWorkingConfig } = useDevice();
|
||||
|
||||
const onSubmit = (data: PowerValidation) => {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { PkiRegenerateDialog } from "@app/components/Dialog/PkiRegenerateDialog";
|
||||
import { PkiRegenerateDialog } from "../../../Dialog/PkiRegenerateDialog.tsx";
|
||||
import { DynamicForm } from "@app/components/Form/DynamicForm.tsx";
|
||||
import { useAppStore } from "@app/core/stores/appStore";
|
||||
import { useAppStore } from "../../../../core/stores/appStore.ts";
|
||||
import {
|
||||
getX25519PrivateKey,
|
||||
getX25519PublicKey,
|
||||
} from "@app/core/utils/x25519";
|
||||
} from "../../../../core/utils/x25519.ts";
|
||||
import type { SecurityValidation } from "@app/validation/config/security.tsx";
|
||||
import { create } from "@bufbuild/protobuf";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
@@ -12,7 +12,7 @@ import { Protobuf } from "@meshtastic/core";
|
||||
import { fromByteArray, toByteArray } from "base64-js";
|
||||
import { Eye, EyeOff } from "lucide-react";
|
||||
import { useReducer } from "react";
|
||||
import { securityReducer } from "./securityReducer";
|
||||
import { securityReducer } from "./securityReducer.tsx";
|
||||
|
||||
export const Security = () => {
|
||||
const { config, setWorkingConfig, setDialogOpen } = useDevice();
|
||||
@@ -308,8 +308,7 @@ export const Security = () => {
|
||||
<PkiRegenerateDialog
|
||||
open={state.privateKeyDialogOpen}
|
||||
onOpenChange={() =>
|
||||
dispatch({ type: "SHOW_PRIVATE_KEY_DIALOG", payload: false })
|
||||
}
|
||||
dispatch({ type: "SHOW_PRIVATE_KEY_DIALOG", payload: false })}
|
||||
onSubmit={pkiRegenerate}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { SecurityAction, SecurityState } from "./types";
|
||||
import type { SecurityAction, SecurityState } from "./types.ts";
|
||||
|
||||
export function securityReducer(
|
||||
state: SecurityState,
|
||||
|
||||
@@ -18,7 +18,7 @@ export type SecurityAction =
|
||||
| { type: "SET_ADMIN_KEY"; payload: string }
|
||||
| { type: "SHOW_PRIVATE_KEY_DIALOG"; payload: boolean }
|
||||
| {
|
||||
type: "REGENERATE_PRIV_PUB_KEY";
|
||||
payload: { privateKey: string; publicKey: string };
|
||||
}
|
||||
type: "REGENERATE_PRIV_PUB_KEY";
|
||||
payload: { privateKey: string; publicKey: string };
|
||||
}
|
||||
| { type: "REGENERATE_ADMIN_KEY"; payload: { adminKey: string } };
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { TabElementProps } from "@app/components/Dialog/NewDeviceDialog";
|
||||
import type { TabElementProps } from "../../Dialog/NewDeviceDialog.tsx";
|
||||
import { Button } from "@components/UI/Button.tsx";
|
||||
import { Mono } from "@components/generic/Mono.tsx";
|
||||
import { useAppStore } from "@core/stores/appStore.ts";
|
||||
@@ -8,7 +8,7 @@ import { randId } from "@core/utils/randId.ts";
|
||||
import { BleConnection, Constants } from "@meshtastic/js";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
export const BLE = ({ closeDialog }: TabElementProps): JSX.Element => {
|
||||
export const BLE = ({ closeDialog }: TabElementProps) => {
|
||||
const [bleDevices, setBleDevices] = useState<BluetoothDevice[]>([]);
|
||||
const { addDevice } = useDeviceStore();
|
||||
const { setSelectedDevice } = useAppStore();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { TabElementProps } from "@app/components/Dialog/NewDeviceDialog";
|
||||
import type { TabElementProps } from "@app/components/Dialog/NewDeviceDialog.tsx";
|
||||
import { Button } from "@components/UI/Button.tsx";
|
||||
import { Input } from "@components/UI/Input.tsx";
|
||||
import { Label } from "@components/UI/Label.tsx";
|
||||
@@ -9,23 +9,23 @@ import { subscribeAll } from "@core/subscriptions.ts";
|
||||
import { randId } from "@core/utils/randId.ts";
|
||||
import { MeshDevice } from "@meshtastic/core";
|
||||
import { TransportHTTP } from "@meshtastic/transport-http";
|
||||
import { type JSX, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
export const HTTP = ({ closeDialog }: TabElementProps): JSX.Element => {
|
||||
export const HTTP = ({ closeDialog }: TabElementProps) => {
|
||||
const [https, setHTTPS] = useState(false);
|
||||
const { addDevice } = useDeviceStore();
|
||||
const { setSelectedDevice } = useAppStore();
|
||||
const { register, handleSubmit, control, watch } = useForm<{
|
||||
const { register, handleSubmit, control } = useForm<{
|
||||
ip: string;
|
||||
tls: boolean;
|
||||
}>({
|
||||
defaultValues: {
|
||||
ip: ["client.meshtastic.org", "localhost"].includes(
|
||||
window.location.hostname,
|
||||
)
|
||||
globalThis.location.hostname,
|
||||
)
|
||||
? "meshtastic.local"
|
||||
: window.location.host,
|
||||
: globalThis.location.host,
|
||||
tls: location.protocol === "https:",
|
||||
},
|
||||
});
|
||||
@@ -61,16 +61,15 @@ export const HTTP = ({ closeDialog }: TabElementProps): JSX.Element => {
|
||||
<Controller
|
||||
name="tls"
|
||||
control={control}
|
||||
render={({ field: { value, onChange, ...rest } }) => (
|
||||
render={({ field: { ...rest } }) => (
|
||||
<>
|
||||
<Label>Use HTTPS</Label>
|
||||
<Switch
|
||||
onCheckedChange={(checked) => {
|
||||
onCheckedChange={(checked: boolean) => {
|
||||
checked ? setHTTPS(true) : setHTTPS(false);
|
||||
}}
|
||||
disabled={
|
||||
location.protocol === "https:" || connectionInProgress
|
||||
}
|
||||
disabled={location.protocol === "https:" ||
|
||||
connectionInProgress}
|
||||
checked={https}
|
||||
{...rest}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { TabElementProps } from "@app/components/Dialog/NewDeviceDialog";
|
||||
import type { TabElementProps } from "../../Dialog/NewDeviceDialog.tsx";
|
||||
import { Button } from "@components/UI/Button.tsx";
|
||||
import { Mono } from "@components/generic/Mono.tsx";
|
||||
import { useAppStore } from "@core/stores/appStore.ts";
|
||||
@@ -9,7 +9,7 @@ import { MeshDevice } from "@meshtastic/core";
|
||||
import { TransportWebSerial } from "@meshtastic/transport-web-serial";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
export const Serial = ({ closeDialog }: TabElementProps): JSX.Element => {
|
||||
export const Serial = ({ closeDialog }: TabElementProps) => {
|
||||
const [serialPorts, setSerialPorts] = useState<SerialPort[]>([]);
|
||||
const { addDevice } = useDeviceStore();
|
||||
const { setSelectedDevice } = useAppStore();
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Separator } from "@app/components/UI/Seperator";
|
||||
import { Separator } from "../../UI/Seperator.tsx";
|
||||
import { H5 } from "@app/components/UI/Typography/H5.tsx";
|
||||
import { Subtle } from "@app/components/UI/Typography/Subtle.tsx";
|
||||
import { formatQuantity } from "@app/core/utils/string";
|
||||
import { Avatar } from "@components/UI/Avatar";
|
||||
import { formatQuantity } from "../../../core/utils/string.ts";
|
||||
import { Avatar } from "../../UI/Avatar.tsx";
|
||||
import { Mono } from "@components/generic/Mono.tsx";
|
||||
import { TimeAgo } from "@components/generic/TimeAgo.tsx";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
@@ -37,21 +37,23 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
|
||||
<Avatar text={node.user?.shortName} />
|
||||
|
||||
<div>
|
||||
{node.user?.publicKey && node.user?.publicKey.length > 0 ? (
|
||||
<LockIcon
|
||||
className="text-green-600"
|
||||
size={12}
|
||||
strokeWidth={3}
|
||||
aria-label="Public Key Enabled"
|
||||
/>
|
||||
) : (
|
||||
<LockOpenIcon
|
||||
className="text-yellow-500"
|
||||
size={12}
|
||||
strokeWidth={3}
|
||||
aria-label="No Public Key"
|
||||
/>
|
||||
)}
|
||||
{node.user?.publicKey && node.user?.publicKey.length > 0
|
||||
? (
|
||||
<LockIcon
|
||||
className="text-green-600"
|
||||
size={12}
|
||||
strokeWidth={3}
|
||||
aria-label="Public Key Enabled"
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<LockOpenIcon
|
||||
className="text-yellow-500"
|
||||
size={12}
|
||||
strokeWidth={3}
|
||||
aria-label="No Public Key"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Star
|
||||
@@ -73,15 +75,13 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
|
||||
node.deviceMetrics?.voltage?.toPrecision(3) ?? "Unknown"
|
||||
} volts`}
|
||||
>
|
||||
{node.deviceMetrics?.batteryLevel > 100 ? (
|
||||
<BatteryChargingIcon size={22} />
|
||||
) : node.deviceMetrics?.batteryLevel > 80 ? (
|
||||
<BatteryFullIcon size={22} />
|
||||
) : node.deviceMetrics?.batteryLevel > 20 ? (
|
||||
<BatteryMediumIcon size={22} />
|
||||
) : (
|
||||
<BatteryLowIcon size={22} />
|
||||
)}
|
||||
{node.deviceMetrics?.batteryLevel > 100
|
||||
? <BatteryChargingIcon size={22} />
|
||||
: node.deviceMetrics?.batteryLevel > 80
|
||||
? <BatteryFullIcon size={22} />
|
||||
: node.deviceMetrics?.batteryLevel > 20
|
||||
? <BatteryMediumIcon size={22} />
|
||||
: <BatteryLowIcon size={22} />}
|
||||
<Subtle aria-label="Battery">
|
||||
{node.deviceMetrics?.batteryLevel > 100
|
||||
? "Charging"
|
||||
|
||||
@@ -7,7 +7,6 @@ import { MessageInput } from "@components/PageComponents/Messages/MessageInput.t
|
||||
import type { Types } from "@meshtastic/core";
|
||||
import { InboxIcon } from "lucide-react";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import type { JSX } from "react";
|
||||
|
||||
export interface ChannelChatProps {
|
||||
messages?: MessageWithState[];
|
||||
@@ -26,7 +25,7 @@ export const ChannelChat = ({
|
||||
messages,
|
||||
channel,
|
||||
to,
|
||||
}: ChannelChatProps): JSX.Element => {
|
||||
}: ChannelChatProps) => {
|
||||
const { nodes } = useDevice();
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -34,8 +33,7 @@ export const ChannelChat = ({
|
||||
const scrollToBottom = useCallback(() => {
|
||||
const scrollContainer = scrollContainerRef.current;
|
||||
if (scrollContainer) {
|
||||
const isNearBottom =
|
||||
scrollContainer.scrollHeight -
|
||||
const isNearBottom = scrollContainer.scrollHeight -
|
||||
scrollContainer.scrollTop -
|
||||
scrollContainer.clientHeight <
|
||||
100;
|
||||
@@ -72,9 +70,8 @@ export const ChannelChat = ({
|
||||
key={message.id}
|
||||
message={message}
|
||||
sender={nodes.get(message.from)}
|
||||
lastMsgSameUser={
|
||||
index > 0 && messages[index - 1].from === message.from
|
||||
}
|
||||
lastMsgSameUser={index > 0 &&
|
||||
messages[index - 1].from === message.from}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -4,14 +4,13 @@ import {
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@app/components/UI/Tooltip";
|
||||
import { useAppStore } from "@app/core/stores/appStore";
|
||||
} from "@components/UI/Tooltip.tsx";
|
||||
import {
|
||||
type MessageWithState,
|
||||
useDeviceStore,
|
||||
} from "@app/core/stores/deviceStore.ts";
|
||||
import { cn } from "@app/core/utils/cn";
|
||||
import { Avatar } from "@components/UI/Avatar";
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
import { Avatar } from "@components/UI/Avatar.tsx";
|
||||
import type { Protobuf } from "@meshtastic/core";
|
||||
import { AlertCircle, CheckCircle2, CircleEllipsis } from "lucide-react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
@@ -94,7 +93,6 @@ const StatusIcon = ({ state, className, ...otherProps }: StatusIconProps) => {
|
||||
const getMessageTextStyles = (state: MessageState) => {
|
||||
const isAcknowledged = state === MESSAGE_STATES.ACK;
|
||||
const isFailed = state === MESSAGE_STATES.FAILED;
|
||||
const isWaiting = state === MESSAGE_STATES.WAITING;
|
||||
|
||||
return cn(
|
||||
"break-words overflow-hidden",
|
||||
@@ -145,16 +143,18 @@ export const Message = ({ lastMsgSameUser, message, sender }: MessageProps) => {
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
{!lastMsgSameUser ? (
|
||||
<div className="flex place-items-center gap-2 mb-1">
|
||||
<Avatar text={messageUser?.shortName} />
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium text-slate-900 dark:text-white truncate">
|
||||
{messageUser?.longName}
|
||||
</span>
|
||||
{!lastMsgSameUser
|
||||
? (
|
||||
<div className="flex place-items-center gap-2 mb-1">
|
||||
<Avatar text={messageUser?.shortName} />
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium text-slate-900 dark:text-white truncate">
|
||||
{messageUser?.longName}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
)
|
||||
: null}
|
||||
</div>
|
||||
<TimeDisplay date={message.rxTime} />
|
||||
<div className="flex place-items-center gap-2 pb-2">
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
import { debounce } from "@app/core/utils/debounce";
|
||||
import { debounce } from "../../../core/utils/debounce.ts";
|
||||
import { Button } from "@components/UI/Button.tsx";
|
||||
import { Input } from "@components/UI/Input.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import type { Types } from "@meshtastic/core";
|
||||
import { SendIcon } from "lucide-react";
|
||||
import {
|
||||
type JSX,
|
||||
startTransition,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { startTransition, useCallback, useMemo, useState } from "react";
|
||||
|
||||
export interface MessageInputProps {
|
||||
to: Types.Destination;
|
||||
@@ -22,7 +16,7 @@ export const MessageInput = ({
|
||||
to,
|
||||
channel,
|
||||
maxBytes,
|
||||
}: MessageInputProps): JSX.Element => {
|
||||
}: MessageInputProps) => {
|
||||
const {
|
||||
connection,
|
||||
setMessageState,
|
||||
@@ -43,7 +37,7 @@ export const MessageInput = ({
|
||||
async (message: string) => {
|
||||
await connection
|
||||
?.sendText(message, to, true, channel)
|
||||
.then((id) =>
|
||||
.then((id: number) =>
|
||||
setMessageState(
|
||||
to === "broadcast" ? "broadcast" : "direct",
|
||||
channel,
|
||||
@@ -51,7 +45,7 @@ export const MessageInput = ({
|
||||
myNodeNum,
|
||||
id,
|
||||
"ack",
|
||||
),
|
||||
)
|
||||
)
|
||||
.catch((e: Types.PacketError) =>
|
||||
setMessageState(
|
||||
@@ -61,7 +55,7 @@ export const MessageInput = ({
|
||||
myNodeNum,
|
||||
e.id,
|
||||
e.error,
|
||||
),
|
||||
)
|
||||
);
|
||||
},
|
||||
[channel, connection, myNodeNum, setMessageState, to],
|
||||
@@ -82,7 +76,7 @@ export const MessageInput = ({
|
||||
<div className="flex gap-2">
|
||||
<form
|
||||
className="w-full"
|
||||
action={async (formData: FormData) => {
|
||||
action={(formData: FormData) => {
|
||||
// prevent user from sending blank/empty message
|
||||
if (localDraft === "") return;
|
||||
const message = formData.get("messageInput") as string;
|
||||
@@ -97,7 +91,7 @@ export const MessageInput = ({
|
||||
<div className="flex grow gap-2">
|
||||
<span className="w-full">
|
||||
<Input
|
||||
autoFocus={true}
|
||||
autoFocus
|
||||
minLength={1}
|
||||
name="messageInput"
|
||||
placeholder="Enter Message"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useDevice } from "@app/core/stores/deviceStore.ts";
|
||||
import type { Protobuf } from "@meshtastic/core";
|
||||
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
||||
import type { JSX } from "react";
|
||||
|
||||
export interface TraceRouteProps {
|
||||
from?: Protobuf.Mesh.NodeInfo;
|
||||
@@ -19,7 +18,7 @@ export const TraceRoute = ({
|
||||
routeBack,
|
||||
snrTowards,
|
||||
snrBack,
|
||||
}: TraceRouteProps): JSX.Element => {
|
||||
}: TraceRouteProps) => {
|
||||
const { nodes } = useDevice();
|
||||
|
||||
return (
|
||||
@@ -38,23 +37,25 @@ export const TraceRoute = ({
|
||||
))}
|
||||
{from?.user?.longName}
|
||||
</span>
|
||||
{routeBack ? (
|
||||
<span className="ml-4 border-l-2 border-l-background-primary pl-2 text-text-primary">
|
||||
<p className="font-semibold">Route back:</p>
|
||||
<p>{from?.user?.longName}</p>
|
||||
<p>↓ {snrBack?.[0] ? snrBack[0] : "??"}dB</p>
|
||||
{routeBack.map((hop, i) => (
|
||||
<span key={nodes.get(hop)?.num}>
|
||||
<p>
|
||||
{nodes.get(hop)?.user?.longName ??
|
||||
`!${numberToHexUnpadded(hop)}`}
|
||||
</p>
|
||||
<p>↓ {snrBack?.[i + 1] ? snrBack[i + 1] : "??"}dB</p>
|
||||
</span>
|
||||
))}
|
||||
{to?.user?.longName}
|
||||
</span>
|
||||
) : null}
|
||||
{routeBack
|
||||
? (
|
||||
<span className="ml-4 border-l-2 border-l-background-primary pl-2 text-text-primary">
|
||||
<p className="font-semibold">Route back:</p>
|
||||
<p>{from?.user?.longName}</p>
|
||||
<p>↓ {snrBack?.[0] ? snrBack[0] : "??"}dB</p>
|
||||
{routeBack.map((hop, i) => (
|
||||
<span key={nodes.get(hop)?.num}>
|
||||
<p>
|
||||
{nodes.get(hop)?.user?.longName ??
|
||||
`!${numberToHexUnpadded(hop)}`}
|
||||
</p>
|
||||
<p>↓ {snrBack?.[i + 1] ? snrBack[i + 1] : "??"}dB</p>
|
||||
</span>
|
||||
))}
|
||||
{to?.user?.longName}
|
||||
</span>
|
||||
)
|
||||
: null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import { create } from "@bufbuild/protobuf";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
|
||||
export const AmbientLighting = (): JSX.Element => {
|
||||
export const AmbientLighting = () => {
|
||||
const { moduleConfig, setWorkingModuleConfig } = useDevice();
|
||||
|
||||
const onSubmit = (data: AmbientLightingValidation) => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
|
||||
export const Audio = (): JSX.Element => {
|
||||
export const Audio = () => {
|
||||
const { moduleConfig, setWorkingModuleConfig } = useDevice();
|
||||
|
||||
const onSubmit = (data: AudioValidation) => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
|
||||
export const CannedMessage = (): JSX.Element => {
|
||||
export const CannedMessage = () => {
|
||||
const { moduleConfig, setWorkingModuleConfig } = useDevice();
|
||||
|
||||
const onSubmit = (data: CannedMessageValidation) => {
|
||||
@@ -63,9 +63,8 @@ export const CannedMessage = (): JSX.Element => {
|
||||
label: "Clockwise event",
|
||||
description: "Select input event.",
|
||||
properties: {
|
||||
enumValue:
|
||||
Protobuf.ModuleConfig
|
||||
.ModuleConfig_CannedMessageConfig_InputEventChar,
|
||||
enumValue: Protobuf.ModuleConfig
|
||||
.ModuleConfig_CannedMessageConfig_InputEventChar,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -74,9 +73,8 @@ export const CannedMessage = (): JSX.Element => {
|
||||
label: "Counter Clockwise event",
|
||||
description: "Select input event.",
|
||||
properties: {
|
||||
enumValue:
|
||||
Protobuf.ModuleConfig
|
||||
.ModuleConfig_CannedMessageConfig_InputEventChar,
|
||||
enumValue: Protobuf.ModuleConfig
|
||||
.ModuleConfig_CannedMessageConfig_InputEventChar,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -85,9 +83,8 @@ export const CannedMessage = (): JSX.Element => {
|
||||
label: "Press event",
|
||||
description: "Select input event",
|
||||
properties: {
|
||||
enumValue:
|
||||
Protobuf.ModuleConfig
|
||||
.ModuleConfig_CannedMessageConfig_InputEventChar,
|
||||
enumValue: Protobuf.ModuleConfig
|
||||
.ModuleConfig_CannedMessageConfig_InputEventChar,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -4,7 +4,7 @@ import { create } from "@bufbuild/protobuf";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
|
||||
export const DetectionSensor = (): JSX.Element => {
|
||||
export const DetectionSensor = () => {
|
||||
const { moduleConfig, setWorkingModuleConfig } = useDevice();
|
||||
|
||||
const onSubmit = (data: DetectionSensorValidation) => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
|
||||
export const ExternalNotification = (): JSX.Element => {
|
||||
export const ExternalNotification = () => {
|
||||
const { moduleConfig, setWorkingModuleConfig } = useDevice();
|
||||
|
||||
const onSubmit = (data: ExternalNotificationValidation) => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { create } from "@bufbuild/protobuf";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
|
||||
export const MQTT = (): JSX.Element => {
|
||||
export const MQTT = () => {
|
||||
const { config, moduleConfig, setWorkingModuleConfig } = useDevice();
|
||||
|
||||
const onSubmit = (data: MqttValidation) => {
|
||||
@@ -165,32 +165,31 @@ export const MQTT = (): JSX.Element => {
|
||||
description:
|
||||
"Position shared will be accurate within this distance",
|
||||
properties: {
|
||||
enumValue:
|
||||
config.display?.units === 0
|
||||
? {
|
||||
"Within 23 km": 10,
|
||||
"Within 12 km": 11,
|
||||
"Within 5.8 km": 12,
|
||||
"Within 2.9 km": 13,
|
||||
"Within 1.5 km": 14,
|
||||
"Within 700 m": 15,
|
||||
"Within 350 m": 16,
|
||||
"Within 200 m": 17,
|
||||
"Within 90 m": 18,
|
||||
"Within 50 m": 19,
|
||||
}
|
||||
: {
|
||||
"Within 15 miles": 10,
|
||||
"Within 7.3 miles": 11,
|
||||
"Within 3.6 miles": 12,
|
||||
"Within 1.8 miles": 13,
|
||||
"Within 0.9 miles": 14,
|
||||
"Within 0.5 miles": 15,
|
||||
"Within 0.2 miles": 16,
|
||||
"Within 600 feet": 17,
|
||||
"Within 300 feet": 18,
|
||||
"Within 150 feet": 19,
|
||||
},
|
||||
enumValue: config.display?.units === 0
|
||||
? {
|
||||
"Within 23 km": 10,
|
||||
"Within 12 km": 11,
|
||||
"Within 5.8 km": 12,
|
||||
"Within 2.9 km": 13,
|
||||
"Within 1.5 km": 14,
|
||||
"Within 700 m": 15,
|
||||
"Within 350 m": 16,
|
||||
"Within 200 m": 17,
|
||||
"Within 90 m": 18,
|
||||
"Within 50 m": 19,
|
||||
}
|
||||
: {
|
||||
"Within 15 miles": 10,
|
||||
"Within 7.3 miles": 11,
|
||||
"Within 3.6 miles": 12,
|
||||
"Within 1.8 miles": 13,
|
||||
"Within 0.9 miles": 14,
|
||||
"Within 0.5 miles": 15,
|
||||
"Within 0.2 miles": 16,
|
||||
"Within 600 feet": 17,
|
||||
"Within 300 feet": 18,
|
||||
"Within 150 feet": 19,
|
||||
},
|
||||
},
|
||||
disabledBy: [
|
||||
{
|
||||
|
||||
@@ -4,7 +4,7 @@ import { create } from "@bufbuild/protobuf";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
|
||||
export const NeighborInfo = (): JSX.Element => {
|
||||
export const NeighborInfo = () => {
|
||||
const { moduleConfig, setWorkingModuleConfig } = useDevice();
|
||||
|
||||
const onSubmit = (data: NeighborInfoValidation) => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
|
||||
export const Paxcounter = (): JSX.Element => {
|
||||
export const Paxcounter = () => {
|
||||
const { moduleConfig, setWorkingModuleConfig } = useDevice();
|
||||
|
||||
const onSubmit = (data: PaxcounterValidation) => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
|
||||
export const RangeTest = (): JSX.Element => {
|
||||
export const RangeTest = () => {
|
||||
const { moduleConfig, setWorkingModuleConfig } = useDevice();
|
||||
|
||||
const onSubmit = (data: RangeTestValidation) => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
|
||||
export const Serial = (): JSX.Element => {
|
||||
export const Serial = () => {
|
||||
const { moduleConfig, setWorkingModuleConfig } = useDevice();
|
||||
|
||||
const onSubmit = (data: SerialValidation) => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
|
||||
export const StoreForward = (): JSX.Element => {
|
||||
export const StoreForward = () => {
|
||||
const { moduleConfig, setWorkingModuleConfig } = useDevice();
|
||||
|
||||
const onSubmit = (data: StoreForwardValidation) => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
|
||||
export const Telemetry = (): JSX.Element => {
|
||||
export const Telemetry = () => {
|
||||
const { moduleConfig, setWorkingModuleConfig } = useDevice();
|
||||
|
||||
const onSubmit = (data: TelemetryValidation) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { cn } from "@app/core/utils/cn.ts";
|
||||
import { AlignLeftIcon, type LucideIcon } from "lucide-react";
|
||||
import Footer from "./UI/Footer";
|
||||
import { Spinner } from "./UI/Spinner";
|
||||
import Footer from "./UI/Footer.tsx";
|
||||
import { Spinner } from "./UI/Spinner.tsx";
|
||||
|
||||
export interface PageLayoutProps {
|
||||
label: string;
|
||||
@@ -44,9 +44,7 @@ export const PageLayout = ({
|
||||
className="transition-all hover:text-accent"
|
||||
onClick={action.onClick}
|
||||
>
|
||||
{action?.isLoading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
{action?.isLoading ? <Spinner /> : (
|
||||
<action.icon
|
||||
className={action.iconClasses}
|
||||
aria-disabled={action.disabled}
|
||||
|
||||
+65
-63
@@ -23,7 +23,7 @@ export interface SidebarProps {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const Sidebar = ({ children }: SidebarProps): JSX.Element => {
|
||||
export const Sidebar = ({ children }: SidebarProps) => {
|
||||
const { hardware, nodes, metadata } = useDevice();
|
||||
const myNode = nodes.get(hardware.myNodeNum);
|
||||
const myMetadata = metadata.get(0);
|
||||
@@ -64,69 +64,71 @@ export const Sidebar = ({ children }: SidebarProps): JSX.Element => {
|
||||
},
|
||||
];
|
||||
|
||||
return showSidebar ? (
|
||||
<div className="min-w-[280px] max-w-min flex-col overflow-y-auto border-r-[0.5px] bg-background-primary border-slate-300 dark:border-slate-700">
|
||||
<div className="flex justify-between px-8 pt-6">
|
||||
<div>
|
||||
<span className="text-lg font-medium">
|
||||
{myNode?.user?.shortName ?? "UNK"}
|
||||
</span>
|
||||
<Subtle>{myNode?.user?.longName ?? "UNK"}</Subtle>
|
||||
return showSidebar
|
||||
? (
|
||||
<div className="min-w-[280px] max-w-min flex-col overflow-y-auto border-r-[0.5px] bg-background-primary border-slate-300 dark:border-slate-700">
|
||||
<div className="flex justify-between px-8 pt-6">
|
||||
<div>
|
||||
<span className="text-lg font-medium">
|
||||
{myNode?.user?.shortName ?? "UNK"}
|
||||
</span>
|
||||
<Subtle>{myNode?.user?.longName ?? "UNK"}</Subtle>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="transition-all hover:text-accent"
|
||||
onClick={() => setDialogOpen("deviceName", true)}
|
||||
>
|
||||
<EditIcon size={16} />
|
||||
</button>
|
||||
<button type="button" onClick={() => setShowSidebar(false)}>
|
||||
<SidebarCloseIcon size={24} />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="transition-all hover:text-accent"
|
||||
onClick={() => setDialogOpen("deviceName", true)}
|
||||
>
|
||||
<EditIcon size={16} />
|
||||
</button>
|
||||
<button type="button" onClick={() => setShowSidebar(false)}>
|
||||
<SidebarCloseIcon size={24} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="px-8 pb-6">
|
||||
<div className="flex items-center">
|
||||
<BatteryMediumIcon size={24} viewBox={"0 0 28 24"} />
|
||||
<Subtle>
|
||||
{myNode?.deviceMetrics?.batteryLevel
|
||||
? myNode?.deviceMetrics?.batteryLevel > 100
|
||||
? "Charging"
|
||||
: `${myNode?.deviceMetrics?.batteryLevel}%`
|
||||
: "UNK"}
|
||||
</Subtle>
|
||||
<div className="px-8 pb-6">
|
||||
<div className="flex items-center">
|
||||
<BatteryMediumIcon size={24} viewBox="0 0 28 24" />
|
||||
<Subtle>
|
||||
{myNode?.deviceMetrics?.batteryLevel
|
||||
? myNode?.deviceMetrics?.batteryLevel > 100
|
||||
? "Charging"
|
||||
: `${myNode?.deviceMetrics?.batteryLevel}%`
|
||||
: "UNK"}
|
||||
</Subtle>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<ZapIcon size={24} viewBox="0 0 36 24" />
|
||||
<Subtle>
|
||||
{myNode?.deviceMetrics?.voltage?.toPrecision(3) ?? "UNK"} volts
|
||||
</Subtle>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<CpuIcon size={24} viewBox="0 0 36 24" />
|
||||
<Subtle>v{myMetadata?.firmwareVersion ?? "UNK"}</Subtle>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<ZapIcon size={24} viewBox={"0 0 36 24"} />
|
||||
<Subtle>
|
||||
{myNode?.deviceMetrics?.voltage?.toPrecision(3) ?? "UNK"} volts
|
||||
</Subtle>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<CpuIcon size={24} viewBox={"0 0 36 24"} />
|
||||
<Subtle>v{myMetadata?.firmwareVersion ?? "UNK"}</Subtle>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SidebarSection label="Navigation">
|
||||
{pages.map((link) => (
|
||||
<SidebarButton
|
||||
key={link.name}
|
||||
label={link.name}
|
||||
Icon={link.icon}
|
||||
onClick={() => {
|
||||
setActivePage(link.page);
|
||||
}}
|
||||
active={link.page === activePage}
|
||||
/>
|
||||
))}
|
||||
</SidebarSection>
|
||||
{children}
|
||||
</div>
|
||||
) : (
|
||||
<div className="px-1 pt-8 border-r-[0.5px] border-slate-700">
|
||||
<button type="button" onClick={() => setShowSidebar(true)}>
|
||||
<SidebarOpenIcon size={24} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
<SidebarSection label="Navigation">
|
||||
{pages.map((link) => (
|
||||
<SidebarButton
|
||||
key={link.name}
|
||||
label={link.name}
|
||||
Icon={link.icon}
|
||||
onClick={() => {
|
||||
setActivePage(link.page);
|
||||
}}
|
||||
active={link.page === activePage}
|
||||
/>
|
||||
))}
|
||||
</SidebarSection>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div className="px-1 pt-8 border-r-[0.5px] border-slate-700">
|
||||
<button type="button" onClick={() => setShowSidebar(true)}>
|
||||
<SidebarOpenIcon size={24} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useTheme } from "@app/core/hooks/useTheme";
|
||||
import { cn } from "@app/core/utils/cn";
|
||||
import { useTheme } from "../core/hooks/useTheme.ts";
|
||||
import { cn } from "../core/utils/cn.ts";
|
||||
import { Monitor, Moon, Sun } from "lucide-react";
|
||||
|
||||
type ThemePreference = "light" | "dark" | "system";
|
||||
@@ -32,11 +32,9 @@ export default function ThemeSwitcher({
|
||||
className,
|
||||
)}
|
||||
onClick={toggleTheme}
|
||||
aria-label={
|
||||
preference === "system"
|
||||
? `System theme (currently ${theme}). Click to change theme.`
|
||||
: `Current theme: ${theme}. Click to change theme.`
|
||||
}
|
||||
aria-label={preference === "system"
|
||||
? `System theme (currently ${theme}). Click to change theme.`
|
||||
: `Current theme: ${theme}. Click to change theme.`}
|
||||
>
|
||||
{themeIcons[preference]}
|
||||
</button>
|
||||
|
||||
@@ -5,8 +5,8 @@ import {
|
||||
ToastProvider,
|
||||
ToastTitle,
|
||||
ToastViewport,
|
||||
} from "@components/UI/Toast";
|
||||
import { useToast } from "@core/hooks/useToast";
|
||||
} from "./UI/Toast.tsx";
|
||||
import { useToast } from "../core/hooks/useToast.ts";
|
||||
|
||||
export function Toaster() {
|
||||
const { toasts } = useToast();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { cn } from "@app/core/utils/cn";
|
||||
import { cn } from "../../core/utils/cn.ts";
|
||||
import type React from "react";
|
||||
|
||||
type RGBColor = {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type VariantProps, cva } from "class-variance-authority";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
@@ -20,7 +20,8 @@ const buttonVariants = cva(
|
||||
"bg-slate-100 text-slate-700 hover:bg-slate-200 dark:bg-slate-500 dark:text-white dark:hover:bg-slate-400",
|
||||
ghost:
|
||||
"bg-transparent hover:bg-slate-100 dark:hover:bg-slate-800 dark:text-slate-100 dark:hover:text-slate-100 data-[state=open]:bg-transparent dark:data-[state=open]:bg-transparent",
|
||||
link: "bg-transparent underline-offset-4 hover:underline text-slate-900 dark:text-slate-100 hover:bg-transparent dark:hover:bg-transparent",
|
||||
link:
|
||||
"bg-transparent underline-offset-4 hover:underline text-slate-900 dark:text-slate-100 hover:bg-transparent dark:hover:bg-transparent",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 py-2 px-4",
|
||||
@@ -38,7 +39,8 @@ const buttonVariants = cva(
|
||||
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
extends
|
||||
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
|
||||
@@ -144,11 +144,11 @@ CommandShortcut.displayName = "CommandShortcut";
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
CommandShortcut,
|
||||
};
|
||||
|
||||
@@ -23,7 +23,7 @@ DialogPortal.displayName = DialogPrimitive.Portal.displayName;
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/50 backdrop-blur-xs transition-opacity animate-in fade-in",
|
||||
@@ -113,10 +113,10 @@ DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
};
|
||||
|
||||
@@ -184,18 +184,18 @@ DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuTrigger,
|
||||
};
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { cn } from "@app/core/utils/cn";
|
||||
import { cn } from "../../core/utils/cn.ts";
|
||||
import React from "react";
|
||||
|
||||
export interface FooterProps extends React.HTMLAttributes<HTMLElement> {}
|
||||
|
||||
const Footer = React.forwardRef<HTMLElement, FooterProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
({ className, ...props }) => {
|
||||
return (
|
||||
<footer className={cn("flex mt-auto justify-center p-2", className)}>
|
||||
<footer
|
||||
className={cn("flex mt-auto justify-center p-2", className)}
|
||||
{...props}
|
||||
>
|
||||
<p>
|
||||
<a
|
||||
href="https://vercel.com/?utm_source=meshtastic&utm_campaign=oss"
|
||||
|
||||
@@ -54,7 +54,6 @@ const Generator = React.forwardRef<HTMLInputElement, GeneratorProps>(
|
||||
disabled,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
import { type VariantProps, cva } from "class-variance-authority";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
|
||||
const inputVariants = cva(
|
||||
@@ -20,7 +20,8 @@ const inputVariants = cva(
|
||||
);
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement>,
|
||||
extends
|
||||
React.InputHTMLAttributes<HTMLInputElement>,
|
||||
VariantProps<typeof inputVariants> {
|
||||
prefix?: string;
|
||||
suffix?: string;
|
||||
|
||||
@@ -216,19 +216,19 @@ MenubarShortcut.displayname = "MenubarShortcut";
|
||||
|
||||
export {
|
||||
Menubar,
|
||||
MenubarMenu,
|
||||
MenubarTrigger,
|
||||
MenubarContent,
|
||||
MenubarItem,
|
||||
MenubarSeparator,
|
||||
MenubarLabel,
|
||||
MenubarCheckboxItem,
|
||||
MenubarContent,
|
||||
MenubarGroup,
|
||||
MenubarItem,
|
||||
MenubarLabel,
|
||||
MenubarMenu,
|
||||
MenubarPortal,
|
||||
MenubarRadioGroup,
|
||||
MenubarRadioItem,
|
||||
MenubarPortal,
|
||||
MenubarSeparator,
|
||||
MenubarShortcut,
|
||||
MenubarSub,
|
||||
MenubarSubContent,
|
||||
MenubarSubTrigger,
|
||||
MenubarGroup,
|
||||
MenubarSub,
|
||||
MenubarShortcut,
|
||||
MenubarTrigger,
|
||||
};
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
export interface ModalProps {
|
||||
title: string;
|
||||
actions?: JSX.Element[];
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const Modal = ({
|
||||
title,
|
||||
actions,
|
||||
children,
|
||||
}: ModalProps): JSX.Element => {
|
||||
return (
|
||||
<div className="rounded-md overflow-hidden w-full">
|
||||
<div className="flex h-12 px-3 bg-slate-200 dark:bg-slate-700 justify-between">
|
||||
<h2 className="my-auto font-semibold text-lg">{title}</h2>
|
||||
{actions && (
|
||||
<div className="my-auto">{actions.map((action) => action)}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="h-full border border-slate-200 dark:border-slate-700">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { cn } from "@app/core/utils/cn";
|
||||
import { cn } from "../../core/utils/cn.ts";
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||
import { Check } from "lucide-react";
|
||||
|
||||
@@ -8,9 +8,8 @@ interface MultiSelectProps {
|
||||
}
|
||||
|
||||
const MultiSelect = ({ children, className = "" }: MultiSelectProps) => {
|
||||
return (
|
||||
<div className={cn("flex flex-wrap gap-2", className)}>{children}</div>
|
||||
);
|
||||
return <div className={cn("flex flex-wrap gap-2", className)}>{children}
|
||||
</div>;
|
||||
};
|
||||
|
||||
interface MultiSelectItemProps {
|
||||
|
||||
@@ -26,4 +26,4 @@ const PopoverContent = React.forwardRef<
|
||||
));
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent };
|
||||
export { Popover, PopoverContent, PopoverTrigger };
|
||||
|
||||
@@ -101,11 +101,11 @@ SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
};
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { Button } from "@components/UI/Button.tsx";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { JSX } from "react";
|
||||
|
||||
export interface SidebarButtonProps {
|
||||
label: string;
|
||||
active?: boolean;
|
||||
Icon?: LucideIcon;
|
||||
element?: JSX.Element;
|
||||
element?;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
@@ -16,7 +15,7 @@ export const SidebarButton = ({
|
||||
Icon,
|
||||
element,
|
||||
onClick,
|
||||
}: SidebarButtonProps): JSX.Element => (
|
||||
}: SidebarButtonProps) => (
|
||||
<Button
|
||||
onClick={onClick}
|
||||
variant={active ? "subtle" : "ghost"}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { cn } from "@app/core/utils/cn";
|
||||
import { cn } from "../../core/utils/cn.ts";
|
||||
|
||||
interface SpinnerProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
size?: "sm" | "md" | "lg";
|
||||
|
||||
@@ -50,4 +50,4 @@ const TabsContent = React.forwardRef<
|
||||
));
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
||||
export { Tabs, TabsContent, TabsList, TabsTrigger };
|
||||
|
||||
+11
-11
@@ -1,9 +1,9 @@
|
||||
import * as ToastPrimitives from "@radix-ui/react-toast";
|
||||
import { type VariantProps, cva } from "class-variance-authority";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { X } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@core/utils/cn";
|
||||
import { cn } from "../../core/utils/cn.ts";
|
||||
|
||||
const ToastProvider = ToastPrimitives.Provider;
|
||||
|
||||
@@ -41,8 +41,8 @@ const toastVariants = cva(
|
||||
|
||||
const Toast = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
||||
VariantProps<typeof toastVariants>
|
||||
& React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root>
|
||||
& VariantProps<typeof toastVariants>
|
||||
>(({ className, variant, ...props }, ref) => {
|
||||
return (
|
||||
<ToastPrimitives.Root
|
||||
@@ -116,13 +116,13 @@ type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
|
||||
type ToastActionElement = React.ReactElement<typeof ToastAction>;
|
||||
|
||||
export {
|
||||
type ToastProps,
|
||||
type ToastActionElement,
|
||||
ToastProvider,
|
||||
ToastViewport,
|
||||
Toast,
|
||||
ToastTitle,
|
||||
ToastDescription,
|
||||
ToastClose,
|
||||
ToastAction,
|
||||
type ToastActionElement,
|
||||
ToastClose,
|
||||
ToastDescription,
|
||||
type ToastProps,
|
||||
ToastProvider,
|
||||
ToastTitle,
|
||||
ToastViewport,
|
||||
};
|
||||
|
||||
@@ -29,8 +29,8 @@ TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||
|
||||
export {
|
||||
Tooltip,
|
||||
TooltipTrigger,
|
||||
TooltipArrow,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipArrow,
|
||||
TooltipTrigger,
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ export interface BlockquoteProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const BlockQuote = ({ children }: BlockquoteProps): JSX.Element => (
|
||||
export const BlockQuote = ({ children }: BlockquoteProps) => (
|
||||
<blockquote className="mt-6 border-l-2 border-slate-300 pl-6 italic text-slate-800 dark:border-slate-600 dark:text-slate-200">
|
||||
{children}
|
||||
</blockquote>
|
||||
|
||||
@@ -2,7 +2,7 @@ export interface CodeProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const Code = ({ children }: CodeProps): JSX.Element => (
|
||||
export const Code = ({ children }: CodeProps) => (
|
||||
<code className="relative rounded-sm bg-slate-100 px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold text-slate-900 dark:bg-slate-800 dark:text-slate-400">
|
||||
{children}
|
||||
</code>
|
||||
|
||||
@@ -2,7 +2,7 @@ export interface H1Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const H1 = ({ children }: H1Props): JSX.Element => (
|
||||
export const H1 = ({ children }: H1Props) => (
|
||||
<h1 className="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl">
|
||||
{children}
|
||||
</h1>
|
||||
|
||||
@@ -2,7 +2,7 @@ export interface H2Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const H2 = ({ children }: H2Props): JSX.Element => (
|
||||
export const H2 = ({ children }: H2Props) => (
|
||||
<h2 className="scroll-m-20 border-b border-b-slate-200 pb-2 text-3xl font-semibold tracking-tight transition-colors first:mt-0 dark:border-b-slate-700">
|
||||
{children}
|
||||
</h2>
|
||||
|
||||
@@ -2,7 +2,7 @@ export interface H3Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const H3 = ({ children }: H3Props): JSX.Element => (
|
||||
export const H3 = ({ children }: H3Props) => (
|
||||
<h3 className="scroll-m-20 text-2xl font-semibold tracking-tight">
|
||||
{children}
|
||||
</h3>
|
||||
|
||||
@@ -5,7 +5,7 @@ export interface H4Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const H4 = ({ className, children }: H4Props): JSX.Element => (
|
||||
export const H4 = ({ className, children }: H4Props) => (
|
||||
<h4
|
||||
className={cn(
|
||||
"scroll-m-20 text-xl font-semibold tracking-tight",
|
||||
|
||||
@@ -5,7 +5,7 @@ export interface H5Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const H5 = ({ className, children }: H5Props): JSX.Element => (
|
||||
export const H5 = ({ className, children }: H5Props) => (
|
||||
<h5
|
||||
className={cn("scroll-m-20 text-lg font-medium tracking-tight", className)}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { cn } from "@app/core/utils/cn";
|
||||
import { cn } from "../../../core/utils/cn.ts";
|
||||
|
||||
export interface LinkProps {
|
||||
href: string;
|
||||
@@ -6,10 +6,10 @@ export interface LinkProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const Link = ({ href, children, className }: LinkProps): JSX.Element => (
|
||||
export const Link = ({ href, children, className }: LinkProps) => (
|
||||
<a
|
||||
href={href}
|
||||
target={"_blank"}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={cn(
|
||||
"font-medium text-slate-900 underline underline-offset-4 dark:text-slate-50",
|
||||
|
||||
@@ -2,6 +2,6 @@ export interface PProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const P = ({ children }: PProps): JSX.Element => (
|
||||
export const P = ({ children }: PProps) => (
|
||||
<p className="leading-7 not-first:mt-6">{children}</p>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ export interface SubtleProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const Subtle = ({ className, children }: SubtleProps): JSX.Element => (
|
||||
export const Subtle = ({ className, children }: SubtleProps) => (
|
||||
<p className={cn("text-sm text-slate-500 dark:text-slate-400", className)}>
|
||||
{children}
|
||||
</p>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const Blur = (): JSX.Element => {
|
||||
export const Blur = () => {
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 backdrop-blur-md backdrop-brightness-press"
|
||||
|
||||
@@ -2,7 +2,7 @@ export const Mono = ({
|
||||
children,
|
||||
className,
|
||||
...rest
|
||||
}: JSX.IntrinsicElements["span"]): JSX.Element => {
|
||||
}: JSX.IntrinsicElements["span"]) => {
|
||||
return (
|
||||
<span
|
||||
className={`font-mono text-sm text-text-secondary ${className ?? ""}`}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ChevronDownIcon, ChevronUpIcon } from "lucide-react";
|
||||
import React, { useState } from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
export interface TableProps {
|
||||
headings: Heading[];
|
||||
rows: JSX.Element[][];
|
||||
rows: [][];
|
||||
}
|
||||
|
||||
export interface Heading {
|
||||
@@ -12,7 +12,7 @@ export interface Heading {
|
||||
sortable: boolean;
|
||||
}
|
||||
|
||||
export const Table = ({ headings, rows }: TableProps): JSX.Element => {
|
||||
export const Table = ({ headings, rows }: TableProps) => {
|
||||
const [sortColumn, setSortColumn] = useState<string | null>("Last Heard");
|
||||
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
|
||||
|
||||
@@ -75,11 +75,9 @@ export const Table = ({ headings, rows }: TableProps): JSX.Element => {
|
||||
<div className="flex gap-2">
|
||||
{heading.title}
|
||||
{sortColumn === heading.title &&
|
||||
(sortOrder === "asc" ? (
|
||||
<ChevronUpIcon size={16} />
|
||||
) : (
|
||||
<ChevronDownIcon size={16} />
|
||||
))}
|
||||
(sortOrder === "asc"
|
||||
? <ChevronUpIcon size={16} />
|
||||
: <ChevronDownIcon size={16} />)}
|
||||
</div>
|
||||
</th>
|
||||
))}
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
import type React from "react";
|
||||
import { createContext, useContext, useEffect, useState } from "react";
|
||||
|
||||
type Theme = "light" | "dark" | "system";
|
||||
|
||||
interface ThemeContextType {
|
||||
theme: Theme;
|
||||
setTheme: (theme: Theme) => void;
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||
|
||||
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
||||
const [theme, setTheme] = useState<Theme>(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
const savedTheme = localStorage.getItem("theme") as Theme;
|
||||
return savedTheme || "system";
|
||||
}
|
||||
return "system";
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement;
|
||||
root.classList.remove("light", "dark");
|
||||
|
||||
if (theme === "system") {
|
||||
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
|
||||
.matches
|
||||
? "dark"
|
||||
: "light";
|
||||
root.classList.add(systemTheme);
|
||||
} else {
|
||||
root.classList.add(theme);
|
||||
}
|
||||
|
||||
localStorage.setItem("theme", theme);
|
||||
}, [theme]);
|
||||
|
||||
useEffect(() => {
|
||||
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
|
||||
const handleChange = () => {
|
||||
if (theme === "system") {
|
||||
const root = window.document.documentElement;
|
||||
root.classList.remove("light", "dark");
|
||||
root.classList.add(mediaQuery.matches ? "dark" : "light");
|
||||
}
|
||||
};
|
||||
|
||||
mediaQuery.addEventListener("change", handleChange);
|
||||
return () => mediaQuery.removeEventListener("change", handleChange);
|
||||
}, [theme]);
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, setTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useTheme() {
|
||||
const context = useContext(ThemeContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("useTheme must be used within a ThemeProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@radix-ui/react-tooltip";
|
||||
import type { JSX } from "react";
|
||||
|
||||
export interface TimeAgoProps {
|
||||
timestamp: number;
|
||||
@@ -43,7 +42,7 @@ const getTimeAgo = (
|
||||
return rtf.format(Math.floor(0 - diff), "second");
|
||||
};
|
||||
|
||||
export const TimeAgo = ({ timestamp }: TimeAgoProps): JSX.Element => {
|
||||
export const TimeAgo = ({ timestamp }: TimeAgoProps) => {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import type { JSX } from "react";
|
||||
|
||||
export interface UptimeProps {
|
||||
seconds: number;
|
||||
}
|
||||
@@ -12,6 +10,6 @@ const getUptime = (seconds: number): string => {
|
||||
return `${days}d ${hours}h ${minutes}m ${secondsLeft}s`;
|
||||
};
|
||||
|
||||
export const Uptime = ({ seconds }: UptimeProps): JSX.Element => {
|
||||
export const Uptime = ({ seconds }: UptimeProps) => {
|
||||
return <span>{getUptime(seconds)}</span>;
|
||||
};
|
||||
|
||||
@@ -14,8 +14,8 @@ export function useBrowserFeatureDetection(): BrowserSupport {
|
||||
["Web Serial", !!navigator?.serial],
|
||||
[
|
||||
"Secure Context",
|
||||
window.location.protocol === "https:" ||
|
||||
window.location.hostname === "localhost",
|
||||
globalThis.location.protocol === "https:" ||
|
||||
globalThis.location.hostname === "localhost",
|
||||
],
|
||||
];
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Button } from "@app/components/UI/Button";
|
||||
import { Button } from "../../components/UI/Button.tsx";
|
||||
import type { CookieAttributes } from "js-cookie";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import useCookie from "./useCookie";
|
||||
import { useToast } from "./useToast";
|
||||
import useCookie from "./useCookie.ts";
|
||||
import { useToast } from "./useToast.ts";
|
||||
|
||||
interface UseBackupReminderOptions {
|
||||
reminderInDays?: number;
|
||||
@@ -26,8 +26,8 @@ const ON_ACCEPT_REMINDER_DAYS = 365;
|
||||
function isReminderExpired(lastShown: string): boolean {
|
||||
const lastShownDate = new Date(lastShown);
|
||||
const now = new Date();
|
||||
const daysSinceLastShown =
|
||||
(now.getTime() - lastShownDate.getTime()) / (1000 * 60 * 60 * 24);
|
||||
const daysSinceLastShown = (now.getTime() - lastShownDate.getTime()) /
|
||||
(1000 * 60 * 60 * 24);
|
||||
return daysSinceLastShown >= 7;
|
||||
}
|
||||
|
||||
@@ -63,8 +63,7 @@ export function useBackupReminder({
|
||||
useEffect(() => {
|
||||
if (!enabled || toastShownRef.current) return;
|
||||
|
||||
const shouldShowReminder =
|
||||
!reminderCookie?.suppressed ||
|
||||
const shouldShowReminder = !reminderCookie?.suppressed ||
|
||||
isReminderExpired(reminderCookie.lastShown);
|
||||
if (!shouldShowReminder) return;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ type Theme = "light" | "dark" | "system";
|
||||
|
||||
export function useTheme() {
|
||||
const getSystemTheme = () =>
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
globalThis.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light";
|
||||
|
||||
@@ -14,7 +14,7 @@ export function useTheme() {
|
||||
);
|
||||
|
||||
const [preference, setPreference] = useState<Theme>(() =>
|
||||
typeof window !== "undefined" ? getStoredPreference() : "light",
|
||||
typeof window !== "undefined" ? getStoredPreference() : "light"
|
||||
);
|
||||
|
||||
const theme = preference === "system" ? getSystemTheme() : preference;
|
||||
@@ -26,7 +26,7 @@ export function useTheme() {
|
||||
useEffect(() => {
|
||||
if (preference !== "system") return;
|
||||
|
||||
const media = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
const media = globalThis.matchMedia("(prefers-color-scheme: dark)");
|
||||
const updateTheme = () => setPreference(getStoredPreference());
|
||||
|
||||
media.addEventListener("change", updateTheme);
|
||||
|
||||
+17
-17
@@ -31,21 +31,21 @@ type ActionType = typeof actionTypes;
|
||||
|
||||
type Action =
|
||||
| {
|
||||
type: ActionType["ADD_TOAST"];
|
||||
toast: ToasterToast;
|
||||
}
|
||||
type: ActionType["ADD_TOAST"];
|
||||
toast: ToasterToast;
|
||||
}
|
||||
| {
|
||||
type: ActionType["UPDATE_TOAST"];
|
||||
toast: Partial<ToasterToast>;
|
||||
}
|
||||
type: ActionType["UPDATE_TOAST"];
|
||||
toast: Partial<ToasterToast>;
|
||||
}
|
||||
| {
|
||||
type: ActionType["DISMISS_TOAST"];
|
||||
toastId?: ToasterToast["id"];
|
||||
}
|
||||
type: ActionType["DISMISS_TOAST"];
|
||||
toastId?: ToasterToast["id"];
|
||||
}
|
||||
| {
|
||||
type: ActionType["REMOVE_TOAST"];
|
||||
toastId?: ToasterToast["id"];
|
||||
};
|
||||
type: ActionType["REMOVE_TOAST"];
|
||||
toastId?: ToasterToast["id"];
|
||||
};
|
||||
|
||||
interface State {
|
||||
toasts: ToasterToast[];
|
||||
@@ -81,7 +81,7 @@ export const reducer = (state: State, action: Action): State => {
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === action.toast.id ? { ...t, ...action.toast } : t,
|
||||
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
||||
),
|
||||
};
|
||||
|
||||
@@ -103,10 +103,10 @@ export const reducer = (state: State, action: Action): State => {
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === toastId || toastId === undefined
|
||||
? {
|
||||
...t,
|
||||
open: false,
|
||||
}
|
||||
: t,
|
||||
...t,
|
||||
open: false,
|
||||
}
|
||||
: t
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ export const useAppStore = create<AppState>()((set, get) => ({
|
||||
);
|
||||
},
|
||||
setNodeNumToBeRemoved: (nodeNum) =>
|
||||
set((state) => ({
|
||||
set(() => ({
|
||||
nodeNumToBeRemoved: nodeNum,
|
||||
})),
|
||||
setConnectDialogOpen: (open: boolean) => {
|
||||
@@ -124,7 +124,7 @@ export const useAppStore = create<AppState>()((set, get) => ({
|
||||
},
|
||||
|
||||
setNodeNumDetails: (nodeNum) =>
|
||||
set((state) => ({
|
||||
set(() => ({
|
||||
nodeNumDetails: nodeNum,
|
||||
})),
|
||||
setActiveChat: (chat) =>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user