diff --git a/ui/src/components/Combobox.tsx b/ui/src/components/Combobox.tsx index 8055043..661b77b 100644 --- a/ui/src/components/Combobox.tsx +++ b/ui/src/components/Combobox.tsx @@ -1,7 +1,14 @@ import { useRef } from "react"; import clsx from "clsx"; -import { Combobox as HeadlessCombobox, ComboboxInput, ComboboxOption, ComboboxOptions } from "@headlessui/react"; +import { + Combobox as HeadlessCombobox, + ComboboxInput, + ComboboxOption, + ComboboxOptions, +} from "@headlessui/react"; + import { cva } from "@/cva.config"; + import Card from "./Card"; export interface ComboboxOption { @@ -22,7 +29,7 @@ const comboboxVariants = cva({ type BaseProps = React.ComponentProps; -interface ComboboxProps extends Omit { +interface ComboboxProps extends Omit { displayValue: (option: ComboboxOption) => string; onInputChange: (option: string) => void; options: () => ComboboxOption[]; @@ -48,72 +55,68 @@ export function Combobox({ const classes = comboboxVariants({ size }); return ( - + {() => ( <> onInputChange(event.target.value)} - disabled={disabled} + ref={inputRef} + className={clsx( + classes, + + // General styling + "block w-full cursor-pointer rounded border-none py-0 font-medium shadow-none outline-0 transition duration-300", + + // Hover + "hover:bg-blue-50/80 active:bg-blue-100/60", + + // Dark mode + "dark:bg-slate-800 dark:text-white dark:hover:bg-slate-700 dark:active:bg-slate-800/60", + + // Focus + "focus:outline-blue-600 focus:ring-2 focus:ring-blue-700 focus:ring-offset-2 dark:focus:outline-blue-500 dark:focus:ring-blue-500", + + // Disabled + disabled && + "pointer-events-none select-none bg-slate-50 text-slate-500/80 disabled:hover:bg-white dark:bg-slate-800 dark:text-slate-400/80 dark:disabled:hover:bg-slate-800", + )} + placeholder={disabled ? disabledMessage : placeholder} + displayValue={displayValue} + onChange={event => onInputChange(event.target.value)} + disabled={disabled} /> - + {options().length > 0 && ( - - {options().map((option) => ( - + {options().map(option => ( + {option.label} ))} )} - + {options().length === 0 && inputRef.current?.value && ( -
-
- {emptyMessage} -
+
+
{emptyMessage}
)} )} ); -} \ No newline at end of file +} diff --git a/ui/src/components/ConfirmDialog.tsx b/ui/src/components/ConfirmDialog.tsx index 6dbff8c..3771096 100644 --- a/ui/src/components/ConfirmDialog.tsx +++ b/ui/src/components/ConfirmDialog.tsx @@ -1,4 +1,9 @@ -import { ExclamationTriangleIcon, CheckCircleIcon, InformationCircleIcon } from "@heroicons/react/24/outline"; +import { + ExclamationTriangleIcon, + CheckCircleIcon, + InformationCircleIcon, +} from "@heroicons/react/24/outline"; + import { cx } from "@/cva.config"; import { Button } from "@/components/Button"; import Modal from "@/components/Modal"; @@ -42,12 +47,15 @@ const variantConfig = { iconBgClass: "bg-blue-100", buttonTheme: "primary", }, -} as Record; + } +>; export function ConfirmDialog({ open, @@ -65,13 +73,18 @@ export function ConfirmDialog({ return (
-
+
-
+
-
+

{title}

@@ -83,12 +96,7 @@ export function ConfirmDialog({
{cancelText && ( -
); -} \ No newline at end of file +} diff --git a/ui/src/components/VideoOverlay.tsx b/ui/src/components/VideoOverlay.tsx index 0d71dcd..ada8f20 100644 --- a/ui/src/components/VideoOverlay.tsx +++ b/ui/src/components/VideoOverlay.tsx @@ -3,11 +3,11 @@ import { ExclamationTriangleIcon } from "@heroicons/react/24/solid"; import { ArrowPathIcon, ArrowRightIcon } from "@heroicons/react/16/solid"; import { motion, AnimatePresence } from "framer-motion"; import { LuPlay } from "react-icons/lu"; +import { BsMouseFill } from "react-icons/bs"; import { Button, LinkButton } from "@components/Button"; import LoadingSpinner from "@components/LoadingSpinner"; import Card, { GridCard } from "@components/Card"; -import { BsMouseFill } from "react-icons/bs"; interface OverlayContentProps { children: React.ReactNode; @@ -242,8 +242,8 @@ export function HDMIErrorOverlay({ show, hdmiState }: HDMIErrorOverlayProps) { Ensure source device is powered on and outputting a signal
  • - If using an adapter, ensure it's compatible and - functioning correctly + If using an adapter, ensure it's compatible and functioning + correctly
  • diff --git a/ui/src/components/WebRTCVideo.tsx b/ui/src/components/WebRTCVideo.tsx index b73135b..2b4cac6 100644 --- a/ui/src/components/WebRTCVideo.tsx +++ b/ui/src/components/WebRTCVideo.tsx @@ -151,7 +151,7 @@ export default function WebRTCVideo() { const isKeyboardLockGranted = await checkNavigatorPermissions("keyboard-lock"); if (isKeyboardLockGranted) { if ("keyboard" in navigator) { - // @ts-ignore + // @ts-expect-error - keyboard lock is not supported in all browsers await navigator.keyboard.lock(); } } diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts index db1fd04..045655e 100644 --- a/ui/src/hooks/stores.ts +++ b/ui/src/hooks/stores.ts @@ -1,6 +1,11 @@ import { create } from "zustand"; import { createJSONStorage, persist } from "zustand/middleware"; -import { MAX_STEPS_PER_MACRO, MAX_TOTAL_MACROS, MAX_KEYS_PER_STEP } from "@/constants/macros"; + +import { + MAX_STEPS_PER_MACRO, + MAX_TOTAL_MACROS, + MAX_KEYS_PER_STEP, +} from "@/constants/macros"; // Define the JsonRpc types for better type checking interface JsonRpcResponse { @@ -564,12 +569,12 @@ export interface UpdateState { setOtaState: (state: UpdateState["otaState"]) => void; setUpdateDialogHasBeenMinimized: (hasBeenMinimized: boolean) => void; modalView: - | "loading" - | "updating" - | "upToDate" - | "updateAvailable" - | "updateCompleted" - | "error"; + | "loading" + | "updating" + | "upToDate" + | "updateAvailable" + | "updateCompleted" + | "error"; setModalView: (view: UpdateState["modalView"]) => void; setUpdateErrorMessage: (errorMessage: string) => void; updateErrorMessage: string | null; @@ -633,12 +638,12 @@ export const useUsbConfigModalStore = create(set => ({ interface LocalAuthModalState { modalView: - | "createPassword" - | "deletePassword" - | "updatePassword" - | "creationSuccess" - | "deleteSuccess" - | "updateSuccess"; + | "createPassword" + | "deletePassword" + | "updatePassword" + | "creationSuccess" + | "deleteSuccess" + | "updateSuccess"; setModalView: (view: LocalAuthModalState["modalView"]) => void; } @@ -719,12 +724,23 @@ export interface NetworkState { setDhcpLeaseExpiry: (expiry: Date) => void; } - -export type IPv6Mode = "disabled" | "slaac" | "dhcpv6" | "slaac_and_dhcpv6" | "static" | "link_local" | "unknown"; +export type IPv6Mode = + | "disabled" + | "slaac" + | "dhcpv6" + | "slaac_and_dhcpv6" + | "static" + | "link_local" + | "unknown"; export type IPv4Mode = "disabled" | "static" | "dhcp" | "unknown"; export type LLDPMode = "disabled" | "basic" | "all" | "unknown"; export type mDNSMode = "disabled" | "auto" | "ipv4_only" | "ipv6_only" | "unknown"; -export type TimeSyncMode = "ntp_only" | "ntp_and_http" | "http_only" | "custom" | "unknown"; +export type TimeSyncMode = + | "ntp_only" + | "ntp_and_http" + | "http_only" + | "custom" + | "unknown"; export interface NetworkSettings { hostname: string; @@ -749,7 +765,7 @@ export const useNetworkStateStore = create((set, get) => ({ lease.lease_expiry = expiry; set({ dhcp_lease: lease }); - } + }, })); export interface KeySequenceStep { @@ -771,8 +787,20 @@ export interface MacrosState { initialized: boolean; loadMacros: () => Promise; saveMacros: (macros: KeySequence[]) => Promise; - sendFn: ((method: string, params: unknown, callback?: ((resp: JsonRpcResponse) => void) | undefined) => void) | null; - setSendFn: (sendFn: ((method: string, params: unknown, callback?: ((resp: JsonRpcResponse) => void) | undefined) => void)) => void; + sendFn: + | (( + method: string, + params: unknown, + callback?: ((resp: JsonRpcResponse) => void) | undefined, + ) => void) + | null; + setSendFn: ( + sendFn: ( + method: string, + params: unknown, + callback?: ((resp: JsonRpcResponse) => void) | undefined, + ) => void, + ) => void; } export const generateMacroId = () => { @@ -785,7 +813,7 @@ export const useMacrosStore = create((set, get) => ({ initialized: false, sendFn: null, - setSendFn: (sendFn) => { + setSendFn: sendFn => { set({ sendFn }); }, @@ -802,7 +830,7 @@ export const useMacrosStore = create((set, get) => ({ try { await new Promise((resolve, reject) => { - sendFn("getKeyboardMacros", {}, (response) => { + sendFn("getKeyboardMacros", {}, response => { if (response.error) { console.error("Error loading macros:", response.error); reject(new Error(response.error.message)); @@ -822,7 +850,7 @@ export const useMacrosStore = create((set, get) => ({ set({ macros: sortedMacros, - initialized: true + initialized: true, }); resolve(); @@ -849,15 +877,23 @@ export const useMacrosStore = create((set, get) => ({ for (const macro of macros) { if (macro.steps.length > MAX_STEPS_PER_MACRO) { - console.error(`Cannot save: macro "${macro.name}" exceeds maximum of ${MAX_STEPS_PER_MACRO} steps`); - throw new Error(`Cannot save: macro "${macro.name}" exceeds maximum of ${MAX_STEPS_PER_MACRO} steps`); + console.error( + `Cannot save: macro "${macro.name}" exceeds maximum of ${MAX_STEPS_PER_MACRO} steps`, + ); + throw new Error( + `Cannot save: macro "${macro.name}" exceeds maximum of ${MAX_STEPS_PER_MACRO} steps`, + ); } for (let i = 0; i < macro.steps.length; i++) { const step = macro.steps[i]; if (step.keys && step.keys.length > MAX_KEYS_PER_STEP) { - console.error(`Cannot save: macro "${macro.name}" step ${i + 1} exceeds maximum of ${MAX_KEYS_PER_STEP} keys`); - throw new Error(`Cannot save: macro "${macro.name}" step ${i + 1} exceeds maximum of ${MAX_KEYS_PER_STEP} keys`); + console.error( + `Cannot save: macro "${macro.name}" step ${i + 1} exceeds maximum of ${MAX_KEYS_PER_STEP} keys`, + ); + throw new Error( + `Cannot save: macro "${macro.name}" step ${i + 1} exceeds maximum of ${MAX_KEYS_PER_STEP} keys`, + ); } } } @@ -867,20 +903,25 @@ export const useMacrosStore = create((set, get) => ({ try { const macrosWithSortOrder = macros.map((macro, index) => ({ ...macro, - sortOrder: macro.sortOrder !== undefined ? macro.sortOrder : index + sortOrder: macro.sortOrder !== undefined ? macro.sortOrder : index, })); - const response = await new Promise((resolve) => { - sendFn("setKeyboardMacros", { params: { macros: macrosWithSortOrder } }, (response) => { - resolve(response); - }); + const response = await new Promise(resolve => { + sendFn( + "setKeyboardMacros", + { params: { macros: macrosWithSortOrder } }, + response => { + resolve(response); + }, + ); }); if (response.error) { console.error("Error saving macros:", response.error); - const errorMessage = typeof response.error.data === 'string' - ? response.error.data - : response.error.message || "Failed to save macros"; + const errorMessage = + typeof response.error.data === "string" + ? response.error.data + : response.error.message || "Failed to save macros"; throw new Error(errorMessage); } @@ -892,5 +933,5 @@ export const useMacrosStore = create((set, get) => ({ } finally { set({ loading: false }); } - } -})); \ No newline at end of file + }, +})); diff --git a/ui/src/routes/devices.$id.settings.macros.tsx b/ui/src/routes/devices.$id.settings.macros.tsx index f809f57..ba1a2ba 100644 --- a/ui/src/routes/devices.$id.settings.macros.tsx +++ b/ui/src/routes/devices.$id.settings.macros.tsx @@ -1,6 +1,15 @@ import { useEffect, Fragment, useMemo, useState, useCallback } from "react"; import { useNavigate } from "react-router-dom"; -import { LuPenLine, LuCopy, LuMoveRight, LuCornerDownRight, LuArrowUp, LuArrowDown, LuTrash2, LuCommand } from "react-icons/lu"; +import { + LuPenLine, + LuCopy, + LuMoveRight, + LuCornerDownRight, + LuArrowUp, + LuArrowDown, + LuTrash2, + LuCommand, +} from "react-icons/lu"; import { KeySequence, useMacrosStore, generateMacroId } from "@/hooks/stores"; import { SettingsPageHeader } from "@/components/SettingsPageheader"; @@ -26,10 +35,10 @@ export default function SettingsMacrosRoute() { const [actionLoadingId, setActionLoadingId] = useState(null); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [macroToDelete, setMacroToDelete] = useState(null); - - const isMaxMacrosReached = useMemo(() => - macros.length >= MAX_TOTAL_MACROS, - [macros.length] + + const isMaxMacrosReached = useMemo( + () => macros.length >= MAX_TOTAL_MACROS, + [macros.length], ); useEffect(() => { @@ -38,75 +47,83 @@ export default function SettingsMacrosRoute() { } }, [initialized, loadMacros]); - const handleDuplicateMacro = useCallback(async (macro: KeySequence) => { - if (!macro?.id || !macro?.name) { - notifications.error("Invalid macro data"); - return; - } - - if (isMaxMacrosReached) { - notifications.error(`Maximum of ${MAX_TOTAL_MACROS} macros allowed`); - return; - } - - setActionLoadingId(macro.id); - - const newMacroCopy: KeySequence = { - ...JSON.parse(JSON.stringify(macro)), - id: generateMacroId(), - name: `${macro.name} ${COPY_SUFFIX}`, - sortOrder: macros.length + 1, - }; - - try { - await saveMacros(normalizeSortOrders([...macros, newMacroCopy])); - notifications.success(`Macro "${newMacroCopy.name}" duplicated successfully`); - } catch (error: unknown) { - if (error instanceof Error) { - notifications.error(`Failed to duplicate macro: ${error.message}`); - } else { - notifications.error("Failed to duplicate macro"); + const handleDuplicateMacro = useCallback( + async (macro: KeySequence) => { + if (!macro?.id || !macro?.name) { + notifications.error("Invalid macro data"); + return; } - } finally { - setActionLoadingId(null); - } - }, [isMaxMacrosReached, macros, saveMacros, setActionLoadingId]); - const handleMoveMacro = useCallback(async (index: number, direction: 'up' | 'down', macroId: string) => { - if (!Array.isArray(macros) || macros.length === 0) { - notifications.error("No macros available"); - return; - } - - const newIndex = direction === 'up' ? index - 1 : index + 1; - if (newIndex < 0 || newIndex >= macros.length) return; - - setActionLoadingId(macroId); - - try { - const newMacros = [...macros]; - [newMacros[index], newMacros[newIndex]] = [newMacros[newIndex], newMacros[index]]; - const updatedMacros = normalizeSortOrders(newMacros); - - await saveMacros(updatedMacros); - notifications.success("Macro order updated successfully"); - } catch (error: unknown) { - if (error instanceof Error) { - notifications.error(`Failed to reorder macros: ${error.message}`); - } else { - notifications.error("Failed to reorder macros"); + if (isMaxMacrosReached) { + notifications.error(`Maximum of ${MAX_TOTAL_MACROS} macros allowed`); + return; } - } finally { - setActionLoadingId(null); - } - }, [macros, saveMacros, setActionLoadingId]); + + setActionLoadingId(macro.id); + + const newMacroCopy: KeySequence = { + ...JSON.parse(JSON.stringify(macro)), + id: generateMacroId(), + name: `${macro.name} ${COPY_SUFFIX}`, + sortOrder: macros.length + 1, + }; + + try { + await saveMacros(normalizeSortOrders([...macros, newMacroCopy])); + notifications.success(`Macro "${newMacroCopy.name}" duplicated successfully`); + } catch (error: unknown) { + if (error instanceof Error) { + notifications.error(`Failed to duplicate macro: ${error.message}`); + } else { + notifications.error("Failed to duplicate macro"); + } + } finally { + setActionLoadingId(null); + } + }, + [isMaxMacrosReached, macros, saveMacros, setActionLoadingId], + ); + + const handleMoveMacro = useCallback( + async (index: number, direction: "up" | "down", macroId: string) => { + if (!Array.isArray(macros) || macros.length === 0) { + notifications.error("No macros available"); + return; + } + + const newIndex = direction === "up" ? index - 1 : index + 1; + if (newIndex < 0 || newIndex >= macros.length) return; + + setActionLoadingId(macroId); + + try { + const newMacros = [...macros]; + [newMacros[index], newMacros[newIndex]] = [newMacros[newIndex], newMacros[index]]; + const updatedMacros = normalizeSortOrders(newMacros); + + await saveMacros(updatedMacros); + notifications.success("Macro order updated successfully"); + } catch (error: unknown) { + if (error instanceof Error) { + notifications.error(`Failed to reorder macros: ${error.message}`); + } else { + notifications.error("Failed to reorder macros"); + } + } finally { + setActionLoadingId(null); + } + }, + [macros, saveMacros, setActionLoadingId], + ); const handleDeleteMacro = useCallback(async () => { if (!macroToDelete?.id) return; setActionLoadingId(macroToDelete.id); try { - const updatedMacros = normalizeSortOrders(macros.filter(m => m.id !== macroToDelete.id)); + const updatedMacros = normalizeSortOrders( + macros.filter(m => m.id !== macroToDelete.id), + ); await saveMacros(updatedMacros); notifications.success(`Macro "${macroToDelete.name}" deleted successfully`); setShowDeleteConfirm(false); @@ -122,135 +139,168 @@ export default function SettingsMacrosRoute() { } }, [macroToDelete, macros, saveMacros]); - const MacroList = useMemo(() => ( -
    - {macros.map((macro, index) => ( - -
    -
    -
    + const MacroList = useMemo( + () => ( +
    + {macros.map((macro, index) => ( + +
    +
    +
    -
    -

    - {macro.name} -

    -

    - - {macro.steps.map((step, stepIndex) => { - const StepIcon = stepIndex === 0 ? LuMoveRight : LuCornerDownRight; +

    +

    + {macro.name} +

    +

    + + {macro.steps.map((step, stepIndex) => { + const StepIcon = stepIndex === 0 ? LuMoveRight : LuCornerDownRight; - return ( - - - - {(Array.isArray(step.modifiers) && step.modifiers.length > 0) || (Array.isArray(step.keys) && step.keys.length > 0) ? ( - <> - {Array.isArray(step.modifiers) && step.modifiers.map((modifier, idx) => ( - - - {modifierDisplayMap[modifier] || modifier} - - {idx < step.modifiers.length - 1 && ( - + + return ( + + + + {(Array.isArray(step.modifiers) && + step.modifiers.length > 0) || + (Array.isArray(step.keys) && step.keys.length > 0) ? ( + <> + {Array.isArray(step.modifiers) && + step.modifiers.map((modifier, idx) => ( + + + {modifierDisplayMap[modifier] || modifier} + + {idx < step.modifiers.length - 1 && ( + + {" "} + +{" "} + + )} + + ))} + + {Array.isArray(step.modifiers) && + step.modifiers.length > 0 && + Array.isArray(step.keys) && + step.keys.length > 0 && ( + + {" "} + +{" "} + )} - - ))} - {Array.isArray(step.modifiers) && step.modifiers.length > 0 && Array.isArray(step.keys) && step.keys.length > 0 && ( - + - )} - - {Array.isArray(step.keys) && step.keys.map((key, idx) => ( - - - {keyDisplayMap[key] || key} - - {idx < step.keys.length - 1 && ( - + - )} - - ))} - - ) : ( - Delay only - )} - {step.delay !== DEFAULT_DELAY && ( - ({step.delay}ms) - )} + {Array.isArray(step.keys) && + step.keys.map((key, idx) => ( + + + {keyDisplayMap[key] || key} + + {idx < step.keys.length - 1 && ( + + {" "} + +{" "} + + )} + + ))} + + ) : ( + + Delay only + + )} + {step.delay !== DEFAULT_DELAY && ( + + ({step.delay}ms) + + )} + - - ); - })} - -

    -
    + ); + })} + +

    +
    -
    -
    -
    - - ))} + + ))} - { - setShowDeleteConfirm(false); - setMacroToDelete(null); - }} - title="Delete Macro" - description={`Are you sure you want to delete "${macroToDelete?.name}"? This action cannot be undone.`} - variant="danger" - confirmText={actionLoadingId === macroToDelete?.id ? "Deleting..." : "Delete"} - onConfirm={handleDeleteMacro} - isConfirming={actionLoadingId === macroToDelete?.id} - /> -
    - ), [macros, actionLoadingId, showDeleteConfirm, macroToDelete, handleDeleteMacro]); + { + setShowDeleteConfirm(false); + setMacroToDelete(null); + }} + title="Delete Macro" + description={`Are you sure you want to delete "${macroToDelete?.name}"? This action cannot be undone.`} + variant="danger" + confirmText={actionLoadingId === macroToDelete?.id ? "Deleting..." : "Delete"} + onConfirm={handleDeleteMacro} + isConfirming={actionLoadingId === macroToDelete?.id} + /> +
    + ), + [ + macros, + showDeleteConfirm, + macroToDelete?.name, + macroToDelete?.id, + actionLoadingId, + handleDeleteMacro, + handleMoveMacro, + handleDuplicateMacro, + navigate, + ], + ); return (
    @@ -259,7 +309,7 @@ export default function SettingsMacrosRoute() { title="Keyboard Macros" description={`Combine keystrokes into a single action for faster workflows.`} /> - { macros.length > 0 && ( + {macros.length > 0 && (
    ); diff --git a/ui/src/routes/devices.$id.settings.network.tsx b/ui/src/routes/devices.$id.settings.network.tsx index a96c08b..1811ca0 100644 --- a/ui/src/routes/devices.$id.settings.network.tsx +++ b/ui/src/routes/devices.$id.settings.network.tsx @@ -15,17 +15,17 @@ import { } from "@/hooks/stores"; import { useJsonRpc } from "@/hooks/useJsonRpc"; import notifications from "@/notifications"; -import { Button, LinkButton } from "@components/Button"; +import { Button } from "@components/Button"; import { GridCard } from "@components/Card"; import InputField from "@components/InputField"; import { SettingsPageHeader } from "../components/SettingsPageheader"; import { SelectMenuBasic } from "../components/SelectMenuBasic"; - -import { SettingsItem } from "./devices.$id.settings"; import Fieldset from "../components/Fieldset"; import { ConfirmDialog } from "../components/ConfirmDialog"; +import { SettingsItem } from "./devices.$id.settings"; + dayjs.extend(relativeTime); const defaultNetworkSettings: NetworkSettings = {