19 changed files with 552 additions and 94 deletions
@ -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[email protected][email protected] |
zustand: 4.1[email protected][email protected] |
||||
|
|
||||
devDependencies: |
devDependencies: |
||||
|
'@tailwindcss/forms': 0.5[email protected] |
||||
'@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/[email protected]: |
||||
|
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[email protected] |
||||
|
dev: true |
||||
|
|
||||
/@tailwindcss/line-clamp/[email protected]: |
/@tailwindcss/line-clamp/[email protected]: |
||||
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: |
||||
|
|||||
@ -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> |
||||
|
); |
||||
|
}; |
||||
@ -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} |
||||
|
/> |
||||
|
</> |
||||
|
); |
||||
|
}; |
||||
Loading…
Reference in new issue