69 changed files with 1744 additions and 1680 deletions
File diff suppressed because it is too large
@ -1,30 +0,0 @@ |
|||
import type { Group } from "@components/CommandPalette/Index.js"; |
|||
import { Combobox } from "@headlessui/react"; |
|||
import { ChevronRightIcon } from "lucide-react"; |
|||
|
|||
export interface GroupViewProps { |
|||
group: Group; |
|||
} |
|||
|
|||
export const GroupView = ({ group }: GroupViewProps): JSX.Element => { |
|||
return ( |
|||
<Combobox.Option |
|||
value={group.label} |
|||
className={({ active }) => |
|||
`flex cursor-default select-none items-center rounded-md px-3 py-2 ${ |
|||
active ? "bg-backgroundPrimary text-textPrimary" : "" |
|||
}` |
|||
} |
|||
> |
|||
{({ active }) => ( |
|||
<> |
|||
<group.icon size={20} /> |
|||
<span className="ml-3 flex-auto truncate">{group.label}</span> |
|||
{active && ( |
|||
<ChevronRightIcon size={16} className="text-textSecondary" /> |
|||
)} |
|||
</> |
|||
)} |
|||
</Combobox.Option> |
|||
); |
|||
}; |
|||
@ -1,13 +0,0 @@ |
|||
import { Mono } from "@components/generic/Mono.js"; |
|||
import { TerminalSquareIcon } from "lucide-react"; |
|||
|
|||
export const NoResults = (): JSX.Element => { |
|||
return ( |
|||
<div className="py-14 px-14 text-center"> |
|||
<TerminalSquareIcon className="mx-auto text-textSecondary" /> |
|||
<Mono className="tracking-tighter"> |
|||
Query does not match any avaliable commands |
|||
</Mono> |
|||
</div> |
|||
); |
|||
}; |
|||
@ -1,40 +0,0 @@ |
|||
import { Fragment, ReactNode } from "react"; |
|||
import { Transition } from "@headlessui/react"; |
|||
|
|||
export interface PaletteTransitionProps { |
|||
children: ReactNode; |
|||
} |
|||
|
|||
export const PaletteTransition = ({ |
|||
children |
|||
}: PaletteTransitionProps): JSX.Element => { |
|||
return ( |
|||
<> |
|||
<Transition.Child |
|||
as={Fragment} |
|||
enter="ease-out duration-200" |
|||
enterFrom="opacity-0" |
|||
enterTo="opacity-100" |
|||
leave="ease-in duration-100" |
|||
leaveFrom="opacity-100" |
|||
leaveTo="opacity-0" |
|||
> |
|||
<div className="bg-gray-500 fixed inset-0 bg-opacity-25 transition-opacity" /> |
|||
</Transition.Child> |
|||
|
|||
<div className="fixed inset-0 z-10 overflow-y-auto p-4 sm:p-6 md:p-20"> |
|||
<Transition.Child |
|||
as={Fragment} |
|||
enter="ease-out duration-200" |
|||
enterFrom="opacity-0 scale-95" |
|||
enterTo="opacity-100 scale-100" |
|||
leave="ease-in duration-100" |
|||
leaveFrom="opacity-100 scale-100" |
|||
leaveTo="opacity-0 scale-95" |
|||
> |
|||
{children} |
|||
</Transition.Child> |
|||
</div> |
|||
</> |
|||
); |
|||
}; |
|||
@ -1,22 +0,0 @@ |
|||
import { Combobox } from "@headlessui/react"; |
|||
import { SearchIcon } from "lucide-react"; |
|||
|
|||
export interface SearchBoxProps { |
|||
setQuery: (query: string) => void; |
|||
} |
|||
|
|||
export const SearchBox = ({ setQuery }: SearchBoxProps): JSX.Element => { |
|||
return ( |
|||
<div className="relative"> |
|||
<SearchIcon |
|||
size={20} |
|||
className="pointer-events-none absolute m-3.5 text-textSecondary" |
|||
/> |
|||
<Combobox.Input |
|||
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-sm text-textPrimary placeholder-textSecondary focus:ring-0" |
|||
placeholder="Search..." |
|||
onChange={(event) => setQuery(event.target.value)} |
|||
/> |
|||
</div> |
|||
); |
|||
}; |
|||
@ -1,80 +0,0 @@ |
|||
import type { Group } from "@components/CommandPalette/Index.js"; |
|||
import { Combobox } from "@headlessui/react"; |
|||
import { ChevronRightIcon } from "lucide-react"; |
|||
|
|||
export interface SearchResultProps { |
|||
group: Group; |
|||
} |
|||
|
|||
export const SearchResult = ({ group }: SearchResultProps): JSX.Element => { |
|||
return ( |
|||
<div className="rounded-md border-2 border-backgroundPrimary py-2"> |
|||
<div className="flex items-center px-3 py-2"> |
|||
<group.icon |
|||
size={16} |
|||
className="flex-none text-gray-900 text-opacity-40" |
|||
/> |
|||
<span className="ml-3 flex-auto truncate">{group.label}</span> |
|||
</div> |
|||
{group.commands.map((command, index) => ( |
|||
<div key={index}> |
|||
<Combobox.Option |
|||
value={command} |
|||
className={({ active }) => |
|||
`mr-2 ml-4 flex cursor-pointer select-none items-center rounded-md px-3 py-1 ${ |
|||
active ? "bg-backgroundPrimary 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.label}</span> |
|||
{active && ( |
|||
<ChevronRightIcon |
|||
size={16} |
|||
className="ml-auto text-gray-400" |
|||
/> |
|||
)} |
|||
</> |
|||
)} |
|||
</Combobox.Option> |
|||
{command.subItems && ( |
|||
<div className=" ml-9 border-l"> |
|||
{command.subItems?.map((item, index) => ( |
|||
<Combobox.Option |
|||
key={index} |
|||
value={item} |
|||
className={({ active }) => |
|||
`mx-2 flex cursor-pointer select-none items-center rounded-md px-3 py-1 ${ |
|||
active |
|||
? "bg-backgroundPrimary bg-opacity-5 text-gray-900" |
|||
: "" |
|||
}` |
|||
} |
|||
> |
|||
{({ active }) => ( |
|||
<> |
|||
{item.icon} |
|||
<span className="ml-3">{item.label}</span> |
|||
{active && ( |
|||
<ChevronRightIcon |
|||
size={16} |
|||
className="ml-auto text-gray-400" |
|||
/> |
|||
)} |
|||
</> |
|||
)} |
|||
</Combobox.Option> |
|||
))} |
|||
</div> |
|||
)} |
|||
</div> |
|||
))} |
|||
</div> |
|||
); |
|||
}; |
|||
@ -0,0 +1,104 @@ |
|||
import { useAppStore } from "@app/core/stores/appStore.js"; |
|||
import { Button } from "@components/UI/Button.js"; |
|||
import { |
|||
PlusIcon, |
|||
ListPlusIcon, |
|||
UsersIcon, |
|||
MapPinIcon, |
|||
CalendarIcon, |
|||
BluetoothIcon, |
|||
UsbIcon, |
|||
NetworkIcon |
|||
} from "lucide-react"; |
|||
import { Subtle } from "./UI/Typography/Subtle.js"; |
|||
import { H3 } from "./UI/Typography/H3.js"; |
|||
import { useDeviceStore } from "@app/core/stores/deviceStore.js"; |
|||
import { useMemo } from "react"; |
|||
import { Separator } from "./UI/Seperator.js"; |
|||
|
|||
export const Dashboard = () => { |
|||
const { setConnectDialogOpen } = useAppStore(); |
|||
const { getDevices } = useDeviceStore(); |
|||
|
|||
const devices = useMemo(() => getDevices(), [getDevices]); |
|||
|
|||
return ( |
|||
<div className="flex flex-col gap-3 p-3"> |
|||
<div className="flex items-center justify-between"> |
|||
<div className="space-y-1"> |
|||
<H3>Connected Devices</H3> |
|||
<Subtle>Manage, connect and disconnect devices</Subtle> |
|||
</div> |
|||
</div> |
|||
|
|||
<Separator /> |
|||
|
|||
<div className="flex h-[450px] rounded-md border border-dashed border-slate-200 p-3 dark:border-slate-700"> |
|||
{devices.length ? ( |
|||
<ul role="list" className="grow divide-y divide-gray-200"> |
|||
{devices.map((device) => { |
|||
return ( |
|||
<li key={device.id}> |
|||
<div className="px-4 py-4 sm:px-6"> |
|||
<div className="flex items-center justify-between"> |
|||
<p className="truncate text-sm font-medium text-accent"> |
|||
{device.nodes.filter( |
|||
(n) => n.data.num === device.hardware.myNodeNum |
|||
)[0]?.data.user?.longName ?? "UNK"} |
|||
</p> |
|||
<div className="inline-flex w-24 justify-center gap-2 rounded-full bg-slate-100 py-1 text-xs font-semibold text-slate-900 transition-colors hover:bg-slate-700 hover:text-slate-50"> |
|||
{device.connection?.connType === "ble" && ( |
|||
<> |
|||
<BluetoothIcon size={16} /> |
|||
BLE |
|||
</> |
|||
)} |
|||
{device.connection?.connType === "serial" && ( |
|||
<> |
|||
<UsbIcon size={16} /> |
|||
Serial |
|||
</> |
|||
)} |
|||
{device.connection?.connType === "http" && ( |
|||
<> |
|||
<NetworkIcon size={16} /> |
|||
Network |
|||
</> |
|||
)} |
|||
</div> |
|||
</div> |
|||
<div className="mt-2 sm:flex sm:justify-between"> |
|||
<div className="flex gap-2 text-sm text-gray-500"> |
|||
<UsersIcon |
|||
size={20} |
|||
className="text-gray-400" |
|||
aria-hidden="true" |
|||
/> |
|||
{device.nodes.length === 0 |
|||
? 0 |
|||
: device.nodes.length - 1} |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</li> |
|||
); |
|||
})} |
|||
</ul> |
|||
) : ( |
|||
<div className="m-auto flex flex-col gap-3 text-center"> |
|||
<ListPlusIcon size={48} className="mx-auto text-textSecondary" /> |
|||
<H3>No Devices</H3> |
|||
<Subtle>Connect atleast one device to get started</Subtle> |
|||
<Button |
|||
className="gap-2" |
|||
onClick={() => setConnectDialogOpen(true)} |
|||
> |
|||
<PlusIcon size={16} /> |
|||
New Connection |
|||
</Button> |
|||
</div> |
|||
)} |
|||
</div> |
|||
</div> |
|||
); |
|||
}; |
|||
@ -0,0 +1,227 @@ |
|||
import { |
|||
Controller, |
|||
DeepPartial, |
|||
FieldValues, |
|||
Path, |
|||
SubmitHandler, |
|||
useForm |
|||
} from "react-hook-form"; |
|||
import { Input } from "./UI/Input.js"; |
|||
import { Label } from "./UI/Label.js"; |
|||
import { ErrorMessage } from "@hookform/error-message"; |
|||
import { |
|||
Select, |
|||
SelectContent, |
|||
SelectItem, |
|||
SelectTrigger, |
|||
SelectValue |
|||
} from "./UI/Select.js"; |
|||
import { Switch } from "./UI/Switch.js"; |
|||
import { H4 } from "./UI/Typography/H4.js"; |
|||
import { Subtle } from "./UI/Typography/Subtle.js"; |
|||
|
|||
interface DisabledBy<T> { |
|||
fieldName: Path<T>; |
|||
selector?: number; |
|||
invert?: boolean; |
|||
} |
|||
|
|||
interface BasicFieldProps<T> { |
|||
name: Path<T>; |
|||
label: string; |
|||
description?: string; |
|||
active?: boolean; |
|||
required?: boolean; |
|||
disabledBy?: DisabledBy<T>[]; |
|||
} |
|||
|
|||
interface InputFieldProps<T> extends BasicFieldProps<T> { |
|||
type: "text" | "number" | "password" | "textarea"; |
|||
suffix?: string; |
|||
} |
|||
|
|||
interface SelectFieldProps<T> extends BasicFieldProps<T> { |
|||
type: "select"; |
|||
|
|||
enumValue: { |
|||
[s: string]: string | number; |
|||
}; |
|||
formatEnumName?: boolean; |
|||
} |
|||
|
|||
interface ToggleFieldProps<T> extends BasicFieldProps<T> { |
|||
type: "toggle"; |
|||
} |
|||
|
|||
export interface FormProps<T extends FieldValues> { |
|||
onSubmit: SubmitHandler<T>; |
|||
defaultValues?: DeepPartial<T>; |
|||
fieldGroups: { |
|||
label: string; |
|||
description: string; |
|||
fields: (InputFieldProps<T> | SelectFieldProps<T> | ToggleFieldProps<T>)[]; |
|||
}[]; |
|||
} |
|||
|
|||
export function DynamicForm<T extends FieldValues>({ |
|||
fieldGroups, |
|||
onSubmit, |
|||
defaultValues |
|||
}: FormProps<T>) { |
|||
const { register, handleSubmit, control, getValues } = useForm<T>({ |
|||
mode: "onChange", |
|||
defaultValues: defaultValues |
|||
}); |
|||
|
|||
const isDisabled = (disabledBy?: DisabledBy<T>[]): boolean => { |
|||
if (!disabledBy) return false; |
|||
|
|||
return disabledBy.some((field) => { |
|||
const value = getValues(field.fieldName); |
|||
return ( |
|||
(typeof value === "boolean" && field.invert ? value : !value) || |
|||
(typeof value === "number" && |
|||
(field.selector && field.invert |
|||
? !field.selector === value |
|||
: field.selector === value)) |
|||
); |
|||
}); |
|||
}; |
|||
|
|||
return ( |
|||
<form |
|||
className="space-y-8 divide-y divide-gray-200" |
|||
onChange={handleSubmit(onSubmit)} |
|||
> |
|||
{fieldGroups.map((fieldGroup) => ( |
|||
<div className="space-y-8 divide-y divide-gray-200 sm:space-y-5"> |
|||
<div> |
|||
<H4 className="font-medium">{fieldGroup.label}</H4> |
|||
<Subtle>{fieldGroup.description}</Subtle> |
|||
</div> |
|||
|
|||
{fieldGroup.fields.map((field) => { |
|||
const fieldWrapperData: FieldWrapperProps = { |
|||
label: field.label, |
|||
description: field.description, |
|||
disabled: isDisabled(field.disabledBy) |
|||
}; |
|||
|
|||
switch (field.type) { |
|||
case "text": |
|||
return ( |
|||
<FieldWrapper {...fieldWrapperData}> |
|||
<Input |
|||
disabled={fieldWrapperData.disabled} |
|||
{...register(field.name)} |
|||
/> |
|||
</FieldWrapper> |
|||
); |
|||
case "number": |
|||
return ( |
|||
<FieldWrapper {...fieldWrapperData}> |
|||
<Input |
|||
type="number" |
|||
disabled={fieldWrapperData.disabled} |
|||
{...register(field.name)} |
|||
/> |
|||
</FieldWrapper> |
|||
); |
|||
case "toggle": |
|||
return ( |
|||
<FieldWrapper {...fieldWrapperData}> |
|||
<Controller |
|||
name={field.name} |
|||
control={control} |
|||
render={({ field: { value, onChange, ...rest } }) => ( |
|||
<Switch |
|||
checked={value} |
|||
onCheckedChange={onChange} |
|||
disabled={fieldWrapperData.disabled} |
|||
{...rest} |
|||
/> |
|||
)} |
|||
/> |
|||
</FieldWrapper> |
|||
); |
|||
case "select": |
|||
const optionsEnumValues = field.enumValue |
|||
? Object.entries(field.enumValue).filter( |
|||
(value) => typeof value[1] === "number" |
|||
) |
|||
: []; |
|||
return ( |
|||
<FieldWrapper {...fieldWrapperData}> |
|||
<Controller |
|||
name={field.name} |
|||
control={control} |
|||
render={({ field: { value, onChange, ...rest } }) => ( |
|||
<Select |
|||
onValueChange={(e) => onChange(parseInt(e))} |
|||
disabled={fieldWrapperData.disabled} |
|||
value={value?.toString()} |
|||
{...rest} |
|||
> |
|||
<SelectTrigger> |
|||
<SelectValue /> |
|||
</SelectTrigger> |
|||
<SelectContent> |
|||
{optionsEnumValues.map(([name, value], index) => ( |
|||
<SelectItem key={index} value={value.toString()}> |
|||
{field.formatEnumName |
|||
? name |
|||
.replace(/_/g, " ") |
|||
.toLowerCase() |
|||
.split(" ") |
|||
.map( |
|||
(s) => |
|||
s.charAt(0).toUpperCase() + |
|||
s.substring(1) |
|||
) |
|||
.join(" ") |
|||
: name} |
|||
</SelectItem> |
|||
))} |
|||
</SelectContent> |
|||
</Select> |
|||
)} |
|||
/> |
|||
</FieldWrapper> |
|||
); |
|||
} |
|||
})} |
|||
</div> |
|||
))} |
|||
</form> |
|||
); |
|||
} |
|||
|
|||
interface FieldWrapperProps { |
|||
label: string; |
|||
description?: string; |
|||
disabled?: boolean; |
|||
children?: React.ReactNode; |
|||
} |
|||
|
|||
const FieldWrapper = ({ |
|||
label, |
|||
description, |
|||
disabled, |
|||
children |
|||
}: FieldWrapperProps): JSX.Element => ( |
|||
<div className="pt-6 sm:pt-5"> |
|||
<div role="group" aria-labelledby="label-notifications"> |
|||
<div className="sm:grid sm:grid-cols-3 sm:items-baseline sm:gap-4"> |
|||
<Label>{label}</Label> |
|||
<div className="sm:col-span-2"> |
|||
<div className="max-w-lg"> |
|||
<p className="text-sm text-gray-500">{description}</p> |
|||
<div className="mt-4 space-y-4"> |
|||
<div className="flex items-center">{children}</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
); |
|||
@ -1,57 +0,0 @@ |
|||
import { |
|||
Menubar, |
|||
MenubarContent, |
|||
MenubarItem, |
|||
MenubarMenu, |
|||
MenubarSeparator, |
|||
MenubarShortcut, |
|||
MenubarTrigger |
|||
} from "@app/components/UI/Menubar.js"; |
|||
import { useAppStore } from "@app/core/stores/appStore.js"; |
|||
|
|||
export const Menu = (): JSX.Element => { |
|||
const { darkMode } = useAppStore(); |
|||
return ( |
|||
<Menubar className="rounded-none dark:bg-slate-900"> |
|||
<MenubarMenu> |
|||
<MenubarTrigger> |
|||
<img |
|||
src={darkMode ? "Logo_White.svg" : "Logo_Black.svg"} |
|||
className="h-4" |
|||
/> |
|||
</MenubarTrigger> |
|||
<MenubarContent> |
|||
<MenubarItem>About Music</MenubarItem> |
|||
<MenubarSeparator /> |
|||
<MenubarItem> |
|||
Preferences... <MenubarShortcut>⌘,</MenubarShortcut> |
|||
</MenubarItem> |
|||
<MenubarSeparator /> |
|||
<MenubarItem> |
|||
Hide Music... <MenubarShortcut>⌘H</MenubarShortcut> |
|||
</MenubarItem> |
|||
<MenubarItem> |
|||
Hide Others... <MenubarShortcut>⇧⌘H</MenubarShortcut> |
|||
</MenubarItem> |
|||
<MenubarShortcut /> |
|||
<MenubarItem> |
|||
Quit Music <MenubarShortcut>⌘Q</MenubarShortcut> |
|||
</MenubarItem> |
|||
</MenubarContent> |
|||
</MenubarMenu> |
|||
<MenubarMenu> |
|||
<MenubarTrigger>File</MenubarTrigger> |
|||
<MenubarContent> |
|||
<MenubarItem> |
|||
New Tab <MenubarShortcut>⌘T</MenubarShortcut> |
|||
</MenubarItem> |
|||
<MenubarItem>New Window</MenubarItem> |
|||
<MenubarSeparator /> |
|||
<MenubarItem>Share</MenubarItem> |
|||
<MenubarSeparator /> |
|||
<MenubarItem>Print</MenubarItem> |
|||
</MenubarContent> |
|||
</MenubarMenu> |
|||
</Menubar> |
|||
); |
|||
}; |
|||
@ -1,77 +0,0 @@ |
|||
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 { Tabs, TabsContent, TabsList, TabsTrigger } from "./UI/Tabs.js"; |
|||
import { Subtle } from "./UI/Typography/Subtle.js"; |
|||
import { Link } from "./UI/Typography/Link.js"; |
|||
|
|||
export const NewDevice = () => { |
|||
const tabs = [ |
|||
{ |
|||
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" |
|||
} |
|||
]; |
|||
|
|||
return ( |
|||
<div className="m-auto"> |
|||
<Tabs defaultValue="HTTP" className="w-[400px]"> |
|||
<TabsList> |
|||
{tabs.map((tab) => ( |
|||
<TabsTrigger value={tab.label} disabled={tab.disabled}> |
|||
{tab.label} |
|||
</TabsTrigger> |
|||
))} |
|||
</TabsList> |
|||
{tabs.map((tab) => ( |
|||
<TabsContent value={tab.label}> |
|||
{tab.disabled ? ( |
|||
<p className="text-sm text-slate-500 dark:text-slate-400"> |
|||
{tab.disabledMessage} |
|||
</p> |
|||
) : ( |
|||
<tab.element /> |
|||
)} |
|||
</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> |
|||
</> |
|||
)} |
|||
</div> |
|||
); |
|||
}; |
|||
@ -0,0 +1,156 @@ |
|||
import * as React from "react"; |
|||
import type { DialogProps } from "@radix-ui/react-dialog"; |
|||
import { Command as CommandPrimitive } from "cmdk"; |
|||
import { Search } from "lucide-react"; |
|||
|
|||
import { cn } from "@core/utils/cn.js"; |
|||
import { Dialog, DialogContent } from "@components/UI/Dialog.js"; |
|||
|
|||
const Command = React.forwardRef< |
|||
React.ElementRef<typeof CommandPrimitive>, |
|||
React.ComponentPropsWithoutRef<typeof CommandPrimitive> |
|||
>(({ className, ...props }, ref) => ( |
|||
<CommandPrimitive |
|||
ref={ref} |
|||
className={cn( |
|||
"flex h-full w-full flex-col overflow-hidden rounded-lg bg-white dark:bg-slate-800", |
|||
className |
|||
)} |
|||
{...props} |
|||
/> |
|||
)); |
|||
Command.displayName = CommandPrimitive.displayName; |
|||
|
|||
interface CommandDialogProps extends DialogProps {} |
|||
|
|||
const CommandDialog = ({ children, ...props }: CommandDialogProps) => { |
|||
return ( |
|||
<Dialog {...props}> |
|||
<DialogContent className="overflow-hidden p-0 shadow-2xl [&_[dialog-overlay]]:bg-red-100"> |
|||
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-slate-500 [&_[cmdk-group]]:px-2 [&_[cmdk-group]_+[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5"> |
|||
{children} |
|||
</Command> |
|||
</DialogContent> |
|||
</Dialog> |
|||
); |
|||
}; |
|||
|
|||
const CommandInput = React.forwardRef< |
|||
React.ElementRef<typeof CommandPrimitive.Input>, |
|||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input> |
|||
>(({ className, ...props }, ref) => ( |
|||
<div |
|||
className="flex items-center border-b border-b-slate-100 px-4 dark:border-b-slate-700" |
|||
cmdk-input-wrapper="" |
|||
> |
|||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" /> |
|||
<CommandPrimitive.Input |
|||
ref={ref} |
|||
className={cn( |
|||
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-slate-400 disabled:cursor-not-allowed disabled:opacity-50 dark:text-slate-50", |
|||
className |
|||
)} |
|||
{...props} |
|||
/> |
|||
</div> |
|||
)); |
|||
|
|||
CommandInput.displayName = CommandPrimitive.Input.displayName; |
|||
|
|||
const CommandList = React.forwardRef< |
|||
React.ElementRef<typeof CommandPrimitive.List>, |
|||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List> |
|||
>(({ className, ...props }, ref) => ( |
|||
<CommandPrimitive.List |
|||
ref={ref} |
|||
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)} |
|||
{...props} |
|||
/> |
|||
)); |
|||
|
|||
CommandList.displayName = CommandPrimitive.List.displayName; |
|||
|
|||
const CommandEmpty = React.forwardRef< |
|||
React.ElementRef<typeof CommandPrimitive.Empty>, |
|||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty> |
|||
>((props, ref) => ( |
|||
<CommandPrimitive.Empty |
|||
ref={ref} |
|||
className="py-6 text-center text-sm" |
|||
{...props} |
|||
/> |
|||
)); |
|||
|
|||
CommandEmpty.displayName = CommandPrimitive.Empty.displayName; |
|||
|
|||
const CommandGroup = React.forwardRef< |
|||
React.ElementRef<typeof CommandPrimitive.Group>, |
|||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group> |
|||
>(({ className, ...props }, ref) => ( |
|||
<CommandPrimitive.Group |
|||
ref={ref} |
|||
className={cn( |
|||
"overflow-hidden py-3 px-2 text-slate-700 dark:text-slate-400 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:pb-1.5 [&_[cmdk-group-heading]]:text-sm [&_[cmdk-group-heading]]:font-semibold [&_[cmdk-group-heading]]:text-slate-900 [&_[cmdk-group-heading]]:dark:text-slate-300", |
|||
className |
|||
)} |
|||
{...props} |
|||
/> |
|||
)); |
|||
|
|||
CommandGroup.displayName = CommandPrimitive.Group.displayName; |
|||
|
|||
const CommandSeparator = React.forwardRef< |
|||
React.ElementRef<typeof CommandPrimitive.Separator>, |
|||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator> |
|||
>(({ className, ...props }, ref) => ( |
|||
<CommandPrimitive.Separator |
|||
ref={ref} |
|||
className={cn("-mx-1 h-px bg-slate-100 dark:bg-slate-700", className)} |
|||
{...props} |
|||
/> |
|||
)); |
|||
CommandSeparator.displayName = CommandPrimitive.Separator.displayName; |
|||
|
|||
const CommandItem = React.forwardRef< |
|||
React.ElementRef<typeof CommandPrimitive.Item>, |
|||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item> |
|||
>(({ className, ...props }, ref) => ( |
|||
<CommandPrimitive.Item |
|||
ref={ref} |
|||
className={cn( |
|||
"relative flex cursor-default select-none items-center rounded-md py-1.5 px-2 text-sm font-medium outline-none aria-selected:bg-slate-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:aria-selected:bg-slate-700", |
|||
className |
|||
)} |
|||
{...props} |
|||
/> |
|||
)); |
|||
|
|||
CommandItem.displayName = CommandPrimitive.Item.displayName; |
|||
|
|||
const CommandShortcut = ({ |
|||
className, |
|||
...props |
|||
}: React.HTMLAttributes<HTMLSpanElement>) => { |
|||
return ( |
|||
<span |
|||
className={cn( |
|||
"ml-auto text-xs tracking-widest text-slate-500", |
|||
className |
|||
)} |
|||
{...props} |
|||
/> |
|||
); |
|||
}; |
|||
CommandShortcut.displayName = "CommandShortcut"; |
|||
|
|||
export { |
|||
Command, |
|||
CommandDialog, |
|||
CommandInput, |
|||
CommandList, |
|||
CommandEmpty, |
|||
CommandGroup, |
|||
CommandItem, |
|||
CommandShortcut, |
|||
CommandSeparator |
|||
}; |
|||
@ -0,0 +1,24 @@ |
|||
import * as React from "react"; |
|||
|
|||
import { cn } from "@core/utils/cn.js"; |
|||
|
|||
export interface InputProps |
|||
extends React.InputHTMLAttributes<HTMLInputElement> {} |
|||
|
|||
const Input = React.forwardRef<HTMLInputElement, InputProps>( |
|||
({ className, ...props }, ref) => { |
|||
return ( |
|||
<input |
|||
className={cn( |
|||
"flex h-10 w-full rounded-md border border-slate-300 bg-transparent py-2 px-3 text-sm placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-700 dark:text-slate-50 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900", |
|||
className |
|||
)} |
|||
ref={ref} |
|||
{...props} |
|||
/> |
|||
); |
|||
} |
|||
); |
|||
Input.displayName = "Input"; |
|||
|
|||
export { Input }; |
|||
@ -1,30 +0,0 @@ |
|||
import { cn } from "@app/core/utils/cn.js"; |
|||
import type { LucideIcon } from "lucide-react"; |
|||
|
|||
export interface SidebarItemProps { |
|||
icon?: LucideIcon; |
|||
element?: JSX.Element; |
|||
onClick?: () => void; |
|||
active?: boolean; |
|||
label: string; |
|||
} |
|||
|
|||
export const SidebarItem = ({ |
|||
icon: Icon, |
|||
element, |
|||
onClick, |
|||
active, |
|||
label |
|||
}: SidebarItemProps): JSX.Element => ( |
|||
<button |
|||
className={cn( |
|||
"text-palette-600 flex w-full cursor-pointer items-center space-x-2.5 rounded-xl bg-transparent px-2.5 py-2.5 transition-all duration-300 hover:text-accent", |
|||
active && "bg-backgroundPrimary text-accent" |
|||
)} |
|||
onClick={() => onClick && onClick()} |
|||
> |
|||
{element} |
|||
{Icon && <Icon size={16} />} |
|||
<span className="align-middle font-mono text-sm">{label}</span> |
|||
</button> |
|||
); |
|||
@ -1,32 +1,17 @@ |
|||
import type { LucideIcon } from "lucide-react"; |
|||
import { H4 } from "../Typography/H4.js"; |
|||
|
|||
export interface SidebarSectionProps { |
|||
title: string; |
|||
action?: { |
|||
icon: LucideIcon; |
|||
onClick: () => void; |
|||
}; |
|||
label: string; |
|||
subheader?: string; |
|||
children: React.ReactNode; |
|||
} |
|||
|
|||
export const SidebarSection = ({ |
|||
title, |
|||
action, |
|||
label: title, |
|||
children |
|||
}: SidebarSectionProps): JSX.Element => ( |
|||
<div className="space-y-1.5"> |
|||
<div className="mb-2 flex items-center justify-between px-2"> |
|||
<div className="font-medium">{title}</div> |
|||
{action && ( |
|||
<button |
|||
className="transition-all duration-300 hover:text-accent" |
|||
onClick={() => action.onClick} |
|||
> |
|||
<action.icon size={16} /> |
|||
</button> |
|||
)} |
|||
</div> |
|||
<ul className="space-y-1">{children}</ul> |
|||
<div className="px-4 py-2"> |
|||
<H4 className="mb-2 ml-2">{title}</H4> |
|||
<div className="space-y-1">{children}</div> |
|||
</div> |
|||
); |
|||
|
|||
@ -0,0 +1,29 @@ |
|||
import type { LucideIcon } from "lucide-react"; |
|||
import { Button } from "../Button.js"; |
|||
|
|||
export interface SidebarButtonProps { |
|||
label: string; |
|||
active?: boolean; |
|||
icon?: LucideIcon; |
|||
element?: JSX.Element; |
|||
onClick?: () => void; |
|||
} |
|||
|
|||
export const SidebarButton = ({ |
|||
label, |
|||
active, |
|||
icon: Icon, |
|||
element, |
|||
onClick |
|||
}: SidebarButtonProps): JSX.Element => ( |
|||
<Button |
|||
onClick={onClick} |
|||
variant={active ? "subtle" : "ghost"} |
|||
size="sm" |
|||
className="w-full justify-start gap-2" |
|||
> |
|||
{Icon && <Icon size={16} />} |
|||
{element && element} |
|||
{label} |
|||
</Button> |
|||
); |
|||
@ -1,9 +1,17 @@ |
|||
import { cn } from "@app/core/utils/cn.js"; |
|||
|
|||
export interface H4Props { |
|||
className?: string; |
|||
children: React.ReactNode; |
|||
} |
|||
|
|||
export const H4 = ({ children }: H4Props): JSX.Element => ( |
|||
<h4 className="mt-8 scroll-m-20 text-xl font-semibold tracking-tight"> |
|||
export const H4 = ({ className, children }: H4Props): JSX.Element => ( |
|||
<h4 |
|||
className={cn( |
|||
"scroll-m-20 text-xl font-semibold tracking-tight", |
|||
className |
|||
)} |
|||
> |
|||
{children} |
|||
</h4> |
|||
); |
|||
|
|||
@ -1,35 +0,0 @@ |
|||
import type { ButtonHTMLAttributes, ComponentType, SVGProps } from "react"; |
|||
|
|||
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> { |
|||
size?: "sm" | "md" | "lg"; |
|||
} |
|||
|
|||
export const Button = ({ |
|||
size = "md", |
|||
children, |
|||
disabled, |
|||
className, |
|||
...rest |
|||
}: ButtonProps): JSX.Element => { |
|||
return ( |
|||
<button |
|||
className={`flex w-full select-none rounded-md bg-accentMuted px-3 text-textPrimary hover:brightness-hover focus:outline-none active:brightness-press ${ |
|||
size === "sm" |
|||
? "h-8 text-sm" |
|||
: size === "md" |
|||
? "h-10 text-sm" |
|||
: "h-10 text-base" |
|||
} ${ |
|||
disabled |
|||
? "cursor-not-allowed text-textSecondary brightness-disabled hover:brightness-disabled" |
|||
: "" |
|||
} ${className}`}
|
|||
disabled={disabled} |
|||
{...rest} |
|||
> |
|||
<div className="m-auto flex shrink-0 items-center gap-2 font-medium"> |
|||
{children} |
|||
</div> |
|||
</button> |
|||
); |
|||
}; |
|||
@ -1,22 +0,0 @@ |
|||
import type { FormEvent, HTMLProps } from "react"; |
|||
|
|||
export interface FormProps extends HTMLProps<HTMLFormElement> { |
|||
onSubmit?: (event: FormEvent<HTMLFormElement>) => Promise<void>; |
|||
} |
|||
|
|||
export const Form = ({ |
|||
children, |
|||
onSubmit, |
|||
...props |
|||
}: FormProps): JSX.Element => { |
|||
return ( |
|||
<form |
|||
className="w-full rounded-md bg-backgroundSecondary px-2" |
|||
onSubmit={onSubmit} |
|||
onChange={onSubmit} |
|||
{...props} |
|||
> |
|||
<div className="flex flex-col gap-3 p-4">{children}</div> |
|||
</form> |
|||
); |
|||
}; |
|||
@ -1,61 +0,0 @@ |
|||
import { forwardRef, SelectHTMLAttributes } from "react"; |
|||
|
|||
import { InfoWrapper, InfoWrapperProps } from "@components/form/InfoWrapper.js"; |
|||
|
|||
export interface SelectProps |
|||
extends SelectHTMLAttributes<HTMLSelectElement>, |
|||
Omit<InfoWrapperProps, "children"> { |
|||
options?: string[]; |
|||
action?: { |
|||
icon: JSX.Element; |
|||
action: () => void; |
|||
}; |
|||
} |
|||
|
|||
export const Select = forwardRef<HTMLSelectElement, SelectProps>(function Input( |
|||
{ |
|||
label, |
|||
description, |
|||
options, |
|||
action, |
|||
disabled, |
|||
error, |
|||
children, |
|||
...rest |
|||
}: SelectProps, |
|||
ref |
|||
) { |
|||
return ( |
|||
<InfoWrapper label={label} description={description} error={error}> |
|||
<div className="flex rounded-md"> |
|||
<select |
|||
ref={ref} |
|||
className={`flex h-10 w-full rounded-md border-transparent bg-backgroundPrimary px-3 text-sm text-textPrimary focus:border-transparent focus:outline-none focus:ring-2 focus:ring-accent ${ |
|||
action ? "rounded-r-none" : "" |
|||
} ${ |
|||
disabled |
|||
? "cursor-not-allowed text-textSecondary brightness-disabled hover:brightness-disabled" |
|||
: "" |
|||
}`}
|
|||
disabled={disabled} |
|||
{...rest} |
|||
> |
|||
{options && |
|||
options.map((option, index) => ( |
|||
<option key={index}>{option}</option> |
|||
))} |
|||
{children} |
|||
</select> |
|||
{action && ( |
|||
<button |
|||
type="button" |
|||
onClick={action.action} |
|||
className="relative -ml-px inline-flex items-center space-x-2 rounded-r-md bg-backgroundPrimary px-4 py-2 text-sm font-medium text-textSecondary brightness-hover hover:brightness-hover focus:outline-none focus:ring-2 focus:ring-accent active:brightness-press" |
|||
> |
|||
{action.icon} |
|||
</button> |
|||
)} |
|||
</div> |
|||
</InfoWrapper> |
|||
); |
|||
}); |
|||
@ -1,13 +0,0 @@ |
|||
export const renderOptions = (enumValue: { |
|||
[s: string]: string | number; |
|||
}): JSX.Element[] => { |
|||
const optionsEnumValues = enumValue |
|||
? Object.entries(enumValue).filter((value) => typeof value[1] === "number") |
|||
: []; |
|||
|
|||
return optionsEnumValues.map(([name, value], index) => ( |
|||
<option key={index} value={value}> |
|||
{name} |
|||
</option> |
|||
)); |
|||
}; |
|||
@ -0,0 +1,22 @@ |
|||
import { IsArray, IsBoolean, IsNumber, IsString, IsUrl } from "class-validator"; |
|||
|
|||
import type { RasterSource } from "@core/stores/appStore.js"; |
|||
|
|||
export class MapValidation { |
|||
@IsArray() |
|||
rasterSources: MapValidation_RasterSources[]; |
|||
} |
|||
|
|||
export class MapValidation_RasterSources implements RasterSource { |
|||
@IsBoolean() |
|||
enabled: boolean; |
|||
|
|||
@IsString() |
|||
title: string; |
|||
|
|||
@IsUrl() |
|||
tiles: string; |
|||
|
|||
@IsNumber() |
|||
tileSize: number; |
|||
} |
|||
Loading…
Reference in new issue