Merge branch 'master' into feature/node-quick-options
This commit is contained in:
@@ -5,9 +5,9 @@ import {
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@components/UI/Command.js";
|
||||
import { useAppStore } from "@core/stores/appStore.js";
|
||||
import { useDevice, useDeviceStore } from "@core/stores/deviceStore.js";
|
||||
} from "@components/UI/Command.tsx";
|
||||
import { useAppStore } from "@core/stores/appStore.ts";
|
||||
import { useDevice, useDeviceStore } from "@core/stores/deviceStore.ts";
|
||||
import { Hashicon } from "@emeraldpay/hashicon-react";
|
||||
import { useCommandState } from "cmdk";
|
||||
import {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { DeviceSelectorButton } from "@components/DeviceSelectorButton.js";
|
||||
import { Separator } from "@components/UI/Seperator.js";
|
||||
import { Code } from "@components/UI/Typography/Code.js";
|
||||
import { useAppStore } from "@core/stores/appStore.js";
|
||||
import { useDeviceStore } from "@core/stores/deviceStore.js";
|
||||
import { DeviceSelectorButton } from "@components/DeviceSelectorButton.tsx";
|
||||
import { Separator } from "@components/UI/Seperator.tsx";
|
||||
import { Code } from "@components/UI/Typography/Code.tsx";
|
||||
import { useAppStore } from "@core/stores/appStore.ts";
|
||||
import { useDeviceStore } from "@core/stores/deviceStore.ts";
|
||||
import { Hashicon } from "@emeraldpay/hashicon-react";
|
||||
import {
|
||||
HomeIcon,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useDevice } from "@app/core/stores/deviceStore.js";
|
||||
import { Button } from "@components/UI/Button.js";
|
||||
import { useDevice } from "@app/core/stores/deviceStore.ts";
|
||||
import { Button } from "@components/UI/Button.tsx";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -7,9 +7,9 @@ import {
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@components/UI/Dialog.js";
|
||||
import { Input } from "@components/UI/Input.js";
|
||||
import { Label } from "@components/UI/Label.js";
|
||||
} from "@components/UI/Dialog.tsx";
|
||||
import { Input } from "@components/UI/Input.tsx";
|
||||
import { Label } from "@components/UI/Label.tsx";
|
||||
import { Protobuf } from "@meshtastic/js";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { RemoveNodeDialog } from "@app/components/Dialog/RemoveNodeDialog.js";
|
||||
import { DeviceNameDialog } from "@components/Dialog/DeviceNameDialog.js";
|
||||
import { ImportDialog } from "@components/Dialog/ImportDialog.js";
|
||||
import { QRDialog } from "@components/Dialog/QRDialog.js";
|
||||
import { RebootDialog } from "@components/Dialog/RebootDialog.js";
|
||||
import { ShutdownDialog } from "@components/Dialog/ShutdownDialog.js";
|
||||
import { useDevice } from "@core/stores/deviceStore.js";
|
||||
import { RemoveNodeDialog } from "@app/components/Dialog/RemoveNodeDialog.tsx";
|
||||
import { DeviceNameDialog } from "@components/Dialog/DeviceNameDialog.tsx";
|
||||
import { ImportDialog } from "@components/Dialog/ImportDialog.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 { PkiBackupDialog } from "@components/Dialog/PKIBackupDialog";
|
||||
|
||||
export const DialogManager = (): JSX.Element => {
|
||||
const { channels, config, dialog, setDialogOpen } = useDevice();
|
||||
@@ -49,6 +50,12 @@ export const DialogManager = (): JSX.Element => {
|
||||
setDialogOpen("nodeRemoval", open);
|
||||
}}
|
||||
/>
|
||||
<PkiBackupDialog
|
||||
open={dialog.pkiBackup}
|
||||
onOpenChange={(open) => {
|
||||
setDialogOpen("pkiBackup", open);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Button } from "@components/UI/Button.js";
|
||||
import { Checkbox } from "@components/UI/Checkbox.js";
|
||||
import { Button } from "@components/UI/Button.tsx";
|
||||
import { Checkbox } from "@components/UI/Checkbox.tsx";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -7,11 +7,11 @@ import {
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@components/UI/Dialog.js";
|
||||
import { Input } from "@components/UI/Input.js";
|
||||
import { Label } from "@components/UI/Label.js";
|
||||
import { Switch } from "@components/UI/Switch.js";
|
||||
import { useDevice } from "@core/stores/deviceStore.js";
|
||||
} from "@components/UI/Dialog.tsx";
|
||||
import { Input } from "@components/UI/Input.tsx";
|
||||
import { Label } from "@components/UI/Label.tsx";
|
||||
import { Switch } from "@components/UI/Switch.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { Protobuf } from "@meshtastic/js";
|
||||
import { toByteArray } from "base64-js";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
import { BLE } from "@components/PageComponents/Connect/BLE.js";
|
||||
import { HTTP } from "@components/PageComponents/Connect/HTTP.js";
|
||||
import { Serial } from "@components/PageComponents/Connect/Serial.js";
|
||||
import {
|
||||
type BrowserFeature,
|
||||
useBrowserFeatureDetection,
|
||||
} from "@app/core/hooks/useBrowserFeatureDetection";
|
||||
import { BLE } from "@components/PageComponents/Connect/BLE.tsx";
|
||||
import { HTTP } from "@components/PageComponents/Connect/HTTP.tsx";
|
||||
import { Serial } from "@components/PageComponents/Connect/Serial.tsx";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@components/UI/Dialog.js";
|
||||
} from "@components/UI/Dialog.tsx";
|
||||
import { AlertCircle, InfoIcon, } from "lucide-react";
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@components/UI/Tabs.js";
|
||||
import { Link } from "@components/UI/Typography/Link.js";
|
||||
import { Subtle } from "@components/UI/Typography/Subtle.js";
|
||||
} from "@components/UI/Tabs.tsx";
|
||||
import { Subtle } from "@components/UI/Typography/Subtle.tsx";
|
||||
import { Link } from "../UI/Typography/Link";
|
||||
import { Fragment } from "react/jsx-runtime";
|
||||
|
||||
export interface TabElementProps {
|
||||
closeDialog: () => void;
|
||||
@@ -23,44 +29,114 @@ export interface TabElementProps {
|
||||
export interface TabManifest {
|
||||
label: string;
|
||||
element: React.FC<TabElementProps>;
|
||||
disabled: boolean;
|
||||
disabledMessage: string;
|
||||
disabledLink?: string;
|
||||
isDisabled: boolean;
|
||||
}
|
||||
|
||||
const tabs: TabManifest[] = [
|
||||
{
|
||||
label: "HTTP",
|
||||
element: HTTP,
|
||||
disabled: false,
|
||||
disabledMessage: "Unsuported connection method",
|
||||
},
|
||||
{
|
||||
label: "Bluetooth",
|
||||
element: BLE,
|
||||
disabled: !navigator.bluetooth,
|
||||
disabledMessage:
|
||||
"Web Bluetooth is currently only supported by Chromium-based browsers",
|
||||
disabledLink:
|
||||
"https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API#browser_compatibility",
|
||||
},
|
||||
{
|
||||
label: "Serial",
|
||||
element: Serial,
|
||||
disabled: !navigator.serial,
|
||||
disabledMessage:
|
||||
"WebSerial is currently only supported by Chromium based browsers: https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API#browser_compatibility",
|
||||
},
|
||||
];
|
||||
export interface NewDeviceProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
interface FeatureErrorProps {
|
||||
missingFeatures: BrowserFeature[];
|
||||
}
|
||||
|
||||
const links: { [key: string]: string } = {
|
||||
"Web Bluetooth":
|
||||
"https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API#browser_compatibility",
|
||||
"Web Serial":
|
||||
"https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API#browser_compatibility",
|
||||
"Secure Context":
|
||||
"https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts",
|
||||
};
|
||||
|
||||
const listFormatter = new Intl.ListFormat('en', {
|
||||
style: 'long',
|
||||
type: 'conjunction'
|
||||
});
|
||||
|
||||
const ErrorMessage = ({ missingFeatures }: FeatureErrorProps) => {
|
||||
if (missingFeatures.length === 0) return null;
|
||||
|
||||
const browserFeatures = missingFeatures.filter(feature => feature !== "Secure Context");
|
||||
const needsSecureContext = missingFeatures.includes("Secure Context");
|
||||
|
||||
const formatFeatureList = (features: string[]) => {
|
||||
const parts = listFormatter.formatToParts(features);
|
||||
return parts.map((part) => {
|
||||
if (part.type === 'element') {
|
||||
return (
|
||||
<Link
|
||||
key={part.value}
|
||||
href={links[part.value]}
|
||||
>
|
||||
{part.value}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
return <Fragment key={part.value}>{part.value}</Fragment>;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Subtle className="flex flex-col items-start gap-2 text-black bg-red-200/80 p-4 rounded-md">
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<AlertCircle size={40} className="mr-2 flex-shrink-0" />
|
||||
<div className="flex flex-col gap-3">
|
||||
<p className="text-sm">
|
||||
{browserFeatures.length > 0 && (
|
||||
<>
|
||||
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{" "}
|
||||
<Link
|
||||
href={links["Secure Context"]}
|
||||
>
|
||||
secure context
|
||||
</Link>
|
||||
. Please connect using HTTPS or localhost.
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Subtle>
|
||||
);
|
||||
};
|
||||
|
||||
export const NewDeviceDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: NewDeviceProps): JSX.Element => {
|
||||
const { unsupported } = useBrowserFeatureDetection();
|
||||
|
||||
const tabs: TabManifest[] = [
|
||||
{
|
||||
label: "HTTP",
|
||||
element: HTTP,
|
||||
isDisabled: false,
|
||||
},
|
||||
{
|
||||
label: "Bluetooth",
|
||||
element: BLE,
|
||||
isDisabled:
|
||||
unsupported.includes("Web Bluetooth") ||
|
||||
unsupported.includes("Secure Context"),
|
||||
},
|
||||
{
|
||||
label: "Serial",
|
||||
element: Serial,
|
||||
isDisabled:
|
||||
unsupported.includes("Web Serial") ||
|
||||
unsupported.includes("Secure Context"),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
@@ -73,7 +149,6 @@ export const NewDeviceDialog = ({
|
||||
<TabsTrigger
|
||||
key={tab.label}
|
||||
value={tab.label}
|
||||
disabled={tab.disabled}
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTrigger>
|
||||
@@ -81,35 +156,13 @@ export const NewDeviceDialog = ({
|
||||
</TabsList>
|
||||
{tabs.map((tab) => (
|
||||
<TabsContent key={tab.label} value={tab.label}>
|
||||
{tab.disabled ? (
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">
|
||||
{tab.disabledMessage}
|
||||
</p>
|
||||
) : (
|
||||
<fieldset disabled={tab.isDisabled}>
|
||||
{tab.isDisabled ? <ErrorMessage missingFeatures={unsupported} /> : null}
|
||||
<tab.element closeDialog={() => onOpenChange(false)} />
|
||||
)}
|
||||
</fieldset>
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
|
||||
{(!navigator.bluetooth || !navigator.serial) && (
|
||||
<>
|
||||
<Subtle>
|
||||
Web Bluetooth and Web Serial are currently only supported by
|
||||
Chromium-based browsers.
|
||||
</Subtle>
|
||||
<Subtle>
|
||||
Read more:
|
||||
<Link href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API#browser_compatibility">
|
||||
Web Bluetooth
|
||||
</Link>
|
||||
|
||||
<Link href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API#browser_compatibility">
|
||||
Web Serial
|
||||
</Link>
|
||||
</Subtle>
|
||||
</>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import { useDevice } from "@app/core/stores/deviceStore";
|
||||
import { Button } from "@components/UI/Button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@components/UI/Dialog.tsx";
|
||||
import { fromByteArray } from "base64-js";
|
||||
import { DownloadIcon, PrinterIcon } from "lucide-react";
|
||||
import React from "react";
|
||||
|
||||
export interface PkiBackupDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const PkiBackupDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: PkiBackupDialogProps) => {
|
||||
const { config, setDialogOpen } = useDevice();
|
||||
const privateKey = config.security?.privateKey;
|
||||
const publicKey = config.security?.publicKey;
|
||||
|
||||
const decodeKeyData = React.useCallback(
|
||||
(key: Uint8Array<ArrayBufferLike>) => {
|
||||
if (!key) return "";
|
||||
return fromByteArray(key ?? new Uint8Array(0));
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const closeDialog = React.useCallback(() => {
|
||||
setDialogOpen("pkiBackup", false);
|
||||
}, [setDialogOpen]);
|
||||
|
||||
const renderPrintWindow = React.useCallback(() => {
|
||||
if (!privateKey || !publicKey) return;
|
||||
|
||||
const printWindow = window.open("", "_blank");
|
||||
if (printWindow) {
|
||||
printWindow.document.write(`
|
||||
<html>
|
||||
<head>
|
||||
<title>=== MESHTASTIC KEYS ===</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; padding: 20px; }
|
||||
h1 { font-size: 18px; }
|
||||
p { font-size: 14px; word-break: break-all; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>=== MESHTASTIC KEYS ===</h1>
|
||||
<br>
|
||||
<h2>Public Key:</h2>
|
||||
<p>${decodeKeyData(publicKey)}</p>
|
||||
<h2>Private Key:</h2>
|
||||
<p>${decodeKeyData(privateKey)}</p>
|
||||
<br>
|
||||
<p>=== END OF KEYS ===</p>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
printWindow.document.close();
|
||||
printWindow.print();
|
||||
closeDialog();
|
||||
}
|
||||
}, [decodeKeyData, privateKey, publicKey, closeDialog]);
|
||||
|
||||
const createDownloadKeyFile = React.useCallback(() => {
|
||||
if (!privateKey || !publicKey) return;
|
||||
|
||||
const decodedPrivateKey = decodeKeyData(privateKey);
|
||||
const decodedPublicKey = decodeKeyData(publicKey);
|
||||
|
||||
const formattedContent = [
|
||||
"=== MESHTASTIC KEYS ===\n\n",
|
||||
"Private Key:\n",
|
||||
decodedPrivateKey,
|
||||
"\n\nPublic Key:\n",
|
||||
decodedPublicKey,
|
||||
"\n\n=== END OF KEYS ===",
|
||||
].join("");
|
||||
|
||||
const blob = new Blob([formattedContent], { type: "text/plain" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = "meshtastic_keys.txt";
|
||||
link.style.display = "none";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
closeDialog();
|
||||
URL.revokeObjectURL(url);
|
||||
}, [decodeKeyData, privateKey, publicKey, closeDialog]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Backup Keys</DialogTitle>
|
||||
<DialogDescription>
|
||||
Its important to backup your public and private keys and store your
|
||||
backup securely!
|
||||
</DialogDescription>
|
||||
<DialogDescription>
|
||||
<span className="font-bold break-before-auto">
|
||||
If you lose your keys, you will need to reset your device.
|
||||
</span>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter className="mt-6">
|
||||
<Button
|
||||
variant={"default"}
|
||||
onClick={() => createDownloadKeyFile()}
|
||||
className=""
|
||||
>
|
||||
<DownloadIcon size={20} className="mr-2" />
|
||||
Download
|
||||
</Button>
|
||||
<Button variant={"default"} onClick={() => renderPrintWindow()}>
|
||||
<PrinterIcon size={20} className="mr-2" />
|
||||
Print
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button } from "@components/UI/Button.js";
|
||||
import { Button } from "@components/UI/Button.tsx";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@components/UI/Dialog.js";
|
||||
} from "@components/UI/Dialog.tsx";
|
||||
|
||||
export interface PkiRegenerateDialogProps {
|
||||
open: boolean;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Checkbox } from "@components/UI/Checkbox.js";
|
||||
import { Checkbox } from "@components/UI/Checkbox.tsx";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -6,9 +6,9 @@ import {
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@components/UI/Dialog.js";
|
||||
import { Input } from "@components/UI/Input.js";
|
||||
import { Label } from "@components/UI/Label.js";
|
||||
} from "@components/UI/Dialog.tsx";
|
||||
import { Input } from "@components/UI/Input.tsx";
|
||||
import { Label } from "@components/UI/Label.tsx";
|
||||
import { Protobuf, type Types } from "@meshtastic/js";
|
||||
import { fromByteArray } from "base64-js";
|
||||
import { ClipboardIcon } from "lucide-react";
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Button } from "@components/UI/Button.js";
|
||||
import { Button } from "@components/UI/Button.tsx";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@components/UI/Dialog.js";
|
||||
import { Input } from "@components/UI/Input.js";
|
||||
import { useDevice } from "@core/stores/deviceStore.js";
|
||||
} from "@components/UI/Dialog.tsx";
|
||||
import { Input } from "@components/UI/Input.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { ClockIcon, RefreshCwIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useAppStore } from "@app/core/stores/appStore";
|
||||
import { useDevice } from "@app/core/stores/deviceStore.js";
|
||||
import { Button } from "@components/UI/Button.js";
|
||||
import { useDevice } from "@app/core/stores/deviceStore.ts";
|
||||
import { Button } from "@components/UI/Button.tsx";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -8,8 +8,8 @@ import {
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@components/UI/Dialog.js";
|
||||
import { Label } from "@components/UI/Label.js";
|
||||
} from "@components/UI/Dialog.tsx";
|
||||
import { Label } from "@components/UI/Label.tsx";
|
||||
|
||||
export interface RemoveNodeDialogProps {
|
||||
open: boolean;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Button } from "@components/UI/Button.js";
|
||||
import { Button } from "@components/UI/Button.tsx";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@components/UI/Dialog.js";
|
||||
import { Input } from "@components/UI/Input.js";
|
||||
import { useDevice } from "@core/stores/deviceStore.js";
|
||||
} from "@components/UI/Dialog.tsx";
|
||||
import { Input } from "@components/UI/Input.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { ClockIcon, PowerIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import {
|
||||
DynamicFormField,
|
||||
type FieldProps,
|
||||
} from "@components/Form/DynamicFormField.js";
|
||||
import { FieldWrapper } from "@components/Form/FormWrapper.js";
|
||||
import { Button } from "@components/UI/Button.js";
|
||||
import { H4 } from "@components/UI/Typography/H4.js";
|
||||
import { Subtle } from "@components/UI/Typography/Subtle.js";
|
||||
} from "@components/Form/DynamicFormField.tsx";
|
||||
import { FieldWrapper } from "@components/Form/FormWrapper.tsx";
|
||||
import { Button } from "@components/UI/Button.tsx";
|
||||
import { H4 } from "@components/UI/Typography/H4.tsx";
|
||||
import { Subtle } from "@components/UI/Typography/Subtle.tsx";
|
||||
import {
|
||||
type Control,
|
||||
type DefaultValues,
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import {
|
||||
GenericInput,
|
||||
type InputFieldProps,
|
||||
} from "@components/Form/FormInput.js";
|
||||
} from "@components/Form/FormInput.tsx";
|
||||
import {
|
||||
PasswordGenerator,
|
||||
type PasswordGeneratorProps,
|
||||
} from "@components/Form/FormPasswordGenerator.js";
|
||||
} from "@components/Form/FormPasswordGenerator.tsx";
|
||||
import {
|
||||
type SelectFieldProps,
|
||||
SelectInput,
|
||||
} from "@components/Form/FormSelect.js";
|
||||
} from "@components/Form/FormSelect.tsx";
|
||||
import {
|
||||
type ToggleFieldProps,
|
||||
ToggleInput,
|
||||
} from "@components/Form/FormToggle.js";
|
||||
} from "@components/Form/FormToggle.tsx";
|
||||
import type { Control, FieldValues } from "react-hook-form";
|
||||
|
||||
export type FieldProps<T> =
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type {
|
||||
BaseFormBuilderProps,
|
||||
GenericFormElementProps,
|
||||
} from "@components/Form/DynamicForm.js";
|
||||
import { Input } from "@components/UI/Input.js";
|
||||
} from "@components/Form/DynamicForm.tsx";
|
||||
import { Input } from "@components/UI/Input.tsx";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import { Eye, EyeOff } from "lucide-react";
|
||||
import type { ChangeEventHandler } from "react";
|
||||
import { useState } from "react";
|
||||
import { Controller, type FieldValues } from "react-hook-form";
|
||||
|
||||
export interface InputFieldProps<T> extends BaseFormBuilderProps<T> {
|
||||
@@ -27,13 +29,28 @@ export function GenericInput<T extends FieldValues>({
|
||||
disabled,
|
||||
field,
|
||||
}: GenericFormElementProps<T, InputFieldProps<T>>) {
|
||||
const [passwordShown, setPasswordShown] = useState(false);
|
||||
const togglePasswordVisiblity = () => {
|
||||
setPasswordShown(!passwordShown);
|
||||
};
|
||||
|
||||
return (
|
||||
<Controller
|
||||
name={field.name}
|
||||
control={control}
|
||||
render={({ field: { value, onChange, ...rest } }) => (
|
||||
<Input
|
||||
type={field.type}
|
||||
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}
|
||||
onChange={(e) => {
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import type {
|
||||
BaseFormBuilderProps,
|
||||
GenericFormElementProps,
|
||||
} from "@components/Form/DynamicForm.js";
|
||||
import { Generator } from "@components/UI/Generator.js";
|
||||
} from "@components/Form/DynamicForm.tsx";
|
||||
import { Generator } from "@components/UI/Generator.tsx";
|
||||
import { Eye, EyeOff } from "lucide-react";
|
||||
import type { ChangeEventHandler, MouseEventHandler } from "react";
|
||||
import { useState } from "react";
|
||||
import { Controller, type FieldValues } from "react-hook-form";
|
||||
import type { ButtonVariant } from "@components/UI/Button";
|
||||
|
||||
export interface PasswordGeneratorProps<T> extends BaseFormBuilderProps<T> {
|
||||
type: "passwordGenerator";
|
||||
@@ -13,7 +16,12 @@ export interface PasswordGeneratorProps<T> extends BaseFormBuilderProps<T> {
|
||||
devicePSKBitCount: number;
|
||||
inputChange: ChangeEventHandler;
|
||||
selectChange: (event: string) => void;
|
||||
buttonClick: MouseEventHandler;
|
||||
actionButtons: {
|
||||
text: string;
|
||||
onClick: React.MouseEventHandler<HTMLButtonElement>;
|
||||
variant: ButtonVariant;
|
||||
className?: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export function PasswordGenerator<T extends FieldValues>({
|
||||
@@ -21,21 +29,33 @@ export function PasswordGenerator<T extends FieldValues>({
|
||||
field,
|
||||
disabled,
|
||||
}: GenericFormElementProps<T, PasswordGeneratorProps<T>>) {
|
||||
const [passwordShown, setPasswordShown] = useState(false);
|
||||
const togglePasswordVisiblity = () => {
|
||||
setPasswordShown(!passwordShown);
|
||||
};
|
||||
|
||||
return (
|
||||
<Controller
|
||||
name={field.name}
|
||||
control={control}
|
||||
render={({ field: { value, ...rest } }) => (
|
||||
<Generator
|
||||
hide={field.hide}
|
||||
type={field.hide && !passwordShown ? "password" : "text"}
|
||||
action={
|
||||
field.hide
|
||||
? {
|
||||
icon: passwordShown ? EyeOff : Eye,
|
||||
onClick: togglePasswordVisiblity,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
devicePSKBitCount={field.devicePSKBitCount}
|
||||
bits={field.bits}
|
||||
inputChange={field.inputChange}
|
||||
selectChange={field.selectChange}
|
||||
buttonClick={field.buttonClick}
|
||||
value={value}
|
||||
variant={field.validationText ? "invalid" : "default"}
|
||||
buttonText="Generate"
|
||||
actionButtons={field.actionButtons}
|
||||
{...field.properties}
|
||||
{...rest}
|
||||
disabled={disabled}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import type {
|
||||
BaseFormBuilderProps,
|
||||
GenericFormElementProps,
|
||||
} from "@components/Form/DynamicForm.js";
|
||||
} from "@components/Form/DynamicForm.tsx";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@components/UI/Select.js";
|
||||
} from "@components/UI/Select.tsx";
|
||||
import { Controller, type FieldValues } from "react-hook-form";
|
||||
|
||||
export interface SelectFieldProps<T> extends BaseFormBuilderProps<T> {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type {
|
||||
BaseFormBuilderProps,
|
||||
GenericFormElementProps,
|
||||
} from "@components/Form/DynamicForm.js";
|
||||
import { Switch } from "@components/UI/Switch.js";
|
||||
} from "@components/Form/DynamicForm.tsx";
|
||||
import { Switch } from "@components/UI/Switch.tsx";
|
||||
import type { ChangeEvent } from "react";
|
||||
import { Controller, type FieldValues } from "react-hook-form";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Label } from "@components/UI/Label.js";
|
||||
import { Label } from "@components/UI/Label.tsx";
|
||||
|
||||
export interface FieldWrapperProps {
|
||||
label: string;
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { useBackupReminder } from "@app/core/hooks/useKeyBackupReminder";
|
||||
import { useDevice } from "@app/core/stores/deviceStore";
|
||||
|
||||
export const KeyBackupReminder = (): JSX.Element => {
|
||||
const { setDialogOpen } = useDevice();
|
||||
|
||||
useBackupReminder({
|
||||
reminderInDays: 7,
|
||||
message:
|
||||
"We recommend backing up your key data regularly. Would you like to back up now?",
|
||||
onAccept: () => setDialogOpen("pkiBackup", true),
|
||||
enabled: true,
|
||||
cookieOptions: {
|
||||
secure: true,
|
||||
sameSite: "strict",
|
||||
},
|
||||
});
|
||||
return <></>;
|
||||
};
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { ChannelValidation } from "@app/validation/channel.js";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.js";
|
||||
import { useToast } from "@core/hooks/useToast.js";
|
||||
import { useDevice } from "@core/stores/deviceStore.js";
|
||||
import type { ChannelValidation } from "@app/validation/channel.tsx";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||
import { useToast } from "@core/hooks/useToast.ts";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { Protobuf } from "@meshtastic/js";
|
||||
import { fromByteArray, toByteArray } from "base64-js";
|
||||
import cryptoRandomString from "crypto-random-string";
|
||||
import { useState } from "react";
|
||||
import { PkiRegenerateDialog } from "../Dialog/PkiRegenerateDialog";
|
||||
|
||||
export interface SettingsPanelProps {
|
||||
channel: Protobuf.Channel.Channel;
|
||||
@@ -22,6 +23,7 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
|
||||
channel?.settings?.psk.length ?? 16,
|
||||
);
|
||||
const [validationText, setValidationText] = useState<string>();
|
||||
const [preSharedDialogOpen, setPreSharedDialogOpen] = useState<boolean>(false);
|
||||
|
||||
const onSubmit = (data: ChannelValidation) => {
|
||||
const channel = new Protobuf.Channel.Channel({
|
||||
@@ -46,7 +48,7 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
|
||||
});
|
||||
};
|
||||
|
||||
const clickEvent = () => {
|
||||
const preSharedKeyRegenerate = () => {
|
||||
setPass(
|
||||
btoa(
|
||||
cryptoRandomString({
|
||||
@@ -56,6 +58,11 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
|
||||
),
|
||||
);
|
||||
setValidationText(undefined);
|
||||
setPreSharedDialogOpen(false);
|
||||
};
|
||||
|
||||
const preSharedClickEvent = () => {
|
||||
setPreSharedDialogOpen(true);
|
||||
};
|
||||
|
||||
const validatePass = (input: string, count: number) => {
|
||||
@@ -79,103 +86,105 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
|
||||
};
|
||||
|
||||
return (
|
||||
<DynamicForm<ChannelValidation>
|
||||
onSubmit={onSubmit}
|
||||
submitType="onSubmit"
|
||||
hasSubmitButton={true}
|
||||
defaultValues={{
|
||||
...channel,
|
||||
...{
|
||||
settings: {
|
||||
...channel?.settings,
|
||||
psk: pass,
|
||||
positionEnabled:
|
||||
channel?.settings?.moduleSettings?.positionPrecision !==
|
||||
<>
|
||||
<DynamicForm<ChannelValidation>
|
||||
onSubmit={onSubmit}
|
||||
submitType="onSubmit"
|
||||
hasSubmitButton={true}
|
||||
defaultValues={{
|
||||
...channel,
|
||||
...{
|
||||
settings: {
|
||||
...channel?.settings,
|
||||
psk: pass,
|
||||
positionEnabled:
|
||||
channel?.settings?.moduleSettings?.positionPrecision !==
|
||||
undefined &&
|
||||
channel?.settings?.moduleSettings?.positionPrecision > 0,
|
||||
preciseLocation:
|
||||
channel?.settings?.moduleSettings?.positionPrecision === 32,
|
||||
positionPrecision:
|
||||
channel?.settings?.moduleSettings?.positionPrecision === undefined
|
||||
? 10
|
||||
: channel?.settings?.moduleSettings?.positionPrecision,
|
||||
channel?.settings?.moduleSettings?.positionPrecision > 0,
|
||||
preciseLocation:
|
||||
channel?.settings?.moduleSettings?.positionPrecision === 32,
|
||||
positionPrecision:
|
||||
channel?.settings?.moduleSettings?.positionPrecision === undefined
|
||||
? 10
|
||||
: channel?.settings?.moduleSettings?.positionPrecision,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
fieldGroups={[
|
||||
{
|
||||
label: "Channel Settings",
|
||||
description: "Crypto, MQTT & misc settings",
|
||||
fields: [
|
||||
{
|
||||
type: "select",
|
||||
name: "role",
|
||||
label: "Role",
|
||||
disabled: channel.index === 0,
|
||||
description:
|
||||
"Device telemetry is sent over PRIMARY. Only one PRIMARY allowed",
|
||||
properties: {
|
||||
enumValue:
|
||||
channel.index === 0
|
||||
? { PRIMARY: 1 }
|
||||
: { DISABLED: 0, SECONDARY: 2 },
|
||||
}}
|
||||
fieldGroups={[
|
||||
{
|
||||
label: "Channel Settings",
|
||||
description: "Crypto, MQTT & misc settings",
|
||||
fields: [
|
||||
{
|
||||
type: "select",
|
||||
name: "role",
|
||||
label: "Role",
|
||||
disabled: channel.index === 0,
|
||||
description:
|
||||
"Device telemetry is sent over PRIMARY. Only one PRIMARY allowed",
|
||||
properties: {
|
||||
enumValue:
|
||||
channel.index === 0
|
||||
? { PRIMARY: 1 }
|
||||
: { DISABLED: 0, SECONDARY: 2 },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "passwordGenerator",
|
||||
name: "settings.psk",
|
||||
label: "pre-Shared Key",
|
||||
description: "256, 128, or 8 bit PSKs allowed",
|
||||
validationText: validationText,
|
||||
devicePSKBitCount: bitCount ?? 0,
|
||||
inputChange: inputChangeEvent,
|
||||
selectChange: selectChangeEvent,
|
||||
buttonClick: clickEvent,
|
||||
properties: {
|
||||
value: pass,
|
||||
{
|
||||
type: "passwordGenerator",
|
||||
name: "settings.psk",
|
||||
label: "Pre-Shared Key",
|
||||
description: "Supported PSK lengths: 256-bit, 128-bit, 8-bit, Empty (0-bit)",
|
||||
validationText: validationText,
|
||||
devicePSKBitCount: bitCount ?? 0,
|
||||
inputChange: inputChangeEvent,
|
||||
selectChange: selectChangeEvent,
|
||||
actionButtons: [{ text: 'Generate', variant: 'success', onClick: preSharedClickEvent }],
|
||||
hide: true,
|
||||
properties: {
|
||||
value: pass,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
name: "settings.name",
|
||||
label: "Name",
|
||||
description:
|
||||
"A unique name for the channel <12 bytes, leave blank for default",
|
||||
},
|
||||
{
|
||||
type: "toggle",
|
||||
name: "settings.uplinkEnabled",
|
||||
label: "Uplink Enabled",
|
||||
description: "Send messages from the local mesh to MQTT",
|
||||
},
|
||||
{
|
||||
type: "toggle",
|
||||
name: "settings.downlinkEnabled",
|
||||
label: "Downlink Enabled",
|
||||
description: "Send messages from MQTT to the local mesh",
|
||||
},
|
||||
{
|
||||
type: "toggle",
|
||||
name: "settings.positionEnabled",
|
||||
label: "Allow Position Requests",
|
||||
description: "Send position to channel",
|
||||
},
|
||||
{
|
||||
type: "toggle",
|
||||
name: "settings.preciseLocation",
|
||||
label: "Precise Location",
|
||||
description: "Send precise location to channel",
|
||||
},
|
||||
{
|
||||
type: "select",
|
||||
name: "settings.positionPrecision",
|
||||
label: "Approximate Location",
|
||||
description:
|
||||
"If not sharing precise location, position shared on channel will be accurate within this distance",
|
||||
properties: {
|
||||
enumValue:
|
||||
config.display?.units === 0
|
||||
? {
|
||||
{
|
||||
type: "text",
|
||||
name: "settings.name",
|
||||
label: "Name",
|
||||
description:
|
||||
"A unique name for the channel <12 bytes, leave blank for default",
|
||||
},
|
||||
{
|
||||
type: "toggle",
|
||||
name: "settings.uplinkEnabled",
|
||||
label: "Uplink Enabled",
|
||||
description: "Send messages from the local mesh to MQTT",
|
||||
},
|
||||
{
|
||||
type: "toggle",
|
||||
name: "settings.downlinkEnabled",
|
||||
label: "Downlink Enabled",
|
||||
description: "Send messages from MQTT to the local mesh",
|
||||
},
|
||||
{
|
||||
type: "toggle",
|
||||
name: "settings.positionEnabled",
|
||||
label: "Allow Position Requests",
|
||||
description: "Send position to channel",
|
||||
},
|
||||
{
|
||||
type: "toggle",
|
||||
name: "settings.preciseLocation",
|
||||
label: "Precise Location",
|
||||
description: "Send precise location to channel",
|
||||
},
|
||||
{
|
||||
type: "select",
|
||||
name: "settings.positionPrecision",
|
||||
label: "Approximate Location",
|
||||
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,
|
||||
@@ -187,7 +196,7 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
|
||||
"Within 90 m": 18,
|
||||
"Within 50 m": 19,
|
||||
}
|
||||
: {
|
||||
: {
|
||||
"Within 15 miles": 10,
|
||||
"Within 7.3 miles": 11,
|
||||
"Within 3.6 miles": 12,
|
||||
@@ -199,11 +208,17 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
|
||||
"Within 300 feet": 18,
|
||||
"Within 150 feet": 19,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<PkiRegenerateDialog
|
||||
open={preSharedDialogOpen}
|
||||
onOpenChange={() => setPreSharedDialogOpen(false)}
|
||||
onSubmit={() => preSharedKeyRegenerate()}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,25 @@
|
||||
import type { BluetoothValidation } from "@app/validation/config/bluetooth.js";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.js";
|
||||
import { useDevice } from "@core/stores/deviceStore.js";
|
||||
import type { BluetoothValidation } from "@app/validation/config/bluetooth.tsx";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { Protobuf } from "@meshtastic/js";
|
||||
import { useState } from "react";
|
||||
|
||||
export const Bluetooth = (): JSX.Element => {
|
||||
const { config, setWorkingConfig } = useDevice();
|
||||
const [bluetoothValidationText, setBluetoothValidationText] = useState<string>();
|
||||
|
||||
const bluetoothPinChangeEvent = (
|
||||
e: React.ChangeEvent<HTMLInputElement>,
|
||||
) => {
|
||||
if (e.target.value[0] == "0")
|
||||
{
|
||||
setBluetoothValidationText("Bluetooth Pin cannot start with 0.");
|
||||
}
|
||||
else
|
||||
{
|
||||
setBluetoothValidationText("");
|
||||
}
|
||||
}
|
||||
|
||||
const onSubmit = (data: BluetoothValidation) => {
|
||||
setWorkingConfig(
|
||||
@@ -52,6 +67,8 @@ export const Bluetooth = (): JSX.Element => {
|
||||
name: "fixedPin",
|
||||
label: "Pin",
|
||||
description: "Pin to use when pairing",
|
||||
validationText: bluetoothValidationText,
|
||||
inputChange: bluetoothPinChangeEvent,
|
||||
disabledBy: [
|
||||
{
|
||||
fieldName: "mode",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { DeviceValidation } from "@app/validation/config/device.js";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.js";
|
||||
import { useDevice } from "@core/stores/deviceStore.js";
|
||||
import type { DeviceValidation } from "@app/validation/config/device.tsx";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { Protobuf } from "@meshtastic/js";
|
||||
|
||||
export const Device = (): JSX.Element => {
|
||||
@@ -32,7 +32,22 @@ export const Device = (): JSX.Element => {
|
||||
label: "Role",
|
||||
description: "What role the device performs on the mesh",
|
||||
properties: {
|
||||
enumValue: Protobuf.Config.Config_DeviceConfig_Role,
|
||||
enumValue: {
|
||||
Client: Protobuf.Config.Config_DeviceConfig_Role.CLIENT,
|
||||
"Client Mute":
|
||||
Protobuf.Config.Config_DeviceConfig_Role.CLIENT_MUTE,
|
||||
Router: Protobuf.Config.Config_DeviceConfig_Role.ROUTER,
|
||||
Repeater: Protobuf.Config.Config_DeviceConfig_Role.REPEATER,
|
||||
Tracker: Protobuf.Config.Config_DeviceConfig_Role.TRACKER,
|
||||
Sensor: Protobuf.Config.Config_DeviceConfig_Role.SENSOR,
|
||||
TAK: Protobuf.Config.Config_DeviceConfig_Role.TAK,
|
||||
"Client Hidden":
|
||||
Protobuf.Config.Config_DeviceConfig_Role.CLIENT_HIDDEN,
|
||||
"Lost and Found":
|
||||
Protobuf.Config.Config_DeviceConfig_Role.LOST_AND_FOUND,
|
||||
"TAK Tracker":
|
||||
Protobuf.Config.Config_DeviceConfig_Role.SENSOR,
|
||||
},
|
||||
formatEnumName: true,
|
||||
},
|
||||
},
|
||||
@@ -79,6 +94,12 @@ export const Device = (): JSX.Element => {
|
||||
label: "Disable Triple Click",
|
||||
description: "Disable triple click",
|
||||
},
|
||||
{
|
||||
type: "toggle",
|
||||
name: "ledHeartbeatDisabled",
|
||||
label: "LED Heartbeat Disabled",
|
||||
description: "Disable default blinking LED",
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { DisplayValidation } from "@app/validation/config/display.js";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.js";
|
||||
import { useDevice } from "@core/stores/deviceStore.js";
|
||||
import type { DisplayValidation } from "@app/validation/config/display.tsx";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { Protobuf } from "@meshtastic/js";
|
||||
|
||||
export const Display = (): JSX.Element => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { LoRaValidation } from "@app/validation/config/lora.js";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.js";
|
||||
import { useDevice } from "@core/stores/deviceStore.js";
|
||||
import type { LoRaValidation } from "@app/validation/config/lora.tsx";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { Protobuf } from "@meshtastic/js";
|
||||
|
||||
export const LoRa = (): JSX.Element => {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { NetworkValidation } from "@app/validation/config/network.js";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.js";
|
||||
import { useDevice } from "@core/stores/deviceStore.js";
|
||||
import type { NetworkValidation } from "@app/validation/config/network.tsx";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import {
|
||||
convertIntToIpAddress,
|
||||
convertIpAddressToInt,
|
||||
} from "@core/utils/ip.js";
|
||||
} from "@core/utils/ip.ts";
|
||||
import { Protobuf } from "@meshtastic/js";
|
||||
|
||||
export const Network = (): JSX.Element => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { PositionValidation } from "@app/validation/config/position.js";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.js";
|
||||
import { useDevice } from "@core/stores/deviceStore.js";
|
||||
import type { PositionValidation } from "@app/validation/config/position.tsx";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { Protobuf } from "@meshtastic/js";
|
||||
|
||||
export const Position = (): JSX.Element => {
|
||||
@@ -77,12 +77,6 @@ export const Position = (): JSX.Element => {
|
||||
label: "Enable Pin",
|
||||
description: "GPS module enable pin override",
|
||||
},
|
||||
{
|
||||
type: "number",
|
||||
name: "channelPrecision",
|
||||
label: "Channel Precision",
|
||||
description: "GPS channel precision",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { PowerValidation } from "@app/validation/config/power.js";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.js";
|
||||
import { useDevice } from "@core/stores/deviceStore.js";
|
||||
import type { PowerValidation } from "@app/validation/config/power.tsx";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { Protobuf } from "@meshtastic/js";
|
||||
|
||||
export const Power = (): JSX.Element => {
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { PkiRegenerateDialog } from "@app/components/Dialog/PkiRegenerateDialog";
|
||||
import { DynamicForm } from "@app/components/Form/DynamicForm.js";
|
||||
import { DynamicForm } from "@app/components/Form/DynamicForm.tsx";
|
||||
import {
|
||||
getX25519PrivateKey,
|
||||
getX25519PublicKey,
|
||||
} from "@app/core/utils/x25519";
|
||||
import type { SecurityValidation } from "@app/validation/config/security.js";
|
||||
import { useDevice } from "@core/stores/deviceStore.js";
|
||||
import type { SecurityValidation } from "@app/validation/config/security.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { Protobuf } from "@meshtastic/js";
|
||||
import { fromByteArray, toByteArray } from "base64-js";
|
||||
import { Eye, EyeOff } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
export const Security = (): JSX.Element => {
|
||||
const { config, nodes, hardware, setWorkingConfig } = useDevice();
|
||||
const { config, nodes, hardware, setWorkingConfig, setDialogOpen } = useDevice();
|
||||
|
||||
const [privateKey, setPrivateKey] = useState<string>(
|
||||
fromByteArray(config.security?.privateKey ?? new Uint8Array(0)),
|
||||
@@ -31,7 +31,7 @@ export const Security = (): JSX.Element => {
|
||||
);
|
||||
const [adminKeyValidationText, setAdminKeyValidationText] =
|
||||
useState<string>();
|
||||
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||
const [privateKeyDialogOpen, setPrivateKeyDialogOpen] = useState<boolean>(false);
|
||||
|
||||
const onSubmit = (data: SecurityValidation) => {
|
||||
if (privateKeyValidationText || adminKeyValidationText) return;
|
||||
@@ -71,9 +71,13 @@ export const Security = (): JSX.Element => {
|
||||
};
|
||||
|
||||
const privateKeyClickEvent = () => {
|
||||
setDialogOpen(true);
|
||||
setPrivateKeyDialogOpen(true);
|
||||
};
|
||||
|
||||
const pkiBackupClickEvent = () => {
|
||||
setDialogOpen("pkiBackup", true);
|
||||
}
|
||||
|
||||
const pkiRegenerate = () => {
|
||||
const privateKey = getX25519PrivateKey();
|
||||
const publicKey = getX25519PublicKey(privateKey);
|
||||
@@ -86,7 +90,7 @@ export const Security = (): JSX.Element => {
|
||||
setPrivateKeyValidationText,
|
||||
);
|
||||
|
||||
setDialogOpen(false);
|
||||
setPrivateKeyDialogOpen(false);
|
||||
};
|
||||
|
||||
const privateKeyInputChangeEvent = (
|
||||
@@ -149,7 +153,18 @@ export const Security = (): JSX.Element => {
|
||||
inputChange: privateKeyInputChangeEvent,
|
||||
selectChange: privateKeySelectChangeEvent,
|
||||
hide: !privateKeyVisible,
|
||||
buttonClick: privateKeyClickEvent,
|
||||
actionButtons: [
|
||||
{
|
||||
text: "Generate",
|
||||
onClick: privateKeyClickEvent,
|
||||
variant: "success",
|
||||
},
|
||||
{
|
||||
text: "Backup Key",
|
||||
onClick: pkiBackupClickEvent,
|
||||
variant: "subtle",
|
||||
},
|
||||
],
|
||||
properties: {
|
||||
value: privateKey,
|
||||
action: {
|
||||
@@ -187,7 +202,7 @@ export const Security = (): JSX.Element => {
|
||||
name: "isManaged",
|
||||
label: "Managed",
|
||||
description:
|
||||
'If true, device is considered to be "managed" by a mesh administrator via admin messages',
|
||||
'If true, device configuration options are only able to be changed remotely by a Remote Admin node via admin messages. Do not enable this option unless a suitable Remote Admin node has been setup, and the public key stored in the field below.',
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
@@ -228,8 +243,8 @@ export const Security = (): JSX.Element => {
|
||||
]}
|
||||
/>
|
||||
<PkiRegenerateDialog
|
||||
open={dialogOpen}
|
||||
onOpenChange={() => setDialogOpen(false)}
|
||||
open={privateKeyDialogOpen}
|
||||
onOpenChange={() => setPrivateKeyDialogOpen(false)}
|
||||
onSubmit={() => pkiRegenerate()}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { TabElementProps } from "@app/components/Dialog/NewDeviceDialog";
|
||||
import { Button } from "@components/UI/Button.js";
|
||||
import { Mono } from "@components/generic/Mono.js";
|
||||
import { useAppStore } from "@core/stores/appStore.js";
|
||||
import { useDeviceStore } from "@core/stores/deviceStore.js";
|
||||
import { subscribeAll } from "@core/subscriptions.js";
|
||||
import { randId } from "@core/utils/randId.js";
|
||||
import { Button } from "@components/UI/Button.tsx";
|
||||
import { Mono } from "@components/generic/Mono.tsx";
|
||||
import { useAppStore } from "@core/stores/appStore.ts";
|
||||
import { useDeviceStore } from "@core/stores/deviceStore.ts";
|
||||
import { subscribeAll } from "@core/subscriptions.ts";
|
||||
import { randId } from "@core/utils/randId.ts";
|
||||
import { BleConnection, Constants } from "@meshtastic/js";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { TabElementProps } from "@app/components/Dialog/NewDeviceDialog";
|
||||
import { Button } from "@components/UI/Button.js";
|
||||
import { Input } from "@components/UI/Input.js";
|
||||
import { Label } from "@components/UI/Label.js";
|
||||
import { Switch } from "@components/UI/Switch.js";
|
||||
import { useAppStore } from "@core/stores/appStore.js";
|
||||
import { useDeviceStore } from "@core/stores/deviceStore.js";
|
||||
import { subscribeAll } from "@core/subscriptions.js";
|
||||
import { randId } from "@core/utils/randId.js";
|
||||
import { Button } from "@components/UI/Button.tsx";
|
||||
import { Input } from "@components/UI/Input.tsx";
|
||||
import { Label } from "@components/UI/Label.tsx";
|
||||
import { Switch } from "@components/UI/Switch.tsx";
|
||||
import { useAppStore } from "@core/stores/appStore.ts";
|
||||
import { useDeviceStore } from "@core/stores/deviceStore.ts";
|
||||
import { subscribeAll } from "@core/subscriptions.ts";
|
||||
import { randId } from "@core/utils/randId.ts";
|
||||
import { HttpConnection } from "@meshtastic/js";
|
||||
import { useState } from "react";
|
||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { TabElementProps } from "@app/components/Dialog/NewDeviceDialog";
|
||||
import { Button } from "@components/UI/Button.js";
|
||||
import { Mono } from "@components/generic/Mono.js";
|
||||
import { useAppStore } from "@core/stores/appStore.js";
|
||||
import { useDeviceStore } from "@core/stores/deviceStore.js";
|
||||
import { subscribeAll } from "@core/subscriptions.js";
|
||||
import { randId } from "@core/utils/randId.js";
|
||||
import { Button } from "@components/UI/Button.tsx";
|
||||
import { Mono } from "@components/generic/Mono.tsx";
|
||||
import { useAppStore } from "@core/stores/appStore.ts";
|
||||
import { useDeviceStore } from "@core/stores/deviceStore.ts";
|
||||
import { subscribeAll } from "@core/subscriptions.ts";
|
||||
import { randId } from "@core/utils/randId.ts";
|
||||
import { SerialConnection } from "@meshtastic/js";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
@@ -14,13 +14,13 @@ export const Serial = ({ closeDialog }: TabElementProps): JSX.Element => {
|
||||
const { setSelectedDevice } = useAppStore();
|
||||
|
||||
const updateSerialPortList = useCallback(async () => {
|
||||
setSerialPorts(await navigator.serial.getPorts());
|
||||
setSerialPorts(await navigator?.serial.getPorts());
|
||||
}, []);
|
||||
|
||||
navigator.serial.addEventListener("connect", () => {
|
||||
navigator?.serial?.addEventListener("connect", () => {
|
||||
updateSerialPortList();
|
||||
});
|
||||
navigator.serial.addEventListener("disconnect", () => {
|
||||
navigator?.serial?.addEventListener("disconnect", () => {
|
||||
updateSerialPortList();
|
||||
});
|
||||
useEffect(() => {
|
||||
@@ -58,9 +58,8 @@ export const Serial = ({ closeDialog }: TabElementProps): JSX.Element => {
|
||||
await onConnect(port);
|
||||
}}
|
||||
>
|
||||
{`# ${index} - ${usbVendorId ?? "UNK"} - ${
|
||||
usbProductId ?? "UNK"
|
||||
}`}
|
||||
{`# ${index} - ${usbVendorId ?? "UNK"} - ${usbProductId ?? "UNK"
|
||||
}`}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
import { Mono } from "@components/generic/Mono.tsx";
|
||||
import { H5 } from "@app/components/UI/Typography/H5.tsx";
|
||||
import { Subtle } from "@app/components/UI/Typography/Subtle.tsx";
|
||||
import { Separator } from "@app/components/UI/Seperator";
|
||||
import { TimeAgo } from "@components/generic/Table/tmp/TimeAgo.tsx";
|
||||
import { Hashicon } from "@emeraldpay/hashicon-react";
|
||||
import { Protobuf } from "@meshtastic/js";
|
||||
import type { Protobuf as ProtobufType } from "@meshtastic/js";
|
||||
import {
|
||||
BatteryChargingIcon,
|
||||
BatteryFullIcon,
|
||||
BatteryLowIcon,
|
||||
BatteryMediumIcon,
|
||||
Dot,
|
||||
LockIcon,
|
||||
LockOpenIcon,
|
||||
MountainSnow,
|
||||
Star,
|
||||
} from "lucide-react";
|
||||
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
||||
|
||||
export interface NodeDetailProps {
|
||||
node: ProtobufType.Mesh.NodeInfo;
|
||||
}
|
||||
|
||||
export const NodeDetail = ({ node }: NodeDetailProps): JSX.Element => {
|
||||
const name = node.user?.longName || `!${numberToHexUnpadded(node.num)}`;
|
||||
const hardwareType = Protobuf.Mesh.HardwareModel[
|
||||
node.user?.hwModel ?? 0
|
||||
].replaceAll("_", " ");
|
||||
|
||||
return (
|
||||
<div className="dark:text-black">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex flex-col items-center gap-2 min-w-6 pt-1">
|
||||
<Hashicon value={node.num.toString()} size={22} />
|
||||
|
||||
<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"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Star
|
||||
fill={node.isFavorite ? "black" : "none"}
|
||||
size={15}
|
||||
aria-label={node.isFavorite ? "Favorite" : "Not a Favorite"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<H5>{name}</H5>
|
||||
|
||||
{hardwareType !== "UNSET" && <Subtle>{hardwareType}</Subtle>}
|
||||
|
||||
{!!node.deviceMetrics?.batteryLevel && (
|
||||
<div
|
||||
className="flex items-center gap-1"
|
||||
title={`${
|
||||
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} />
|
||||
)}
|
||||
<Subtle aria-label="Battery">
|
||||
{node.deviceMetrics?.batteryLevel > 100
|
||||
? "Charging"
|
||||
: node.deviceMetrics?.batteryLevel + "%"}
|
||||
</Subtle>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2 items-center">
|
||||
{node.user?.shortName && <div>"{node.user?.shortName}"</div>}
|
||||
{node.user?.id && <div>{node.user?.id}</div>}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex gap-1"
|
||||
title={new Date(node.lastHeard * 1000).toLocaleString(
|
||||
navigator.language,
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
{node.lastHeard > 0 && (
|
||||
<div>
|
||||
Heard <TimeAgo timestamp={node.lastHeard * 1000} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{node.viaMqtt && (
|
||||
<div style={{ color: "#660066" }} className="font-medium">
|
||||
MQTT
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator className="my-1" />
|
||||
|
||||
<div className="flex mt-2 text-sm">
|
||||
<div className="flex items-center flex-grow">
|
||||
<div className="border-2 border-black rounded px-0.5 mr-1">
|
||||
{isNaN(node.hopsAway) ? "?" : node.hopsAway}
|
||||
</div>
|
||||
<div>{node.hopsAway === 1 ? "Hop" : "Hops"}</div>
|
||||
</div>
|
||||
{node.position?.altitude && (
|
||||
<div className="flex items-center flex-grow">
|
||||
<MountainSnow
|
||||
size={15}
|
||||
className="ml-2 mr-1"
|
||||
aria-label="Elevation"
|
||||
/>
|
||||
<div>{node.position?.altitude} ft</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex mt-2">
|
||||
{!!node.deviceMetrics?.channelUtilization && (
|
||||
<div className="flex-grow">
|
||||
<div>Channel Util</div>
|
||||
<Mono>
|
||||
{node.deviceMetrics?.channelUtilization.toPrecision(3)}%
|
||||
</Mono>
|
||||
</div>
|
||||
)}
|
||||
{!!node.deviceMetrics?.airUtilTx && (
|
||||
<div className="flex-grow">
|
||||
<div>Airtime Util</div>
|
||||
<Mono>{node.deviceMetrics?.airUtilTx.toPrecision(3)}%</Mono>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{node.snr !== 0 && (
|
||||
<div className="mt-2">
|
||||
<div>SNR</div>
|
||||
<Mono className="flex items-center text-xs">
|
||||
{node.snr}db
|
||||
<Dot />
|
||||
{Math.min(Math.max((node.snr + 10) * 5, 0), 100)}%
|
||||
<Dot />
|
||||
{(node.snr + 10) * 5}raw
|
||||
</Mono>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Subtle } from "@app/components/UI/Typography/Subtle.js";
|
||||
import { Subtle } from "@app/components/UI/Typography/Subtle.tsx";
|
||||
import {
|
||||
type MessageWithState,
|
||||
useDevice,
|
||||
} from "@app/core/stores/deviceStore.js";
|
||||
import { Message } from "@components/PageComponents/Messages/Message.js";
|
||||
import { MessageInput } from "@components/PageComponents/Messages/MessageInput.js";
|
||||
import { TraceRoute } from "@components/PageComponents/Messages/TraceRoute.js";
|
||||
} from "@app/core/stores/deviceStore.ts";
|
||||
import { Message } from "@components/PageComponents/Messages/Message.tsx";
|
||||
import { MessageInput } from "@components/PageComponents/Messages/MessageInput.tsx";
|
||||
import { TraceRoute } from "@components/PageComponents/Messages/TraceRoute.tsx";
|
||||
import type { Protobuf, Types } from "@meshtastic/js";
|
||||
import { InboxIcon } from "lucide-react";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { MessageWithState } from "@app/core/stores/deviceStore.js";
|
||||
import type { MessageWithState } from "@app/core/stores/deviceStore.ts";
|
||||
import { Hashicon } from "@emeraldpay/hashicon-react";
|
||||
import type { Protobuf } from "@meshtastic/js";
|
||||
import {
|
||||
@@ -44,6 +44,9 @@ export const Message = ({
|
||||
<span className="cursor-pointer font-medium text-textPrimary">
|
||||
{sender?.user?.longName ?? "UNK"}
|
||||
</span>
|
||||
<span className="mt-1 font-mono text-xs text-textSecondary">
|
||||
{message.rxTime.toLocaleDateString()}
|
||||
</span>
|
||||
<span className="mt-1 font-mono text-xs text-textSecondary">
|
||||
{message.rxTime.toLocaleTimeString(undefined, {
|
||||
hour: "2-digit",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Button } from "@components/UI/Button.js";
|
||||
import { Input } from "@components/UI/Input.js";
|
||||
import { useDevice } from "@core/stores/deviceStore.js";
|
||||
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/js";
|
||||
import { SendIcon } from "lucide-react";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useDevice } from "@app/core/stores/deviceStore.js";
|
||||
import { useDevice } from "@app/core/stores/deviceStore.ts";
|
||||
import type { Protobuf } from "@meshtastic/js";
|
||||
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useDevice } from "@app/core/stores/deviceStore.js";
|
||||
import type { AmbientLightingValidation } from "@app/validation/moduleConfig/ambientLighting.js";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.js";
|
||||
import { useDevice } from "@app/core/stores/deviceStore.ts";
|
||||
import type { AmbientLightingValidation } from "@app/validation/moduleConfig/ambientLighting.tsx";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||
import { Protobuf } from "@meshtastic/js";
|
||||
|
||||
export const AmbientLighting = (): JSX.Element => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { AudioValidation } from "@app/validation/moduleConfig/audio.js";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.js";
|
||||
import { useDevice } from "@core/stores/deviceStore.js";
|
||||
import type { AudioValidation } from "@app/validation/moduleConfig/audio.tsx";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { Protobuf } from "@meshtastic/js";
|
||||
|
||||
export const Audio = (): JSX.Element => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { CannedMessageValidation } from "@app/validation/moduleConfig/cannedMessage.js";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.js";
|
||||
import { useDevice } from "@core/stores/deviceStore.js";
|
||||
import type { CannedMessageValidation } from "@app/validation/moduleConfig/cannedMessage.tsx";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { Protobuf } from "@meshtastic/js";
|
||||
|
||||
export const CannedMessage = (): JSX.Element => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useDevice } from "@app/core/stores/deviceStore.js";
|
||||
import type { DetectionSensorValidation } from "@app/validation/moduleConfig/detectionSensor.js";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.js";
|
||||
import { useDevice } from "@app/core/stores/deviceStore.ts";
|
||||
import type { DetectionSensorValidation } from "@app/validation/moduleConfig/detectionSensor.tsx";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||
import { Protobuf } from "@meshtastic/js";
|
||||
|
||||
export const DetectionSensor = (): JSX.Element => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ExternalNotificationValidation } from "@app/validation/moduleConfig/externalNotification.js";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.js";
|
||||
import { useDevice } from "@core/stores/deviceStore.js";
|
||||
import type { ExternalNotificationValidation } from "@app/validation/moduleConfig/externalNotification.tsx";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { Protobuf } from "@meshtastic/js";
|
||||
|
||||
export const ExternalNotification = (): JSX.Element => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useDevice } from "@app/core/stores/deviceStore.js";
|
||||
import type { MqttValidation } from "@app/validation/moduleConfig/mqtt.js";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.js";
|
||||
import { useDevice } from "@app/core/stores/deviceStore.ts";
|
||||
import type { MqttValidation } from "@app/validation/moduleConfig/mqtt.tsx";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||
import { Protobuf } from "@meshtastic/js";
|
||||
|
||||
export const MQTT = (): JSX.Element => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useDevice } from "@app/core/stores/deviceStore.js";
|
||||
import type { NeighborInfoValidation } from "@app/validation/moduleConfig/neighborInfo.js";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.js";
|
||||
import { useDevice } from "@app/core/stores/deviceStore.ts";
|
||||
import type { NeighborInfoValidation } from "@app/validation/moduleConfig/neighborInfo.tsx";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||
import { Protobuf } from "@meshtastic/js";
|
||||
|
||||
export const NeighborInfo = (): JSX.Element => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { PaxcounterValidation } from "@app/validation/moduleConfig/paxcounter.js";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.js";
|
||||
import { useDevice } from "@core/stores/deviceStore.js";
|
||||
import type { PaxcounterValidation } from "@app/validation/moduleConfig/paxcounter.tsx";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { Protobuf } from "@meshtastic/js";
|
||||
|
||||
export const Paxcounter = (): JSX.Element => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { RangeTestValidation } from "@app/validation/moduleConfig/rangeTest.js";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.js";
|
||||
import { useDevice } from "@core/stores/deviceStore.js";
|
||||
import type { RangeTestValidation } from "@app/validation/moduleConfig/rangeTest.tsx";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { Protobuf } from "@meshtastic/js";
|
||||
|
||||
export const RangeTest = (): JSX.Element => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { SerialValidation } from "@app/validation/moduleConfig/serial.js";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.js";
|
||||
import { useDevice } from "@core/stores/deviceStore.js";
|
||||
import type { SerialValidation } from "@app/validation/moduleConfig/serial.tsx";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { Protobuf } from "@meshtastic/js";
|
||||
|
||||
export const Serial = (): JSX.Element => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { StoreForwardValidation } from "@app/validation/moduleConfig/storeForward.js";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.js";
|
||||
import { useDevice } from "@core/stores/deviceStore.js";
|
||||
import type { StoreForwardValidation } from "@app/validation/moduleConfig/storeForward.ts";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { Protobuf } from "@meshtastic/js";
|
||||
|
||||
export const StoreForward = (): JSX.Element => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { TelemetryValidation } from "@app/validation/moduleConfig/telemetry.js";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.js";
|
||||
import { useDevice } from "@core/stores/deviceStore.js";
|
||||
import type { TelemetryValidation } from "@app/validation/moduleConfig/telemetry.tsx";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { Protobuf } from "@meshtastic/js";
|
||||
|
||||
export const Telemetry = (): JSX.Element => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { cn } from "@app/core/utils/cn.js";
|
||||
import { cn } from "@app/core/utils/cn.ts";
|
||||
import { AlignLeftIcon, type LucideIcon } from "lucide-react";
|
||||
import Footer from "./UI/Footer";
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { SidebarSection } from "@components/UI/Sidebar/SidebarSection.js";
|
||||
import { SidebarButton } from "@components/UI/Sidebar/sidebarButton.js";
|
||||
import { Subtle } from "@components/UI/Typography/Subtle.js";
|
||||
import { useDevice } from "@core/stores/deviceStore.js";
|
||||
import type { Page } from "@core/stores/deviceStore.js";
|
||||
import { SidebarSection } from "@components/UI/Sidebar/SidebarSection.tsx";
|
||||
import { SidebarButton } from "@components/UI/Sidebar/sidebarButton.tsx";
|
||||
import { Subtle } from "@components/UI/Typography/Subtle.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import type { Page } from "@core/stores/deviceStore.ts";
|
||||
import {
|
||||
BatteryMediumIcon,
|
||||
CpuIcon,
|
||||
@@ -12,9 +12,12 @@ import {
|
||||
MapIcon,
|
||||
MessageSquareIcon,
|
||||
SettingsIcon,
|
||||
SidebarCloseIcon,
|
||||
SidebarOpenIcon,
|
||||
UsersIcon,
|
||||
ZapIcon,
|
||||
} from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
export interface SidebarProps {
|
||||
children?: React.ReactNode;
|
||||
@@ -25,6 +28,7 @@ export const Sidebar = ({ children }: SidebarProps): JSX.Element => {
|
||||
const myNode = nodes.get(hardware.myNodeNum);
|
||||
const myMetadata = metadata.get(0);
|
||||
const { activePage, setActivePage, setDialogOpen } = useDevice();
|
||||
const [showSidebar, setShowSidebar] = useState<boolean>(true);
|
||||
|
||||
interface NavLink {
|
||||
name: string;
|
||||
@@ -60,7 +64,7 @@ export const Sidebar = ({ children }: SidebarProps): JSX.Element => {
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
return showSidebar ? (
|
||||
<div className="min-w-[280px] max-w-min flex-col overflow-y-auto border-r-[0.5px] border-slate-300 bg-transparent dark:border-slate-700">
|
||||
<div className="flex justify-between px-8 pt-6">
|
||||
<div>
|
||||
@@ -76,11 +80,20 @@ export const Sidebar = ({ children }: SidebarProps): JSX.Element => {
|
||||
>
|
||||
<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 ?? "UNK"}%</Subtle>
|
||||
<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"} />
|
||||
@@ -109,5 +122,11 @@ export const Sidebar = ({ children }: SidebarProps): JSX.Element => {
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
+13
-14
@@ -1,5 +1,3 @@
|
||||
import { useToast } from "@core/hooks/useToast.js";
|
||||
|
||||
import {
|
||||
Toast,
|
||||
ToastClose,
|
||||
@@ -7,24 +5,25 @@ import {
|
||||
ToastProvider,
|
||||
ToastTitle,
|
||||
ToastViewport,
|
||||
} from "@components/UI/Toast.js";
|
||||
} from "@components/UI/Toast";
|
||||
import { useToast } from "@core/hooks/useToast";
|
||||
|
||||
export function Toaster() {
|
||||
const { toasts } = useToast();
|
||||
|
||||
return (
|
||||
<ToastProvider>
|
||||
{toasts.map(({ id, title, description, action, ...props }) => (
|
||||
<Toast key={id} {...props}>
|
||||
{toasts.map(({ id, title, description, action, duration, ...props }) => (
|
||||
<Toast
|
||||
key={id}
|
||||
{...props}
|
||||
|
||||
duration={duration}
|
||||
className="flex flex-col gap-4"
|
||||
>
|
||||
<div className="grid gap-1">
|
||||
{title && (
|
||||
<ToastTitle className="dark:text-white">{title}</ToastTitle>
|
||||
)}
|
||||
{description && (
|
||||
<ToastDescription className="dark:text-white-400">
|
||||
{description}
|
||||
</ToastDescription>
|
||||
)}
|
||||
{title && <ToastTitle>{title}</ToastTitle>}
|
||||
{description && <ToastDescription>{description}</ToastDescription>}
|
||||
</div>
|
||||
{action}
|
||||
<ToastClose />
|
||||
@@ -33,4 +32,4 @@ export function Toaster() {
|
||||
<ToastViewport />
|
||||
</ToastProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type VariantProps, cva } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@core/utils/cn.js";
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 dark:hover:bg-slate-800 dark:hover:text-slate-100 disabled:opacity-50 dark:focus:ring-slate-400 disabled:pointer-events-none dark:focus:ring-offset-slate-900 data-[state=open]:bg-slate-100 dark:data-[state=open]:bg-slate-800",
|
||||
@@ -35,9 +35,11 @@ const buttonVariants = cva(
|
||||
},
|
||||
);
|
||||
|
||||
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {}
|
||||
VariantProps<typeof buttonVariants> { }
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, ...props }, ref) => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||
import { Check } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@core/utils/cn.js";
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
|
||||
const Checkbox = React.forwardRef<
|
||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||
|
||||
@@ -3,8 +3,8 @@ import { Command as CommandPrimitive } from "cmdk";
|
||||
import { Search } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { Dialog, DialogContent } from "@components/UI/Dialog.js";
|
||||
import { cn } from "@core/utils/cn.js";
|
||||
import { Dialog, DialogContent } from "@components/UI/Dialog.tsx";
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
|
||||
const Command = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive>,
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { X } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@core/utils/cn.js";
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
|
||||
const Dialog = DialogPrimitive.Root;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import { Check, ChevronRight, Circle } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@core/utils/cn.js";
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root;
|
||||
|
||||
|
||||
@@ -1,26 +1,30 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { Button } from "@components/UI/Button.js";
|
||||
import { Input } from "@components/UI/Input.js";
|
||||
import { Button, type ButtonVariant } from "@components/UI/Button.tsx";
|
||||
import { Input } from "@components/UI/Input.tsx";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@components/UI/Select.js";
|
||||
} from "@components/UI/Select.tsx";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
|
||||
export interface GeneratorProps extends React.BaseHTMLAttributes<HTMLElement> {
|
||||
hide?: boolean;
|
||||
type: "text" | "password";
|
||||
devicePSKBitCount?: number;
|
||||
value: string;
|
||||
variant: "default" | "invalid";
|
||||
buttonText?: string;
|
||||
actionButtons: {
|
||||
text: string;
|
||||
onClick: React.MouseEventHandler<HTMLButtonElement>;
|
||||
variant: ButtonVariant;
|
||||
className?: string;
|
||||
}[];
|
||||
bits?: { text: string; value: string; key: string }[];
|
||||
selectChange: (event: string) => void;
|
||||
inputChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
buttonClick: React.MouseEventHandler<HTMLButtonElement>;
|
||||
action?: {
|
||||
icon: LucideIcon;
|
||||
onClick: () => void;
|
||||
@@ -31,19 +35,19 @@ export interface GeneratorProps extends React.BaseHTMLAttributes<HTMLElement> {
|
||||
const Generator = React.forwardRef<HTMLInputElement, GeneratorProps>(
|
||||
(
|
||||
{
|
||||
hide = true,
|
||||
type,
|
||||
devicePSKBitCount,
|
||||
variant,
|
||||
value,
|
||||
buttonText,
|
||||
actionButtons,
|
||||
bits = [
|
||||
{ text: "256 bit", value: "32", key: "bit256" },
|
||||
{ text: "128 bit", value: "16", key: "bit128" },
|
||||
{ text: "8 bit", value: "1", key: "bit8" },
|
||||
{ text: "Empty", value: "0", key: "empty" },
|
||||
],
|
||||
selectChange,
|
||||
inputChange,
|
||||
buttonClick,
|
||||
action,
|
||||
disabled,
|
||||
...props
|
||||
@@ -68,7 +72,7 @@ const Generator = React.forwardRef<HTMLInputElement, GeneratorProps>(
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
type={hide ? "password" : "text"}
|
||||
type={type}
|
||||
id="pskInput"
|
||||
variant={variant}
|
||||
value={value}
|
||||
@@ -93,15 +97,21 @@ const Generator = React.forwardRef<HTMLInputElement, GeneratorProps>(
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
type="button"
|
||||
variant="success"
|
||||
onClick={buttonClick}
|
||||
disabled={disabled}
|
||||
{...props}
|
||||
>
|
||||
{buttonText}
|
||||
</Button>
|
||||
<div className="flex ml-4 space-x-4">
|
||||
{actionButtons?.map(({ text, onClick, variant, className }) => (
|
||||
<Button
|
||||
key={text}
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
variant={variant}
|
||||
className={className}
|
||||
{...props}
|
||||
>
|
||||
{text}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@core/utils/cn.js";
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
import { type VariantProps, cva } from "class-variance-authority";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@core/utils/cn.js";
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as MenubarPrimitive from "@radix-ui/react-menubar";
|
||||
import { Check, ChevronRight, Circle } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@core/utils/cn.js";
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
|
||||
const MenubarMenu = MenubarPrimitive.Menu;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@core/utils/cn.js";
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
|
||||
const Popover = PopoverPrimitive.Root;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@core/utils/cn.js";
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
|
||||
const ScrollArea = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as SelectPrimitive from "@radix-ui/react-select";
|
||||
import { Check, ChevronDown } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@core/utils/cn.js";
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
|
||||
const Select = SelectPrimitive.Root;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@core/utils/cn.js";
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { H4 } from "@components/UI/Typography/H4.js";
|
||||
import { H4 } from "@components/UI/Typography/H4.tsx";
|
||||
|
||||
export interface SidebarSectionProps {
|
||||
label: string;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button } from "@components/UI/Button.js";
|
||||
import { Button } from "@components/UI/Button.tsx";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
|
||||
export interface SidebarButtonProps {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@core/utils/cn.js";
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
|
||||
const Switch = React.forwardRef<
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@core/utils/cn.js";
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
|
||||
const Tabs = TabsPrimitive.Root;
|
||||
|
||||
|
||||
+35
-35
@@ -1,11 +1,11 @@
|
||||
import * as ToastPrimitives from "@radix-ui/react-toast";
|
||||
import { type VariantProps, cva } from "class-variance-authority";
|
||||
import { X } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import * as React from "react"
|
||||
import * as ToastPrimitives from "@radix-ui/react-toast"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { X } from 'lucide-react'
|
||||
|
||||
import { cn } from "@core/utils/cn.js";
|
||||
import { cn } from "@core/utils/cn"
|
||||
|
||||
const ToastProvider = ToastPrimitives.Provider;
|
||||
const ToastProvider = ToastPrimitives.Provider
|
||||
|
||||
const ToastViewport = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
||||
@@ -14,35 +14,34 @@ const ToastViewport = React.forwardRef<
|
||||
<ToastPrimitives.Viewport
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed top-0 z-50 flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
||||
className,
|
||||
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-24 sm:right-6 sm:top-auto sm:flex-col md:max-w-[420px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
|
||||
))
|
||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
|
||||
|
||||
const toastVariants = cva(
|
||||
"data-[swipe=move]:transition-none grow-1 group relative pointer-events-auto flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full mt-4 data-[state=closed]:slide-out-to-right-full dark:border-slate-700 last:mt-0 sm:last:mt-4",
|
||||
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-white border-slate-200 dark:bg-slate-800 dark:border-slate-700",
|
||||
default: "border bg-background text-foreground dark:bg-slate-700 dark:border-slate-600 dark:text-slate-50",
|
||||
destructive:
|
||||
"group destructive bg-red-600 text-white border-red-600 dark:border-red-600",
|
||||
"group destructive bg-red-600 text-white dark:border-red-900 dark:bg-red-900 dark:text-red-50"
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
)
|
||||
|
||||
const Toast = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
||||
VariantProps<typeof toastVariants>
|
||||
VariantProps<typeof toastVariants>
|
||||
>(({ className, variant, ...props }, ref) => {
|
||||
return (
|
||||
<ToastPrimitives.Root
|
||||
@@ -50,9 +49,9 @@ const Toast = React.forwardRef<
|
||||
className={cn(toastVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
Toast.displayName = ToastPrimitives.Root.displayName;
|
||||
)
|
||||
})
|
||||
Toast.displayName = ToastPrimitives.Root.displayName
|
||||
|
||||
const ToastAction = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Action>,
|
||||
@@ -61,13 +60,13 @@ const ToastAction = React.forwardRef<
|
||||
<ToastPrimitives.Action
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border border-slate-200 bg-transparent px-3 text-sm font-medium transition-colors hover:bg-slate-100 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-red-100 group-[.destructive]:hover:border-slate-50 group-[.destructive]:hover:bg-red-100 group-[.destructive]:hover:text-red-600 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600 dark:border-slate-700 dark:text-slate-100 dark:hover:bg-slate-700 dark:hover:text-slate-100 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900 dark:data-[state=open]:bg-slate-800",
|
||||
className,
|
||||
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastAction.displayName = ToastPrimitives.Action.displayName;
|
||||
))
|
||||
ToastAction.displayName = ToastPrimitives.Action.displayName
|
||||
|
||||
const ToastClose = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Close>,
|
||||
@@ -76,16 +75,16 @@ const ToastClose = React.forwardRef<
|
||||
<ToastPrimitives.Close
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"absolute right-2 top-2 rounded-md p-1 text-slate-500 opacity-0 transition-opacity hover:text-slate-900 focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600 dark:hover:text-slate-50",
|
||||
className,
|
||||
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600 dark:text-slate-400 dark:hover:text-slate-50",
|
||||
className
|
||||
)}
|
||||
toast-close=""
|
||||
{...props}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</ToastPrimitives.Close>
|
||||
));
|
||||
ToastClose.displayName = ToastPrimitives.Close.displayName;
|
||||
))
|
||||
ToastClose.displayName = ToastPrimitives.Close.displayName
|
||||
|
||||
const ToastTitle = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Title>,
|
||||
@@ -96,8 +95,8 @@ const ToastTitle = React.forwardRef<
|
||||
className={cn("text-sm font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastTitle.displayName = ToastPrimitives.Title.displayName;
|
||||
))
|
||||
ToastTitle.displayName = ToastPrimitives.Title.displayName
|
||||
|
||||
const ToastDescription = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Description>,
|
||||
@@ -108,12 +107,12 @@ const ToastDescription = React.forwardRef<
|
||||
className={cn("text-sm opacity-90", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastDescription.displayName = ToastPrimitives.Description.displayName;
|
||||
))
|
||||
ToastDescription.displayName = ToastPrimitives.Description.displayName
|
||||
|
||||
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
|
||||
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
|
||||
|
||||
type ToastActionElement = React.ReactElement<typeof ToastAction>;
|
||||
type ToastActionElement = React.ReactElement<typeof ToastAction>
|
||||
|
||||
export {
|
||||
type ToastProps,
|
||||
@@ -125,4 +124,5 @@ export {
|
||||
ToastDescription,
|
||||
ToastClose,
|
||||
ToastAction,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@core/utils/cn.js";
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { cn } from "@app/core/utils/cn.js";
|
||||
import { cn } from "@app/core/utils/cn.ts";
|
||||
|
||||
export interface H4Props {
|
||||
className?: string;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { cn } from "@app/core/utils/cn.ts";
|
||||
|
||||
export interface H5Props {
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const H5 = ({ className, children }: H5Props): JSX.Element => (
|
||||
<h5
|
||||
className={cn("scroll-m-20 text-lg font-medium tracking-tight", className)}
|
||||
>
|
||||
{children}
|
||||
</h5>
|
||||
);
|
||||
@@ -1,14 +1,17 @@
|
||||
import { cn } from "@app/core/utils/cn";
|
||||
|
||||
export interface LinkProps {
|
||||
href: string;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const Link = ({ href, children }: LinkProps): JSX.Element => (
|
||||
export const Link = ({ href, children, className }: LinkProps): JSX.Element => (
|
||||
<a
|
||||
href={href}
|
||||
target={"_blank"}
|
||||
rel="noopener noreferrer"
|
||||
className="font-medium text-slate-900 underline underline-offset-4 dark:text-slate-50"
|
||||
className={cn("font-medium text-slate-900 underline underline-offset-4 dark:text-slate-50", className)}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { cn } from "@app/core/utils/cn.js";
|
||||
import { cn } from "@app/core/utils/cn.ts";
|
||||
|
||||
export interface SubtleProps {
|
||||
className?: string;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useAppStore } from "@core/stores/appStore.js";
|
||||
import { useAppStore } from "@core/stores/appStore.ts";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export interface ThemeControllerProps {
|
||||
|
||||
Reference in New Issue
Block a user