Add command palette & cleanup
This commit is contained in:
@@ -47,6 +47,7 @@
|
|||||||
"zustand": "4.1.1"
|
"zustand": "4.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@tailwindcss/forms": "^0.5.3",
|
||||||
"@types/chrome": "^0.0.197",
|
"@types/chrome": "^0.0.197",
|
||||||
"@types/geodesy": "^2.2.3",
|
"@types/geodesy": "^2.2.3",
|
||||||
"@types/node": "^18.8.3",
|
"@types/node": "^18.8.3",
|
||||||
|
|||||||
Generated
+16
@@ -7,6 +7,7 @@ specifiers:
|
|||||||
'@hookform/resolvers': ^2.9.8
|
'@hookform/resolvers': ^2.9.8
|
||||||
'@meshtastic/eslint-config': ^1.0.8
|
'@meshtastic/eslint-config': ^1.0.8
|
||||||
'@meshtastic/meshtasticjs': ^0.6.104
|
'@meshtastic/meshtasticjs': ^0.6.104
|
||||||
|
'@tailwindcss/forms': ^0.5.3
|
||||||
'@tailwindcss/line-clamp': ^0.4.2
|
'@tailwindcss/line-clamp': ^0.4.2
|
||||||
'@tailwindcss/typography': ^0.5.7
|
'@tailwindcss/typography': ^0.5.7
|
||||||
'@types/chrome': ^0.0.197
|
'@types/chrome': ^0.0.197
|
||||||
@@ -76,6 +77,7 @@ dependencies:
|
|||||||
zustand: 4.1.1_immer@9.0.15+react@18.2.0
|
zustand: 4.1.1_immer@9.0.15+react@18.2.0
|
||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@tailwindcss/forms': 0.5.3_tailwindcss@3.1.8
|
||||||
'@types/chrome': 0.0.197
|
'@types/chrome': 0.0.197
|
||||||
'@types/geodesy': 2.2.3
|
'@types/geodesy': 2.2.3
|
||||||
'@types/node': 18.8.3
|
'@types/node': 18.8.3
|
||||||
@@ -774,6 +776,15 @@ packages:
|
|||||||
resolution: {integrity: sha512-WfqfX/eXGiAd3RJe4VU2snh/ZPwtSjLG4ynQ/vYzvghTh7dHFcI1wl+nrkWG6lGhukOxOsUHfv8dUXr58D0ayg==}
|
resolution: {integrity: sha512-WfqfX/eXGiAd3RJe4VU2snh/ZPwtSjLG4ynQ/vYzvghTh7dHFcI1wl+nrkWG6lGhukOxOsUHfv8dUXr58D0ayg==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@tailwindcss/forms/0.5.3_tailwindcss@3.1.8:
|
||||||
|
resolution: {integrity: sha512-y5mb86JUoiUgBjY/o6FJSFZSEttfb3Q5gllE4xoKjAAD+vBrnIhE4dViwUuow3va8mpH4s9jyUbUbrRGoRdc2Q==}
|
||||||
|
peerDependencies:
|
||||||
|
tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1'
|
||||||
|
dependencies:
|
||||||
|
mini-svg-data-uri: 1.4.4
|
||||||
|
tailwindcss: 3.1.8_postcss@8.4.17
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@tailwindcss/line-clamp/0.4.2_tailwindcss@3.1.8:
|
/@tailwindcss/line-clamp/0.4.2_tailwindcss@3.1.8:
|
||||||
resolution: {integrity: sha512-HFzAQuqYCjyy/SX9sLGB1lroPzmcnWv1FHkIpmypte10hptf4oPUfucryMKovZh2u0uiS9U5Ty3GghWfEJGwVw==}
|
resolution: {integrity: sha512-HFzAQuqYCjyy/SX9sLGB1lroPzmcnWv1FHkIpmypte10hptf4oPUfucryMKovZh2u0uiS9U5Ty3GghWfEJGwVw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -2883,6 +2894,11 @@ packages:
|
|||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/mini-svg-data-uri/1.4.4:
|
||||||
|
resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==}
|
||||||
|
hasBin: true
|
||||||
|
dev: true
|
||||||
|
|
||||||
/minimatch/3.1.2:
|
/minimatch/3.1.2:
|
||||||
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import { useAppStore } from "@app/core/stores/appStore.js";
|
|||||||
import { DeviceWrapper } from "@app/DeviceWrapper.js";
|
import { DeviceWrapper } from "@app/DeviceWrapper.js";
|
||||||
import { useDeviceStore } from "@core/stores/deviceStore.js";
|
import { useDeviceStore } from "@core/stores/deviceStore.js";
|
||||||
|
|
||||||
|
import { CommandPalette } from "./components/CommandPalette/Index.js";
|
||||||
import { DeviceSelector } from "./components/DeviceSelector.js";
|
import { DeviceSelector } from "./components/DeviceSelector.js";
|
||||||
|
import { DialogManager } from "./components/Dialog/DialogManager.js";
|
||||||
import { NewDevice } from "./components/NewDevice.js";
|
import { NewDevice } from "./components/NewDevice.js";
|
||||||
import { PageNav } from "./components/PageNav.js";
|
import { PageNav } from "./components/PageNav.js";
|
||||||
import { Sidebar } from "./components/Sidebar.js";
|
import { Sidebar } from "./components/Sidebar.js";
|
||||||
@@ -25,11 +27,13 @@ export const App = (): JSX.Element => {
|
|||||||
|
|
||||||
{device && (
|
{device && (
|
||||||
<DeviceWrapper device={device}>
|
<DeviceWrapper device={device}>
|
||||||
|
<CommandPalette />
|
||||||
<Toaster
|
<Toaster
|
||||||
toastOptions={{
|
toastOptions={{
|
||||||
duration: 2000,
|
duration: 2000,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<DialogManager />
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
<PageNav />
|
<PageNav />
|
||||||
<MapProvider>
|
<MapProvider>
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import type React from "react";
|
||||||
|
|
||||||
|
import { Combobox } from "@headlessui/react";
|
||||||
|
import { ChevronRightIcon } from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
|
import type { Group } from "./Index.js";
|
||||||
|
|
||||||
|
export interface GroupViewProps {
|
||||||
|
group: Group;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GroupView = ({ group }: GroupViewProps): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<Combobox.Option
|
||||||
|
value={group.name}
|
||||||
|
className={({ active }) =>
|
||||||
|
`flex cursor-default select-none items-center rounded-md px-3 py-2 ${
|
||||||
|
active ? "bg-gray-900 bg-opacity-5 text-gray-900" : ""
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{({ active }) => (
|
||||||
|
<>
|
||||||
|
<group.icon
|
||||||
|
className={`h-6 w-6 flex-none text-gray-900 text-opacity-40 ${
|
||||||
|
active ? "text-opacity-100" : ""
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
<span className="ml-3 flex-auto truncate">{group.name}</span>
|
||||||
|
{active && <ChevronRightIcon className="h-5 text-gray-400" />}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Combobox.Option>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,268 @@
|
|||||||
|
/**
|
||||||
|
* Contextual
|
||||||
|
* - Reset nodedb
|
||||||
|
* - Map commands
|
||||||
|
* - Disconnect
|
||||||
|
* Debug commands
|
||||||
|
* - Re-configure
|
||||||
|
* - clear parts of store (messages, positions, telemetry etc)
|
||||||
|
*
|
||||||
|
* Application
|
||||||
|
* - Light/Dark mode
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type React from "react";
|
||||||
|
import { Fragment, useState } from "react";
|
||||||
|
|
||||||
|
import { useDevice } from "@app/core/providers/useDevice.js";
|
||||||
|
import { useAppStore } from "@app/core/stores/appStore.js";
|
||||||
|
import { Combobox, Dialog, Transition } from "@headlessui/react";
|
||||||
|
import {
|
||||||
|
ArchiveBoxXMarkIcon,
|
||||||
|
ArrowPathIcon,
|
||||||
|
ArrowsRightLeftIcon,
|
||||||
|
BeakerIcon,
|
||||||
|
BugAntIcon,
|
||||||
|
Cog8ToothIcon,
|
||||||
|
CubeTransparentIcon,
|
||||||
|
DevicePhoneMobileIcon,
|
||||||
|
IdentificationIcon,
|
||||||
|
InboxIcon,
|
||||||
|
LinkIcon,
|
||||||
|
MapIcon,
|
||||||
|
MoonIcon,
|
||||||
|
PlusIcon,
|
||||||
|
QrCodeIcon,
|
||||||
|
Square3Stack3DIcon,
|
||||||
|
TrashIcon,
|
||||||
|
UsersIcon,
|
||||||
|
WindowIcon,
|
||||||
|
XCircleIcon,
|
||||||
|
} from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
|
import { GroupView } from "./GroupView.js";
|
||||||
|
import { NoResults } from "./NoResults.js";
|
||||||
|
import { PaletteTransition } from "./PaletteTransition.js";
|
||||||
|
import { SearchBox } from "./SearchBox.js";
|
||||||
|
import { SearchResult } from "./SearchResult.js";
|
||||||
|
|
||||||
|
export interface Group {
|
||||||
|
name: string;
|
||||||
|
icon: (props: React.ComponentProps<"svg">) => JSX.Element;
|
||||||
|
commands: Command[];
|
||||||
|
}
|
||||||
|
export interface Command {
|
||||||
|
name: string;
|
||||||
|
icon: (props: React.ComponentProps<"svg">) => JSX.Element;
|
||||||
|
action?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CommandPalette = (): JSX.Element => {
|
||||||
|
const [query, setQuery] = useState("");
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const { setQRDialogOpen, setActivePage, connection } = useDevice();
|
||||||
|
const { setSelectedDevice, removeDevice, selectedDevice } = useAppStore();
|
||||||
|
|
||||||
|
const groups: Group[] = [
|
||||||
|
{
|
||||||
|
name: "Goto",
|
||||||
|
icon: LinkIcon,
|
||||||
|
commands: [
|
||||||
|
{
|
||||||
|
name: "Messages",
|
||||||
|
icon: InboxIcon,
|
||||||
|
action() {
|
||||||
|
setActivePage("messages");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Map",
|
||||||
|
icon: MapIcon,
|
||||||
|
action() {
|
||||||
|
setActivePage("map");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Extensions",
|
||||||
|
icon: BeakerIcon,
|
||||||
|
action() {
|
||||||
|
setActivePage("extensions");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Config",
|
||||||
|
icon: Cog8ToothIcon,
|
||||||
|
action() {
|
||||||
|
setActivePage("config");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Channels",
|
||||||
|
icon: Square3Stack3DIcon,
|
||||||
|
action() {
|
||||||
|
setActivePage("channels");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Peers",
|
||||||
|
icon: UsersIcon,
|
||||||
|
action() {
|
||||||
|
setActivePage("peers");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Info",
|
||||||
|
icon: IdentificationIcon,
|
||||||
|
action() {
|
||||||
|
setActivePage("info");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Manage",
|
||||||
|
icon: DevicePhoneMobileIcon,
|
||||||
|
commands: [
|
||||||
|
{
|
||||||
|
name: "Switch Node",
|
||||||
|
icon: ArrowsRightLeftIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Connect New Node",
|
||||||
|
icon: PlusIcon,
|
||||||
|
action() {
|
||||||
|
setSelectedDevice(0);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Contextual",
|
||||||
|
icon: CubeTransparentIcon,
|
||||||
|
commands: [
|
||||||
|
{
|
||||||
|
name: "QR Code Generator",
|
||||||
|
icon: QrCodeIcon,
|
||||||
|
action() {
|
||||||
|
setQRDialogOpen(true);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Reset Peers",
|
||||||
|
icon: TrashIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Disconnect",
|
||||||
|
icon: XCircleIcon,
|
||||||
|
action() {
|
||||||
|
void connection?.disconnect();
|
||||||
|
setSelectedDevice(0);
|
||||||
|
removeDevice(selectedDevice ?? 0);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Debug",
|
||||||
|
icon: BugAntIcon,
|
||||||
|
commands: [
|
||||||
|
{
|
||||||
|
name: "Reconfigure",
|
||||||
|
icon: ArrowPathIcon,
|
||||||
|
action() {
|
||||||
|
void connection?.configure();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Clear Messages",
|
||||||
|
icon: ArchiveBoxXMarkIcon,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Application",
|
||||||
|
icon: WindowIcon,
|
||||||
|
commands: [
|
||||||
|
{
|
||||||
|
name: "Toggle Dark Mode",
|
||||||
|
icon: MoonIcon,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
window.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "k") {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
||||||
|
setOpen(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const filtered =
|
||||||
|
query === ""
|
||||||
|
? []
|
||||||
|
: groups
|
||||||
|
.map((group) => {
|
||||||
|
return {
|
||||||
|
...group,
|
||||||
|
commands: group.commands.filter((command) => {
|
||||||
|
return `${group.name} ${command.name}`
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(query.toLowerCase());
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((group) => group.commands.length);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Transition.Root
|
||||||
|
show={open}
|
||||||
|
as={Fragment}
|
||||||
|
afterLeave={() => setQuery("")}
|
||||||
|
appear
|
||||||
|
>
|
||||||
|
<Dialog as="div" className="relative z-10" onClose={setOpen}>
|
||||||
|
<PaletteTransition>
|
||||||
|
<Dialog.Panel className="mx-auto max-w-2xl transform divide-y divide-gray-500 divide-opacity-10 overflow-hidden rounded-xl bg-white bg-opacity-80 shadow-2xl ring-1 ring-black ring-opacity-5 backdrop-blur backdrop-filter transition-all">
|
||||||
|
<Combobox<Command | string>
|
||||||
|
onChange={(input) => {
|
||||||
|
if (typeof input === "string") {
|
||||||
|
setQuery(input);
|
||||||
|
} else if (input.action) {
|
||||||
|
setOpen(false);
|
||||||
|
input.action();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SearchBox setQuery={setQuery} />
|
||||||
|
|
||||||
|
{query === "" || filtered.length > 0 ? (
|
||||||
|
<Combobox.Options
|
||||||
|
static
|
||||||
|
className="max-h-80 scroll-py-2 divide-y divide-gray-500 divide-opacity-10 overflow-y-auto"
|
||||||
|
>
|
||||||
|
<li className="p-2">
|
||||||
|
<ul className="flex flex-col gap-2 text-sm text-gray-700">
|
||||||
|
{filtered.map((group, index) => (
|
||||||
|
<SearchResult key={index} group={group} />
|
||||||
|
))}
|
||||||
|
{query === "" &&
|
||||||
|
groups.map((group, index) => (
|
||||||
|
<GroupView key={index} group={group} />
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</Combobox.Options>
|
||||||
|
) : (
|
||||||
|
query !== "" && filtered.length === 0 && <NoResults />
|
||||||
|
)}
|
||||||
|
</Combobox>
|
||||||
|
</Dialog.Panel>
|
||||||
|
</PaletteTransition>
|
||||||
|
</Dialog>
|
||||||
|
</Transition.Root>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import type React from "react";
|
||||||
|
|
||||||
|
import { CommandLineIcon } from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
|
import { Mono } from "../Mono.js";
|
||||||
|
|
||||||
|
export const NoResults = (): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<div className="py-14 px-14 text-center">
|
||||||
|
<CommandLineIcon className="mx-auto h-6 text-slate-500" />
|
||||||
|
<Mono className="tracking-tighter">
|
||||||
|
Query does not match any avaliable commands
|
||||||
|
</Mono>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import type React from "react";
|
||||||
|
import { Fragment } from "react";
|
||||||
|
|
||||||
|
import { Transition } from "@headlessui/react";
|
||||||
|
|
||||||
|
export interface PaletteTransitionProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PaletteTransition = ({
|
||||||
|
children,
|
||||||
|
}: PaletteTransitionProps): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Transition.Child
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<div className="fixed inset-0 bg-gray-500 bg-opacity-25 transition-opacity" />
|
||||||
|
</Transition.Child>
|
||||||
|
|
||||||
|
<div className="fixed inset-0 z-10 overflow-y-auto p-4 sm:p-6 md:p-20">
|
||||||
|
<Transition.Child
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0 scale-95"
|
||||||
|
enterTo="opacity-100 scale-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100 scale-100"
|
||||||
|
leaveTo="opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import type React from "react";
|
||||||
|
|
||||||
|
import { Combobox } from "@headlessui/react";
|
||||||
|
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
|
export interface SearchBoxProps {
|
||||||
|
setQuery: (query: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SearchBox = ({ setQuery }: SearchBoxProps): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
<MagnifyingGlassIcon className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-gray-900 text-opacity-40" />
|
||||||
|
<Combobox.Input
|
||||||
|
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-sm text-gray-900 placeholder-gray-500 focus:ring-0"
|
||||||
|
placeholder="Search..."
|
||||||
|
onChange={(event) => setQuery(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import type React from "react";
|
||||||
|
|
||||||
|
import { Combobox } from "@headlessui/react";
|
||||||
|
import { ChevronRightIcon } from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
|
import type { Group } from "./Index.js";
|
||||||
|
|
||||||
|
export interface SearchResultProps {
|
||||||
|
group: Group;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SearchResult = ({ group }: SearchResultProps): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<div className="rounded-md border border-gray-300 py-2 shadow-md">
|
||||||
|
<div className="flex items-center px-3 py-2">
|
||||||
|
<group.icon className="h-6 w-6 flex-none text-gray-900 text-opacity-40" />
|
||||||
|
<span className="ml-3 flex-auto truncate">{group.name}</span>
|
||||||
|
</div>
|
||||||
|
{group.commands.map((command, index) => (
|
||||||
|
<Combobox.Option
|
||||||
|
key={index}
|
||||||
|
value={command}
|
||||||
|
className={({ active }) =>
|
||||||
|
`mr-2 ml-4 flex cursor-pointer select-none items-center rounded-md px-3 py-1 ${
|
||||||
|
active ? "bg-gray-900 bg-opacity-5 text-gray-900" : ""
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{({ active }) => (
|
||||||
|
<>
|
||||||
|
<command.icon
|
||||||
|
className={`h-4 flex-none text-gray-900 text-opacity-40 ${
|
||||||
|
active ? "text-opacity-100" : ""
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
<span className="ml-3">{command.name}</span>
|
||||||
|
{active && (
|
||||||
|
<ChevronRightIcon className="ml-auto h-4 text-gray-400" />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Combobox.Option>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -3,7 +3,7 @@ import type React from "react";
|
|||||||
import { useAppStore } from "@app/core/stores/appStore.js";
|
import { useAppStore } from "@app/core/stores/appStore.js";
|
||||||
import { useDeviceStore } from "@app/core/stores/deviceStore.js";
|
import { useDeviceStore } from "@app/core/stores/deviceStore.js";
|
||||||
import { Hashicon } from "@emeraldpay/hashicon-react";
|
import { Hashicon } from "@emeraldpay/hashicon-react";
|
||||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
import { CommandLineIcon, PlusIcon } from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
import { Mono } from "./Mono.js";
|
import { Mono } from "./Mono.js";
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ export const DeviceSelector = (): JSX.Element => {
|
|||||||
const { selectedDevice, setSelectedDevice } = useAppStore();
|
const { selectedDevice, setSelectedDevice } = useAppStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-16 items-center whitespace-nowrap bg-slate-50 py-12 [writing-mode:vertical-rl]">
|
<div className="flex h-full w-16 items-center whitespace-nowrap bg-slate-50 pt-12 [writing-mode:vertical-rl]">
|
||||||
<Mono>Connected Devices</Mono>
|
<Mono>Connected Devices</Mono>
|
||||||
<span className="mt-6 flex gap-4 font-bold text-slate-900">
|
<span className="mt-6 flex gap-4 font-bold text-slate-900">
|
||||||
{getDevices().map((device) => (
|
{getDevices().map((device) => (
|
||||||
@@ -45,6 +45,13 @@ export const DeviceSelector = (): JSX.Element => {
|
|||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
<img src="Logo_Black.svg" className="mt-auto px-3" />
|
<img src="Logo_Black.svg" className="mt-auto px-3" />
|
||||||
|
<div className="my-4 flex flex-col gap-2 [writing-mode:horizontal-tb]">
|
||||||
|
<CommandLineIcon className="h-6 text-slate-400" />
|
||||||
|
<Mono className="text-xs">
|
||||||
|
<kbd className="rounded-md bg-slate-200 p-0.5 pr-1 italic">Ctrl</kbd>+
|
||||||
|
<kbd className="rounded-md bg-slate-200 p-0.5 pr-1 italic">K</kbd>
|
||||||
|
</Mono>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import type React from "react";
|
||||||
|
|
||||||
|
import { useDevice } from "@app/core/providers/useDevice.js";
|
||||||
|
|
||||||
|
import { QRDialog } from "./QRDialog.js";
|
||||||
|
|
||||||
|
export const DialogManager = (): JSX.Element => {
|
||||||
|
const { channels, config, QRDialogOpen, setQRDialogOpen } = useDevice();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<QRDialog
|
||||||
|
isOpen={QRDialogOpen}
|
||||||
|
close={() => {
|
||||||
|
setQRDialogOpen(false);
|
||||||
|
}}
|
||||||
|
channels={channels.map((ch) => ch.config)}
|
||||||
|
loraConfig={config.lora}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -64,38 +64,40 @@ export const QRDialog = ({
|
|||||||
<span className="text-sm text-slate-600">
|
<span className="text-sm text-slate-600">
|
||||||
The current LoRa configuration will also be shared.
|
The current LoRa configuration will also be shared.
|
||||||
</span>
|
</span>
|
||||||
{channels.map((channel) => (
|
<div className="flex flex-col gap-1">
|
||||||
<Checkbox
|
{channels.map((channel) => (
|
||||||
key={channel.index}
|
<Checkbox
|
||||||
disabled={
|
key={channel.index}
|
||||||
channel.index === 0 ||
|
disabled={
|
||||||
channel.role === Protobuf.Channel_Role.DISABLED
|
channel.index === 0 ||
|
||||||
}
|
channel.role === Protobuf.Channel_Role.DISABLED
|
||||||
label={
|
|
||||||
channel.settings?.name.length
|
|
||||||
? channel.settings.name
|
|
||||||
: channel.role === Protobuf.Channel_Role.PRIMARY
|
|
||||||
? "Primary"
|
|
||||||
: `Channel: ${channel.index}`
|
|
||||||
}
|
|
||||||
checked={
|
|
||||||
channel.index === 0 ||
|
|
||||||
selectedChannels.includes(channel.index)
|
|
||||||
}
|
|
||||||
onChange={() => {
|
|
||||||
if (selectedChannels.includes(channel.index)) {
|
|
||||||
setSelectedChannels(
|
|
||||||
selectedChannels.filter((c) => c !== channel.index)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
setSelectedChannels([
|
|
||||||
...selectedChannels,
|
|
||||||
channel.index,
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}}
|
label={
|
||||||
/>
|
channel.settings?.name.length
|
||||||
))}
|
? channel.settings.name
|
||||||
|
: channel.role === Protobuf.Channel_Role.PRIMARY
|
||||||
|
? "Primary"
|
||||||
|
: `Channel: ${channel.index}`
|
||||||
|
}
|
||||||
|
checked={
|
||||||
|
channel.index === 0 ||
|
||||||
|
selectedChannels.includes(channel.index)
|
||||||
|
}
|
||||||
|
onChange={() => {
|
||||||
|
if (selectedChannels.includes(channel.index)) {
|
||||||
|
setSelectedChannels(
|
||||||
|
selectedChannels.filter((c) => c !== channel.index)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setSelectedChannels([
|
||||||
|
...selectedChannels,
|
||||||
|
channel.index,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-grow flex-col">
|
<div className="flex flex-grow flex-col">
|
||||||
<div className="m-auto flex">
|
<div className="m-auto flex">
|
||||||
@@ -107,7 +109,7 @@ export const QRDialog = ({
|
|||||||
value={QRCodeURL}
|
value={QRCodeURL}
|
||||||
action={{
|
action={{
|
||||||
icon: <ClipboardIcon className="h-4" />,
|
icon: <ClipboardIcon className="h-4" />,
|
||||||
action: () => {
|
action() {
|
||||||
console.log("");
|
console.log("");
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -3,39 +3,20 @@ import { forwardRef, InputHTMLAttributes } from "react";
|
|||||||
|
|
||||||
export interface CheckboxProps extends InputHTMLAttributes<HTMLInputElement> {
|
export interface CheckboxProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||||
label: string;
|
label: string;
|
||||||
description?: string;
|
|
||||||
options?: string[];
|
|
||||||
prefix?: string;
|
|
||||||
suffix?: string;
|
|
||||||
action?: {
|
|
||||||
icon: JSX.Element;
|
|
||||||
action: () => void;
|
|
||||||
};
|
|
||||||
error?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
|
export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
|
||||||
function Input(
|
function Input({ label, disabled, ...rest }: CheckboxProps, ref) {
|
||||||
{
|
|
||||||
label,
|
|
||||||
description,
|
|
||||||
options,
|
|
||||||
prefix,
|
|
||||||
suffix,
|
|
||||||
action,
|
|
||||||
error,
|
|
||||||
children,
|
|
||||||
...rest
|
|
||||||
}: CheckboxProps,
|
|
||||||
ref
|
|
||||||
) {
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex items-start">
|
<div className="relative flex items-start">
|
||||||
<div className="flex h-5 items-center">
|
<div className="flex h-5 items-center">
|
||||||
<input
|
<input
|
||||||
ref={ref}
|
ref={ref}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
className={`h-4 w-4 rounded border-transparent bg-orange-100 text-orange-500 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-orange-500 ${
|
||||||
|
disabled ? "cursor-not-allowed text-orange-200" : ""
|
||||||
|
}`}
|
||||||
|
disabled={disabled}
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -43,7 +24,6 @@ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
|
|||||||
<label htmlFor="comments" className="font-medium text-gray-700">
|
<label htmlFor="comments" className="font-medium text-gray-700">
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
<p className="text-gray-500">{description}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
|
|||||||
)}
|
)}
|
||||||
<input
|
<input
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={`flex h-10 w-full rounded-md bg-orange-100 px-3 text-sm focus:outline-none focus:ring-2 focus:ring-orange-500 ${
|
className={`flex h-10 w-full rounded-md border-transparent bg-orange-100 px-3 text-sm focus:border-transparent focus:outline-none focus:ring-2 focus:ring-orange-500 ${
|
||||||
prefix ? "rounded-l-none" : ""
|
prefix ? "rounded-l-none" : ""
|
||||||
} ${action ? "rounded-r-none" : ""}`}
|
} ${action ? "rounded-r-none" : ""}`}
|
||||||
{...rest}
|
{...rest}
|
||||||
@@ -46,7 +46,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={action.action}
|
onClick={action.action}
|
||||||
className="relative -ml-px inline-flex items-center space-x-2 rounded-r-md bg-orange-200 px-4 py-2 text-sm font-medium hover:bg-orange-300 focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-orange-500"
|
className="relative -ml-px inline-flex items-center space-x-2 rounded-r-md bg-orange-200 px-4 py-2 text-sm font-medium hover:bg-orange-300 focus:outline-none focus:ring-2 focus:ring-orange-500"
|
||||||
>
|
>
|
||||||
{action.icon}
|
{action.icon}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export const Select = forwardRef<HTMLSelectElement, SelectProps>(function Input(
|
|||||||
<div className="flex">
|
<div className="flex">
|
||||||
<select
|
<select
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={`flex h-10 w-full rounded-md bg-orange-100 px-3 text-sm focus:outline-none focus:ring-2 focus:ring-orange-500 ${
|
className={`flex h-10 w-full rounded-md border-transparent bg-orange-100 px-3 text-sm focus:border-transparent focus:outline-none focus:ring-2 focus:ring-orange-500 ${
|
||||||
prefix ? "rounded-l-none" : ""
|
prefix ? "rounded-l-none" : ""
|
||||||
} ${action ? "rounded-r-none" : ""} ${
|
} ${action ? "rounded-r-none" : ""} ${
|
||||||
disabled ? "cursor-not-allowed" : ""
|
disabled ? "cursor-not-allowed" : ""
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ export const Toggle = ({
|
|||||||
} ${disabled ? "cursor-not-allowed bg-orange-400" : ""}`}
|
} ${disabled ? "cursor-not-allowed bg-orange-400" : ""}`}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
aria-hidden="true"
|
|
||||||
className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
|
className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
|
||||||
checked ? "translate-x-5" : "translate-x-0"
|
checked ? "translate-x-5" : "translate-x-0"
|
||||||
}`}
|
}`}
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export interface Device {
|
|||||||
waypoints: Protobuf.Waypoint[];
|
waypoints: Protobuf.Waypoint[];
|
||||||
regionUnset: boolean;
|
regionUnset: boolean;
|
||||||
currentMetrics: Protobuf.DeviceMetrics;
|
currentMetrics: Protobuf.DeviceMetrics;
|
||||||
|
QRDialogOpen: boolean;
|
||||||
|
|
||||||
setReady(ready: boolean): void;
|
setReady(ready: boolean): void;
|
||||||
setStatus: (status: Types.DeviceStatusEnum) => void;
|
setStatus: (status: Types.DeviceStatusEnum) => void;
|
||||||
@@ -72,6 +73,7 @@ export interface Device {
|
|||||||
addMessage: (message: MessageWithAck) => void;
|
addMessage: (message: MessageWithAck) => void;
|
||||||
addWaypointMessage: (message: WaypointIDWithAck) => void;
|
addWaypointMessage: (message: WaypointIDWithAck) => void;
|
||||||
ackMessage: (channelIndex: number, messageId: number) => void;
|
ackMessage: (channelIndex: number, messageId: number) => void;
|
||||||
|
setQRDialogOpen: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeviceState {
|
export interface DeviceState {
|
||||||
@@ -107,6 +109,7 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
|
|||||||
waypoints: [],
|
waypoints: [],
|
||||||
regionUnset: false,
|
regionUnset: false,
|
||||||
currentMetrics: Protobuf.DeviceMetrics.create(),
|
currentMetrics: Protobuf.DeviceMetrics.create(),
|
||||||
|
QRDialogOpen: false,
|
||||||
|
|
||||||
setReady: (ready: boolean) => {
|
setReady: (ready: boolean) => {
|
||||||
set(
|
set(
|
||||||
@@ -475,6 +478,16 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
setQRDialogOpen: (open: boolean) => {
|
||||||
|
set(
|
||||||
|
produce<DeviceState>((draft) => {
|
||||||
|
const device = draft.devices.get(id);
|
||||||
|
if (device) {
|
||||||
|
device.QRDialogOpen = open;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
+17
-30
@@ -1,17 +1,14 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
import { Channel } from "@app/components/PageComponents/Channel.js";
|
import { Channel } from "@app/components/PageComponents/Channel.js";
|
||||||
import { Button } from "@components/Button.js";
|
import { Button } from "@components/Button.js";
|
||||||
import { QRDialog } from "@components/Dialog/QRDialog.js";
|
|
||||||
import { TabbedContent, TabType } from "@components/layout/page/TabbedContent";
|
import { TabbedContent, TabType } from "@components/layout/page/TabbedContent";
|
||||||
import { useDevice } from "@core/providers/useDevice.js";
|
import { useDevice } from "@core/providers/useDevice.js";
|
||||||
import { QrCodeIcon } from "@heroicons/react/24/outline";
|
import { QrCodeIcon } from "@heroicons/react/24/outline";
|
||||||
import { Protobuf } from "@meshtastic/meshtasticjs";
|
import { Protobuf } from "@meshtastic/meshtasticjs";
|
||||||
|
|
||||||
export const ChannelsPage = (): JSX.Element => {
|
export const ChannelsPage = (): JSX.Element => {
|
||||||
const { channels, config } = useDevice();
|
const { channels, setQRDialogOpen } = useDevice();
|
||||||
const [QRDialogOpen, setQRDialogOpen] = useState(false);
|
|
||||||
|
|
||||||
const tabs: TabType[] = channels.map((channel) => {
|
const tabs: TabType[] = channels.map((channel) => {
|
||||||
return {
|
return {
|
||||||
@@ -25,31 +22,21 @@ export const ChannelsPage = (): JSX.Element => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<TabbedContent
|
||||||
<QRDialog
|
tabs={tabs}
|
||||||
isOpen={QRDialogOpen}
|
actions={[
|
||||||
close={() => {
|
() => (
|
||||||
setQRDialogOpen(false);
|
<Button
|
||||||
}}
|
variant="secondary"
|
||||||
channels={channels.map((ch) => ch.config)}
|
iconBefore={<QrCodeIcon className="w-4" />}
|
||||||
loraConfig={config.lora}
|
onClick={() => {
|
||||||
/>
|
setQRDialogOpen(true);
|
||||||
<TabbedContent
|
}}
|
||||||
tabs={tabs}
|
>
|
||||||
actions={[
|
QR Code
|
||||||
() => (
|
</Button>
|
||||||
<Button
|
),
|
||||||
variant="secondary"
|
]}
|
||||||
iconBefore={<QrCodeIcon className="w-4" />}
|
/>
|
||||||
onClick={() => {
|
|
||||||
setQRDialogOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
QR Code
|
|
||||||
</Button>
|
|
||||||
),
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+1
-1
@@ -17,5 +17,5 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
extend: {},
|
extend: {},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [require("@tailwindcss/forms")],
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user