From 91c0ea85a632be3197d85a49cd3affd742bf6d33 Mon Sep 17 00:00:00 2001 From: lucas lelievre Date: Mon, 12 Jun 2023 00:50:54 +0200 Subject: [PATCH] Mobile gui basics (#713) * Mobile implementation without settings and widgets * Working Settings and widgets + Few tweaks for responsive * Make linter happy * Update gui/src/components/Navbar.tsx Co-authored-by: Uriel * Update gui/src/components/onboarding/pages/CalibrationTutorial.tsx Co-authored-by: Uriel * Update gui/src/components/onboarding/pages/CalibrationTutorial.tsx Co-authored-by: Uriel * Update gui/src/components/onboarding/pages/CalibrationTutorial.tsx Co-authored-by: Uriel * Update gui/src/components/onboarding/pages/CalibrationTutorial.tsx Co-authored-by: Uriel * Update gui/src/components/settings/pages/Serial.tsx Co-authored-by: Uriel * Apply review changes * Revert removing full height in body * Fix onboarding manual body proportions on mobile * make connect trackers page work on mobile * Apply suggestions from code review Co-authored-by: DevMiner * Applie required changes * rollback server ip * Remove placeholder string * Remove unused isMobile * Remove unused isMobile * error on unused vars * make it warn but make the lint script error on warnings --------- Co-authored-by: Uriel Co-authored-by: DevMiner --- gui/index.html | 2 +- gui/package.json | 3 +- gui/src-tauri/tauri.conf.json | 4 +- gui/src/App.tsx | 120 +- gui/src/components/MainLayout.tsx | 97 +- gui/src/components/Navbar.tsx | 80 +- gui/src/components/TopBar.tsx | 253 +-- gui/src/components/WidgetsComponent.tsx | 75 + gui/src/components/commons/BaseModal.tsx | 2 +- .../components/commons/BodyInteractions.tsx | 7 +- gui/src/components/commons/Dropdown.tsx | 70 +- gui/src/components/commons/TipBox.tsx | 2 +- gui/src/components/commons/Typography.tsx | 10 +- gui/src/components/home/Home.tsx | 90 +- .../components/onboarding/BodyAssignment.tsx | 3 + .../onboarding/OnboardingLayout.tsx | 8 +- .../components/onboarding/SkipSetupButton.tsx | 2 +- .../onboarding/SkipSetupWarningModal.tsx | 2 +- .../components/onboarding/StepperSlider.tsx | 4 +- .../onboarding/pages/CalibrationTutorial.tsx | 24 +- .../onboarding/pages/ConnectTracker.tsx | 24 +- gui/src/components/onboarding/pages/Home.tsx | 6 +- .../onboarding/pages/ResetTutorial.tsx | 127 +- .../components/onboarding/pages/WifiCreds.tsx | 4 +- .../AssignmentTutorial.tsx | 122 +- .../body-proportions/AutomaticProportions.tsx | 28 +- .../body-proportions/BodyProportions.tsx | 71 +- .../body-proportions/ManualProportions.tsx | 85 +- .../body-proportions/ProportionsChoose.tsx | 225 +-- .../autobone-steps/PutTrackersOn.tsx | 36 +- .../autobone-steps/Recording.tsx | 2 +- .../autobone-steps/Requirements.tsx | 4 +- .../autobone-steps/StartRecording.tsx | 4 +- .../pages/mounting/AutomaticMounting.tsx | 8 +- .../pages/mounting/ManualMounting.tsx | 63 +- .../pages/mounting/MountingChoose.tsx | 176 ++- .../pages/mounting/MountingSelectionMenu.tsx | 8 +- .../mounting/mounting-steps/MountingReset.tsx | 30 +- .../mounting/mounting-steps/Preparation.tsx | 29 +- .../mounting/mounting-steps/PutTrackersOn.tsx | 42 +- .../trackers-assign/TrackerAssignment.tsx | 19 +- .../trackers-assign/TrackerSelectionMenu.tsx | 11 +- .../components/settings/SettingsLayout.tsx | 100 +- .../settings/SettingsPageLayout.tsx | 37 +- .../components/settings/SettingsSidebar.tsx | 4 - .../settings/pages/GeneralSettings.tsx | 1373 +++++++++-------- .../settings/pages/OSCRouterSettings.tsx | 230 ++- gui/src/components/settings/pages/Serial.tsx | 26 +- .../components/settings/pages/VMCSettings.tsx | 316 ++-- .../settings/pages/VRCOSCSettings.tsx | 324 ++-- .../SingleTrackerBodyAssignmentMenu.tsx | 83 +- gui/src/components/tracker/TrackerCard.tsx | 2 +- .../components/tracker/TrackerPartCard.tsx | 2 +- .../components/tracker/TrackerSettings.tsx | 2 +- gui/src/components/vr-mode/VRModePage.tsx | 9 + gui/src/hooks/breakpoint.ts | 28 + gui/src/hooks/layout.ts | 4 +- gui/src/index.css | 2 +- gui/src/vite-env.d.ts | 2 + ...tailwind.config.cjs => tailwind.config.ts} | 24 +- gui/tsconfig.json | 2 +- package-lock.json | 76 + 62 files changed, 2581 insertions(+), 2047 deletions(-) create mode 100644 gui/src/components/WidgetsComponent.tsx create mode 100644 gui/src/components/vr-mode/VRModePage.tsx create mode 100644 gui/src/hooks/breakpoint.ts rename gui/{tailwind.config.cjs => tailwind.config.ts} (93%) diff --git a/gui/index.html b/gui/index.html index 5fd7098a2..3f55eac0f 100644 --- a/gui/index.html +++ b/gui/index.html @@ -3,7 +3,7 @@ - + diff --git a/gui/package.json b/gui/package.json index b84028c44..d92334cee 100644 --- a/gui/package.json +++ b/gui/package.json @@ -27,6 +27,7 @@ "react-dom": "^18.0.0", "react-hook-form": "^7.29.0", "react-modal": "3.15.1", + "react-responsive": "^9.0.2", "react-router-dom": "^6.2.2", "semver": "^7.5.0", "solarxr-protocol": "file:../solarxr-protocol", @@ -39,7 +40,7 @@ "dev": "tauri dev", "skipbundler": "tauri build -b none", "tauri": "tauri", - "lint": "eslint \"src/**/*.{js,jsx,ts,tsx,json}\" && prettier --check \"src/**/*.{js,jsx,ts,tsx,css,md,json}\"", + "lint": "eslint --max-warnings=0 \"src/**/*.{js,jsx,ts,tsx,json}\" && prettier --check \"src/**/*.{js,jsx,ts,tsx,css,md,json}\"", "lint:fix": "eslint --fix \"src/**/*.{js,jsx,ts,tsx,json}\"", "format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,css,md,json}\"", "preview-vite": "vite preview", diff --git a/gui/src-tauri/tauri.conf.json b/gui/src-tauri/tauri.conf.json index 18eae812f..10c7e35e6 100644 --- a/gui/src-tauri/tauri.conf.json +++ b/gui/src-tauri/tauri.conf.json @@ -90,8 +90,8 @@ "title": "SlimeVR", "width": 1289, "height": 709, - "minWidth": 880, - "minHeight": 740, + "minWidth": 393, + "minHeight": 851, "resizable": true, "fullscreen": false, "decorations": false, diff --git a/gui/src/App.tsx b/gui/src/App.tsx index d408da742..83a5323d2 100644 --- a/gui/src/App.tsx +++ b/gui/src/App.tsx @@ -49,6 +49,9 @@ import { CalibrationTutorialPage } from './components/onboarding/pages/Calibrati import { AssignmentTutorialPage } from './components/onboarding/pages/assignment-preparation/AssignmentTutorial'; import { open } from '@tauri-apps/api/shell'; import semver from 'semver'; +import { tauri } from '../src-tauri/tauri.conf.json'; +import { useBreakpoint } from './hooks/breakpoint'; +import { VRModePage } from './components/vr-mode/VRModePage'; export const GH_REPO = 'SlimeVR/SlimeVR-Server'; export const VersionContext = createContext(''); @@ -56,8 +59,10 @@ export const DOCS_SITE = 'https://docs.slimevr.dev/'; function Layout() { const { loading } = useConfig(); + if (loading) return <>; + const { isMobile } = useBreakpoint('mobile'); return ( <> @@ -66,15 +71,23 @@ function Layout() { + } /> + + + + } + /> + } @@ -135,7 +148,10 @@ function Layout() { ); } -const MIN_SIZE = { width: 880, height: 740 }; +const MIN_SIZE = { + width: tauri.windows[0].minWidth, + height: tauri.windows[0].minHeight, +}; export default function App() { const websocketAPI = useProvideWebsocketApi(); @@ -160,17 +176,19 @@ export default function App() { fetchReleases().catch(() => console.error('failed to fetch releases')); }, []); - useEffect(() => { - os.type() - .then((type) => document.body.classList.add(type.toLowerCase())) - .catch(console.error); - - return () => { + if (window.__TAURI_METADATA__) { + useEffect(() => { os.type() - .then((type) => document.body.classList.remove(type.toLowerCase())) + .then((type) => document.body.classList.add(type.toLowerCase())) .catch(console.error); - }; - }, []); + + return () => { + os.type() + .then((type) => document.body.classList.remove(type.toLowerCase())) + .catch(console.error); + }; + }, []); + } // This doesn't seem to resize it live, but if you close it, it gets restored to min size useEffect(() => { @@ -195,45 +213,47 @@ export default function App() { return () => clearInterval(interval); }, []); - useEffect(() => { - const unlisten = listen( - 'server-status', - (event: Event<[string, string]>) => { - const [eventType, s] = event.payload; - if ('stderr' === eventType) { - // This strange invocation is what lets us lose the line information in the console - // See more here: https://stackoverflow.com/a/48994308 - setTimeout( - console.log.bind( - console, - `%c[SERVER] %c${s}`, - 'color:cyan', - 'color:red' - ) - ); - } else if (eventType === 'stdout') { - setTimeout( - console.log.bind( - console, - `%c[SERVER] %c${s}`, - 'color:cyan', - 'color:green' - ) - ); - } else if (eventType === 'error') { - console.error('Error: %s', s); - } else if (eventType === 'terminated') { - console.error('Server Process Terminated: %s', s); - } else if (eventType === 'other') { - console.log('Other process event: %s', s); + if (window.__TAURI_METADATA__) { + useEffect(() => { + const unlisten = listen( + 'server-status', + (event: Event<[string, string]>) => { + const [eventType, s] = event.payload; + if ('stderr' === eventType) { + // This strange invocation is what lets us lose the line information in the console + // See more here: https://stackoverflow.com/a/48994308 + setTimeout( + console.log.bind( + console, + `%c[SERVER] %c${s}`, + 'color:cyan', + 'color:red' + ) + ); + } else if (eventType === 'stdout') { + setTimeout( + console.log.bind( + console, + `%c[SERVER] %c${s}`, + 'color:cyan', + 'color:green' + ) + ); + } else if (eventType === 'error') { + console.error('Error: %s', s); + } else if (eventType === 'terminated') { + console.error('Server Process Terminated: %s', s); + } else if (eventType === 'other') { + console.log('Other process event: %s', s); + } } - } - ); - return () => { - // eslint-disable-next-line @typescript-eslint/no-empty-function - unlisten.then(() => {}); - }; - }, []); + ); + return () => { + // eslint-disable-next-line @typescript-eslint/no-empty-function + unlisten.then(() => {}); + }; + }, []); + } useEffect(() => { function onKeyboard(ev: KeyboardEvent) { diff --git a/gui/src/components/MainLayout.tsx b/gui/src/components/MainLayout.tsx index 9277833ab..f421ac3b8 100644 --- a/gui/src/components/MainLayout.tsx +++ b/gui/src/components/MainLayout.tsx @@ -1,62 +1,38 @@ import classNames from 'classnames'; -import { ReactNode, useEffect, useMemo, useState } from 'react'; +import { ReactNode, useEffect, useState } from 'react'; import { LegTweaksTmpChangeT, LegTweaksTmpClearT, - ResetType, RpcMessage, SettingsRequestT, - SettingsResponseT, - StatusData, } from 'solarxr-protocol'; -import { useConfig } from '../hooks/config'; -import { useLayout } from '../hooks/layout'; -import { BVHButton } from './BVHButton'; -import { ResetButton } from './home/ResetButton'; +import { useElemSize, useLayout } from '../hooks/layout'; import { Navbar } from './Navbar'; import { TopBar } from './TopBar'; -import { DeveloperModeWidget } from './widgets/DeveloperModeWidget'; -import { OverlayWidget } from './widgets/OverlayWidget'; -import { ClearDriftCompensationButton } from './ClearDriftCompensationButton'; import { useWebsocketAPI } from '../hooks/websocket-api'; -import { useStatusContext, parseStatusToLocale } from '../hooks/status-system'; -import { Localized } from '@fluent/react'; -import { TipBox } from './commons/TipBox'; -import { useAppContext } from '../hooks/app'; -import { TrackingPauseButton } from './TrackingPauseButton'; +import { WidgetsComponent } from './WidgetsComponent'; export function MainLayoutRoute({ children, background = true, widgets = true, + isMobile = undefined, }: { children: ReactNode; background?: boolean; + isMobile?: boolean; widgets?: boolean; }) { + const { height, ref: navRef } = useElemSize(); const { layoutHeight, ref } = useLayout(); const { layoutWidth, ref: refw } = useLayout(); - const { config } = useConfig(); - const { useRPCPacket, sendRPCPacket } = useWebsocketAPI(); - const { trackers } = useAppContext(); - const [driftCompensationEnabled, setDriftCompensationEnabled] = - useState(false); + const { sendRPCPacket } = useWebsocketAPI(); const [ProportionsLastPageOpen, setProportionsLastPageOpen] = useState(true); - const { statuses } = useStatusContext(); - const unprioritizedStatuses = useMemo( - () => Object.values(statuses).filter((status) => !status.prioritized), - [statuses] - ); useEffect(() => { sendRPCPacket(RpcMessage.SettingsRequest, new SettingsRequestT()); }, []); - useRPCPacket(RpcMessage.SettingsResponse, (settings: SettingsResponseT) => { - if (settings.driftCompensation != null) - setDriftCompensationEnabled(settings.driftCompensation.enabled); - }); - function usePageChanged(callback: () => void) { useEffect(() => { callback(); @@ -87,69 +63,34 @@ export function MainLayoutRoute({ return ( <> -
-
- +
+
+ {!isMobile && }
{children}
- {widgets && ( + {!isMobile && widgets && (
-
- - - {config?.debug && ( - - )} - - - {driftCompensationEnabled && ( - - )} -
-
- -
-
- {unprioritizedStatuses.map((status) => ( - - - {`Warning, you should fix ${ - StatusData[status.dataType] - }`} - - - ))} -
- {config?.debug && ( -
- -
- )} +
)}
+
{isMobile && }
); diff --git a/gui/src/components/Navbar.tsx b/gui/src/components/Navbar.tsx index 558c1d68c..a01705e19 100644 --- a/gui/src/components/Navbar.tsx +++ b/gui/src/components/Navbar.tsx @@ -8,6 +8,7 @@ import { HumanIcon } from './commons/icon/HumanIcon'; import { RulerIcon } from './commons/icon/RulerIcon'; import { SparkleIcon } from './commons/icon/SparkleIcon'; import { WrenchIcon } from './commons/icon/WrenchIcons'; +import { useBreakpoint } from '../hooks/breakpoint'; export function NavButton({ to, @@ -31,7 +32,9 @@ export function NavButton({ to={to} state={state} className={classnames( - 'flex flex-col justify-center gap-4 w-[85px] py-3 rounded-md group select-text', + 'flex flex-col justify-center xs:gap-4 mobile:gap-2', + 'xs:w-[85px] mobile:w-[80px] mobile:h-[80px]', + 'xs:py-3 mobile:py-4 rounded-md mobile:rounded-b-none group select-text', { 'bg-accent-background-50 fill-accent-background-20': doesMatch, 'hover:bg-background-70': !doesMatch, @@ -60,41 +63,56 @@ export function NavButton({ ); } -export function Navbar() { +export function MainLinks() { const { l10n } = useLocalization(); return ( + <> + }> + {l10n.getString('navbar-home')} + + } + > + {l10n.getString('navbar-trackers_assign')} + + } + > + {l10n.getString('navbar-mounting')} + + } + > + {l10n.getString('navbar-body_proportions')} + + }> + {l10n.getString('navbar-onboarding')} + + + ); +} + +export function Navbar() { + const { isMobile } = useBreakpoint('mobile'); + const { l10n } = useLocalization(); + + return isMobile ? ( +
+ +
+ ) : (
- }> - {l10n.getString('navbar-home')} - - } - > - {l10n.getString('navbar-trackers_assign')} - - } - > - {l10n.getString('navbar-mounting')} - - } - > - {l10n.getString('navbar-body_proportions')} - - }> - {l10n.getString('navbar-onboarding')} - +
{ + const url = `https://github.com/${GH_REPO}/releases`; + open(url).catch(() => window.open(url, '_blank')); + }} + > + {(__VERSION_TAG__ || __COMMIT_HASH__) + (__GIT_CLEAN__ ? '' : '-dirty')} +
+ ); +} export function TopBar({ progress, @@ -25,6 +45,8 @@ export function TopBar({ children?: ReactNode; progress?: number; }) { + const isTauri = useIsTauri(); + const { isMobile } = useBreakpoint('mobile'); const { useRPCPacket, sendRPCPacket } = useWebsocketAPI(); const version = useContext(VersionContext); const [localIp, setLocalIp] = useState(null); @@ -44,108 +66,147 @@ export function TopBar({ ); return ( -
-
-
- - - -
- SlimeVR -
-
{ - const url = `https://github.com/${GH_REPO}/releases`; - open(url).catch(() => window.open(url, '_blank')); - }} - > - {(__VERSION_TAG__ || __COMMIT_HASH__) + - (__GIT_CLEAN__ ? '' : '-dirty')} -
- {doesMatchSettings && ( -
- {localIp || 'unknown local ip'} -
- )} - {version && ( -
{ - const url = document.body.classList.contains('windows_nt') - ? 'https://slimevr.dev/download' - : `https://github.com/${GH_REPO}/releases/latest`; - open(url).catch(() => window.open(url, '_blank')); - }} - > - -
- )} -
-
-
+ <> +
- {progress !== undefined && ( - +
+ + + + {(isTauri || !isMobile) && ( +
+ SlimeVR +
+ )} + {!isMobile && ( + <> + + {doesMatchSettings && ( +
+ {localIp || 'unknown local ip'} +
+ )} + + )} + + {version && ( +
{ + const url = document.body.classList.contains('windows_nt') + ? 'https://slimevr.dev/download' + : `https://github.com/${GH_REPO}/releases/latest`; + open(url).catch(() => window.open(url, '_blank')); + }} + > + +
+ )} +
+
+
+ {!isMobile && ( + <> +
+ {progress !== undefined && ( + + )} +
+ + )} + + {!isTauri && ( +
+
+ SlimeVR +
+
+ )} +
+
+ + + + + {!isMobile && ( +
+ open(DOCS_SITE).catch(() => window.open(DOCS_SITE, '_blank')) + } + > + +
+ )} + + {isTauri && ( + <> +
appWindow.minimize()} + > + +
+
appWindow.toggleMaximize()} + > + +
+
appWindow.close()} + > + +
+ )}
-
-
- open(DOCS_SITE).catch(() => window.open(DOCS_SITE, '_blank')) - } - > - + {isMobile && progress !== undefined && ( +
+
-
appWindow.minimize()} - > - -
-
appWindow.toggleMaximize()} - > - -
-
appWindow.close()} - > - -
-
-
+ )} + ); } diff --git a/gui/src/components/WidgetsComponent.tsx b/gui/src/components/WidgetsComponent.tsx new file mode 100644 index 000000000..be0912a6f --- /dev/null +++ b/gui/src/components/WidgetsComponent.tsx @@ -0,0 +1,75 @@ +import { Localized } from '@fluent/react'; +import { BVHButton } from './BVHButton'; +import { ClearDriftCompensationButton } from './ClearDriftCompensationButton'; +import { TrackingPauseButton } from './TrackingPauseButton'; +import { ResetButton } from './home/ResetButton'; +import { OverlayWidget } from './widgets/OverlayWidget'; +import { TipBox } from './commons/TipBox'; +import { DeveloperModeWidget } from './widgets/DeveloperModeWidget'; +import { useConfig } from '../hooks/config'; +import { + ResetType, + RpcMessage, + SettingsResponseT, + StatusData, +} from 'solarxr-protocol'; +import { useMemo, useState } from 'react'; +import { parseStatusToLocale, useStatusContext } from '../hooks/status-system'; +import { useWebsocketAPI } from '../hooks/websocket-api'; +import { useAppContext } from '../hooks/app'; + +export function WidgetsComponent() { + const { config } = useConfig(); + const { useRPCPacket } = useWebsocketAPI(); + const [driftCompensationEnabled, setDriftCompensationEnabled] = + useState(false); + const { trackers } = useAppContext(); + const { statuses } = useStatusContext(); + const unprioritizedStatuses = useMemo( + () => Object.values(statuses).filter((status) => !status.prioritized), + [statuses] + ); + + useRPCPacket(RpcMessage.SettingsResponse, (settings: SettingsResponseT) => { + if (settings.driftCompensation != null) + setDriftCompensationEnabled(settings.driftCompensation.enabled); + }); + + return ( + <> +
+ + + {config?.debug && ( + + )} + + + {driftCompensationEnabled && ( + + )} +
+
+ +
+
+ {unprioritizedStatuses.map((status) => ( + + + {`Warning, you should fix ${StatusData[status.dataType]}`} + + + ))} +
+ {config?.debug && ( +
+ +
+ )} + + ); +} diff --git a/gui/src/components/commons/BaseModal.tsx b/gui/src/components/commons/BaseModal.tsx index e2861ac19..8b28cd4db 100644 --- a/gui/src/components/commons/BaseModal.tsx +++ b/gui/src/components/commons/BaseModal.tsx @@ -25,7 +25,7 @@ export function BaseModal({ props.className || classNames( 'items-center focus:ring-transparent focus:ring-offset-transparent', - 'focus:outline-transparent outline-none bg-background-60 p-6 rounded-lg', + 'focus:outline-transparent outline-none bg-background-60 p-6 rounded-lg m-2', 'text-background-10' ) } diff --git a/gui/src/components/commons/BodyInteractions.tsx b/gui/src/components/commons/BodyInteractions.tsx index 3907dc68a..a8a6bb123 100644 --- a/gui/src/components/commons/BodyInteractions.tsx +++ b/gui/src/components/commons/BodyInteractions.tsx @@ -2,6 +2,7 @@ import classNames from 'classnames'; import { ReactNode, useLayoutEffect, useRef, useState } from 'react'; import { BodyPart } from 'solarxr-protocol'; import { PersonFrontIcon } from './PersonFrontIcon'; +import { useBreakpoint } from '../../hooks/breakpoint'; export function BodyInteractions({ leftControls, @@ -22,6 +23,8 @@ export function BodyInteractions({ onSelectRole: (role: BodyPart) => void; highlightedRoles: BodyPart[]; }) { + const { isMobile } = useBreakpoint('mobile'); + const personRef = useRef(null); const leftContainerRef = useRef(null); const rightContainerRef = useRef(null); @@ -123,7 +126,7 @@ export function BodyInteractions({ controlPosition.left < slot.left ? controlPosition.width : 0; const constolLeft = controlPosition.left + offsetX; - const LINE_BREAK_WIDTH = 40; + const LINE_BREAK_WIDTH = isMobile ? 20 : 40; const leftOffsetX = LINE_BREAK_WIDTH * (controlPosition.left < slot.left ? -1 : 1); @@ -160,7 +163,7 @@ export function BodyInteractions({ ref={personRef} className={classNames( 'relative w-full flex justify-center', - variant === 'tracker-select' && 'mx-10' + variant === 'tracker-select' && 'mobile:mx-0 xs:mx-10' )} > diff --git a/gui/src/components/commons/Dropdown.tsx b/gui/src/components/commons/Dropdown.tsx index 21a6e693c..fad5fd154 100644 --- a/gui/src/components/commons/Dropdown.tsx +++ b/gui/src/components/commons/Dropdown.tsx @@ -14,14 +14,16 @@ export function Dropdown({ direction = 'up', variant = 'primary', alignment = 'right', + display = 'fit', placeholder, control, name, items = [], }: { direction?: DropdownDirection; - variant?: 'primary' | 'secondary'; + variant?: 'primary' | 'secondary' | 'tertiary'; alignment?: 'right' | 'left'; + display?: 'fit' | 'block'; placeholder: string; control: Control; name: string; @@ -37,10 +39,39 @@ export function Dropdown({ } } - document.addEventListener('wheel', onWheelEvent, { passive: true }); + function onTouchEvent(event: TouchEvent) { + // Check if we touch scroll outside of the dropdown + if ( + isOpen && + !document + .querySelector('div.dropdown-scroll') + ?.contains(event.target as HTMLDivElement) + ) { + setOpen(false); + } + } + function onClick(event: MouseEvent) { + const isInDropdownScroll = document + .querySelector('div.dropdown-scroll') + ?.contains(event.target as HTMLDivElement); + const isInDropdown = document + .querySelector('div.dropdown') + ?.contains(event.target as HTMLDivElement); + if (isOpen && !isInDropdownScroll && !isInDropdown) { + setOpen(false); + } + } + + document.addEventListener('click', onClick, false); + document.addEventListener('touchmove', onTouchEvent, false); // TS doesn't let me specify { passive: true }, but I believe it will work anyways - return () => document.removeEventListener('wheel', onWheelEvent); + document.addEventListener('wheel', onWheelEvent, { passive: true }); + return () => { + document.removeEventListener('wheel', onWheelEvent); + document.removeEventListener('click', onClick); + document.removeEventListener('touchmove', onTouchEvent); + }; }, [isOpen]); return ( @@ -55,15 +86,23 @@ export function Dropdown({ onClick={() => setOpen(false)} >
)} -
+
setOpen((open) => !open)} onKeyDown={(ev) => a11yClick(ev) && setOpen((open) => !open)} @@ -101,21 +140,32 @@ export function Dropdown({ className={classNames( 'absolute z-10 rounded shadow min-w-max max-h-[50vh]', 'overflow-y-auto dropdown-scroll', + display === 'fit' && 'w-fit', + display === 'block' && 'w-full', direction === 'up' && 'bottom-[45px]', direction === 'down' && 'top-[45px]', variant == 'primary' && 'bg-background-60', variant == 'secondary' && 'bg-background-70', + variant == 'tertiary' && 'bg-accent-background-30', alignment === 'right' && 'right-0', alignment === 'left' && 'left-0' )} > -
    +
      {items.map((item) => (
    • { onChange(item.value); diff --git a/gui/src/components/commons/TipBox.tsx b/gui/src/components/commons/TipBox.tsx index 441b5ee6d..61ec3735e 100644 --- a/gui/src/components/commons/TipBox.tsx +++ b/gui/src/components/commons/TipBox.tsx @@ -60,7 +60,7 @@ export function WarningBox({
      {children} diff --git a/gui/src/components/commons/Typography.tsx b/gui/src/components/commons/Typography.tsx index 15f3f66cb..2f8ae100e 100644 --- a/gui/src/components/commons/Typography.tsx +++ b/gui/src/components/commons/Typography.tsx @@ -9,7 +9,12 @@ export function Typography({ children, italic = false, }: { - variant?: 'main-title' | 'section-title' | 'standard' | 'vr-accessible'; + variant?: + | 'main-title' + | 'section-title' + | 'standard' + | 'vr-accessible' + | 'mobile-title'; bold?: boolean; italic?: boolean; block?: boolean; @@ -26,6 +31,7 @@ export function Typography({ const tags = { 'main-title': 'h1', 'section-title': 'h2', + 'mobile-title': 'h1', standard: 'p', 'vr-accessible': 'p', }; @@ -37,6 +43,8 @@ export function Typography({ { className: classNames([ 'transition-colors', + variant === 'mobile-title' && + 'xs:text-main-title mobile:text-section-title', variant === 'main-title' && 'text-main-title', variant === 'section-title' && 'text-section-title', variant === 'standard' && diff --git a/gui/src/components/home/Home.tsx b/gui/src/components/home/Home.tsx index d34b2a690..c6a395554 100644 --- a/gui/src/components/home/Home.tsx +++ b/gui/src/components/home/Home.tsx @@ -1,5 +1,5 @@ import { Localized, useLocalization } from '@fluent/react'; -import { useNavigate } from 'react-router-dom'; +import { NavLink, useNavigate } from 'react-router-dom'; import { StatusData, TrackerDataT } from 'solarxr-protocol'; import { useConfig } from '../../hooks/config'; import { useTrackers } from '../../hooks/tracker'; @@ -13,6 +13,7 @@ import { } from '../../hooks/status-system'; import { useMemo } from 'react'; import { WarningBox } from '../commons/TipBox'; +import { HeadsetIcon } from '../commons/icon/HeadsetIcon'; const DONT_REPEAT_STATUSES = [StatusData.StatusTrackerReset]; @@ -39,13 +40,20 @@ export function Home() { }, [statuses]); return ( -
      -
      - {filteredStatuses - .filter(([, status]) => status.prioritized) - .map(([, status]) => ( -
      +
      + + + +
      +
      + {filteredStatuses + .filter(([, status]) => status.prioritized) + .map(([, status]) => ( @@ -53,42 +61,44 @@ export function Home() { {`Warning, you should fix ${StatusData[status.dataType]}`} + ))} +
      +
      + {trackers.length === 0 && ( +
      + + {l10n.getString('home-no_trackers')} +
      - ))} -
      - {trackers.length === 0 && ( -
      - - {l10n.getString('home-no_trackers')} - -
      - )} + )} - {!config?.debug && trackers.length > 0 && ( -
      - {trackers.map(({ tracker, device }, index) => ( - sendToSettings(tracker)} - smol - interactable - warning={Object.values(statuses).some((status) => - trackerStatusRelated(tracker, status) - )} - /> - ))} + {!config?.debug && trackers.length > 0 && ( +
      + {trackers.map(({ tracker, device }, index) => ( + sendToSettings(tracker)} + smol + interactable + warning={Object.values(statuses).some((status) => + trackerStatusRelated(tracker, status) + )} + /> + ))} +
      + )} + {config?.debug && trackers.length > 0 && ( +
      + sendToSettings(tracker)} + > +
      + )}
      - )} - {config?.debug && trackers.length > 0 && ( -
      - sendToSettings(tracker)} - > -
      - )} +
      ); } diff --git a/gui/src/components/onboarding/BodyAssignment.tsx b/gui/src/components/onboarding/BodyAssignment.tsx index fd7017619..65ec28b7d 100644 --- a/gui/src/components/onboarding/BodyAssignment.tsx +++ b/gui/src/components/onboarding/BodyAssignment.tsx @@ -34,12 +34,14 @@ export function BodyAssignment({ rolesWithErrors = {}, highlightedRoles = [], onlyAssigned = false, + width, }: { advanced: boolean; onlyAssigned?: boolean; rolesWithErrors?: Partial>; highlightedRoles?: BodyPart[]; onRoleSelected: (role: BodyPart) => void; + width?: number; }) { const { useAssignedTrackers } = useTrackers(); @@ -74,6 +76,7 @@ export function BodyAssignment({ return ( <> (); + const { isMobile } = useBreakpoint('mobile'); const { state } = useOnboarding(); return !state.alonePage ? ( @@ -13,15 +15,15 @@ export function OnboardingLayout({ children }: { children: ReactNode }) {
      {children}
      ) : ( - -
      {children}
      + +
      {children}
      ); } diff --git a/gui/src/components/onboarding/SkipSetupButton.tsx b/gui/src/components/onboarding/SkipSetupButton.tsx index 860989969..908d8c3dd 100644 --- a/gui/src/components/onboarding/SkipSetupButton.tsx +++ b/gui/src/components/onboarding/SkipSetupButton.tsx @@ -30,7 +30,7 @@ export function SkipSetupButton({ className={classNames( 'text-background-40 hover:text-background-30', 'stroke-background-40 hover:stroke-background-30', - 'absolute -top-10 right-0' + 'absolute xs:-top-10 xs:right-4 mobile:top-0 mobile:right-0' )} onClick={onClick} > diff --git a/gui/src/components/onboarding/SkipSetupWarningModal.tsx b/gui/src/components/onboarding/SkipSetupWarningModal.tsx index 6f53e3c9f..9879eed08 100644 --- a/gui/src/components/onboarding/SkipSetupWarningModal.tsx +++ b/gui/src/components/onboarding/SkipSetupWarningModal.tsx @@ -39,7 +39,7 @@ export function SkipSetupWarningModal({ overlayClassName={props.overlayClassName} >
      -
      +
      }}> Warning: The setup is needed for good tracking, this is diff --git a/gui/src/components/onboarding/StepperSlider.tsx b/gui/src/components/onboarding/StepperSlider.tsx index 84c0ffe0f..1ef25df21 100644 --- a/gui/src/components/onboarding/StepperSlider.tsx +++ b/gui/src/components/onboarding/StepperSlider.tsx @@ -40,7 +40,7 @@ export function StepContainer({ return (
      {type === 'numbered' && ( -
      +
      {step + 1} diff --git a/gui/src/components/onboarding/pages/CalibrationTutorial.tsx b/gui/src/components/onboarding/pages/CalibrationTutorial.tsx index 570015acb..edd39eb7c 100644 --- a/gui/src/components/onboarding/pages/CalibrationTutorial.tsx +++ b/gui/src/components/onboarding/pages/CalibrationTutorial.tsx @@ -77,11 +77,11 @@ export function CalibrationTutorialPage() { modalVisible={skipWarning} onClick={() => setSkipWarning(true)} > -
      -
      +
      +
      - + {l10n.getString('onboarding-calibration_tutorial')} @@ -97,6 +97,11 @@ export function CalibrationTutorialPage() {
      +
      +
      + +
      +
      @@ -116,11 +121,11 @@ export function CalibrationTutorialPage() {
      {progressText}
      -
      +
      @@ -132,7 +137,7 @@ export function CalibrationTutorialPage() { }} disabled={isCounting} hidden={CalibrationStatus.SUCCESS === calibrationStatus} - className="ml-auto" + className="xs:ml-auto" > {l10n.getString('onboarding-calibration_tutorial-calibrate')} @@ -140,7 +145,7 @@ export function CalibrationTutorialPage() { variant="primary" to="/onboarding/assign-tutorial" className={classNames( - 'ml-auto', + 'xs:ml-auto', CalibrationStatus.SUCCESS !== calibrationStatus && 'hidden' )} > @@ -149,10 +154,9 @@ export function CalibrationTutorialPage() {
      -
      -
      +
      +
      - {/* */}
      diff --git a/gui/src/components/onboarding/pages/ConnectTracker.tsx b/gui/src/components/onboarding/pages/ConnectTracker.tsx index d266f1846..b5a796c0f 100644 --- a/gui/src/components/onboarding/pages/ConnectTracker.tsx +++ b/gui/src/components/onboarding/pages/ConnectTracker.tsx @@ -23,6 +23,7 @@ import { TrackerCard } from '../../tracker/TrackerCard'; import { SkipSetupWarningModal } from '../SkipSetupWarningModal'; import { SkipSetupButton } from '../SkipSetupButton'; import { useBnoExists } from '../../../hooks/imu-logic'; +import { useBreakpoint } from '../../../hooks/breakpoint'; const BOTTOM_HEIGHT = 80; @@ -57,9 +58,12 @@ const statusProgressMap = { }; export function ConnectTrackersPage() { + const { isMobile } = useBreakpoint('mobile'); const { l10n } = useLocalization(); - const { layoutHeight, ref } = useLayout(); - const { trackers, useConnectedTrackers } = useTrackers(); + const { layoutHeight, ref } = isMobile + ? { layoutHeight: 0, ref: undefined } + : useLayout(); + const { useConnectedTrackers } = useTrackers(); const { applyProgress, state, skipSetup } = useOnboarding(); const navigate = useNavigate(); const { sendRPCPacket, useRPCPacket } = useWebsocketAPI(); @@ -128,13 +132,13 @@ export function ConnectTrackersPage() { }, [provisioningStatus]); return ( -
      +
      setSkipWarning(true)} > -
      +
      {l10n.getString('onboarding-connect_tracker-title')} @@ -225,7 +229,7 @@ export function ConnectTrackersPage() {
      -
      +
      {l10n.getString('onboarding-connect_tracker-connected_trackers', { amount: connectedTrackers.length, @@ -233,20 +237,20 @@ export function ConnectTrackersPage() {
      -
      +
      {Array.from({ ...connectedTrackers, - length: Math.max(trackers.length, 20), + length: Math.max(connectedTrackers.length, isMobile ? 1 : 20), }).map((tracker, index) => (
      {!tracker && (
      -
      +
      - + {l10n.getString('onboarding-home')}
      -
      +
      +
      setSkipWarning(true)} > -
      -
      -
      - - {l10n.getString('onboarding-reset_tutorial')} - - - {l10n.getString('onboarding-reset_tutorial-description')} - -
      - +
      +
      + + {l10n.getString('onboarding-reset_tutorial')} + + + {l10n.getString('onboarding-reset_tutorial-description')} + +
      + - + - -
      -
      = order.length && 'hidden' + 'ml-auto', + order.length > curIndex + 1 && 'hidden' )} > - - {l10n.getString(`onboarding-reset_tutorial-${curIndex}`, { - taps: tapSettings[curIndex], - })} - -
      + {l10n.getString('onboarding-continue')} +
      -
      - -
      = order.length && 'hidden' - )} - > - - {l10n.getString(`onboarding-reset_tutorial-${curIndex}`, { - taps: tapSettings[curIndex], - })} - -
      +
      = order.length && 'hidden' + )} + > + + {l10n.getString(`onboarding-reset_tutorial-${curIndex}`, { + taps: tapSettings[curIndex], + })} + +
      +
      +
      + +
      = order.length && 'hidden' + )} + > + + {l10n.getString(`onboarding-reset_tutorial-${curIndex}`, { + taps: tapSettings[curIndex], + })} +
      diff --git a/gui/src/components/onboarding/pages/WifiCreds.tsx b/gui/src/components/onboarding/pages/WifiCreds.tsx index 382dc7c09..f17538d6c 100644 --- a/gui/src/components/onboarding/pages/WifiCreds.tsx +++ b/gui/src/components/onboarding/pages/WifiCreds.tsx @@ -28,13 +28,13 @@ export function WifiCredsPage() { className="flex flex-col w-full h-full" onSubmit={handleSubmit(submitWifiCreds)} > -
      +
      setSkipWarning(true)} > -
      +
      {l10n.getString('onboarding-wifi_creds')} diff --git a/gui/src/components/onboarding/pages/assignment-preparation/AssignmentTutorial.tsx b/gui/src/components/onboarding/pages/assignment-preparation/AssignmentTutorial.tsx index 864c9ba20..72148ed62 100644 --- a/gui/src/components/onboarding/pages/assignment-preparation/AssignmentTutorial.tsx +++ b/gui/src/components/onboarding/pages/assignment-preparation/AssignmentTutorial.tsx @@ -29,67 +29,69 @@ export function AssignmentTutorialPage() { modalVisible={skipWarning} onClick={() => setSkipWarning(true)} > -
      -
      - - {l10n.getString('onboarding-assignment_tutorial')} - -
      -
      -
      -
      -
      - - {l10n.getString( - 'onboarding-assignment_tutorial-first_step' - )} - -
      -
      - -
      -
      -
      -
      - - {l10n.getString( - 'onboarding-assignment_tutorial-second_step' - )} - -
      -
      - -
      -
      - - {l10n.getString( - 'onboarding-assignment_tutorial-second_step-continuation' - )} - -
      -
      - -
      -
      +
      +
      +
      + + {l10n.getString('onboarding-assignment_tutorial')} +
      -
      - - +
      +
      +
      +
      + + {l10n.getString( + 'onboarding-assignment_tutorial-first_step' + )} + +
      +
      + +
      +
      +
      +
      + + {l10n.getString( + 'onboarding-assignment_tutorial-second_step' + )} + +
      +
      + +
      +
      + + {l10n.getString( + 'onboarding-assignment_tutorial-second_step-continuation' + )} + +
      +
      + +
      +
      +
      +
      + + +
      diff --git a/gui/src/components/onboarding/pages/body-proportions/AutomaticProportions.tsx b/gui/src/components/onboarding/pages/body-proportions/AutomaticProportions.tsx index 6f47aec1a..5e87a8794 100644 --- a/gui/src/components/onboarding/pages/body-proportions/AutomaticProportions.tsx +++ b/gui/src/components/onboarding/pages/body-proportions/AutomaticProportions.tsx @@ -39,7 +39,7 @@ export function AutomaticProportionsPage() { return ( -
      +
      -
      -
      - -
      + {!isCounting ? l10n.getString('reset-reset_all') : timer} +
      +
      - + {children}
      @@ -39,6 +40,8 @@ export function BodyProportions({ type: 'linear' | 'ratio'; variant: 'onboarding' | 'alone'; }) { + const { isMobile } = useBreakpoint('mobile'); + const [bodyParts, _ratioMode, currentSelection, dispatch, setRatioMode] = useManualProportions(); const { currentLocales } = useLocaleConfig(); @@ -69,8 +72,8 @@ export function BodyProportions({
      <> @@ -79,7 +82,7 @@ export function BodyProportions({ 'index' in props && props.index !== undefined ? props.bones[props.index].value : originalValue; - const selected = currentSelection.label === label; + const selected = isMobile || currentSelection.label === label; const selectNew = () => { switch (type) { @@ -130,8 +133,9 @@ export function BodyProportions({ > {!precise && ( - type === LabelType.GroupPart + onClick={() => { + selectNew(); + return type === LabelType.GroupPart ? dispatch({ type: ProportionChangeType.Ratio, value: -0.05, @@ -139,15 +143,16 @@ export function BodyProportions({ : dispatch({ type: ProportionChangeType.Linear, value: -5, - }) - } + }); + }} > {configFormat.format(-5)} )} - type === LabelType.GroupPart + onClick={() => { + selectNew(); + return type === LabelType.GroupPart ? dispatch({ type: ProportionChangeType.Ratio, value: -0.01, @@ -155,15 +160,16 @@ export function BodyProportions({ : dispatch({ type: ProportionChangeType.Linear, value: -1, - }) - } + }); + }} > {configFormat.format(-1)} {precise && ( - type === LabelType.GroupPart + onClick={() => { + selectNew(); + return type === LabelType.GroupPart ? dispatch({ type: ProportionChangeType.Ratio, value: -0.005, @@ -171,8 +177,8 @@ export function BodyProportions({ : dispatch({ type: ProportionChangeType.Linear, value: -0.5, - }) - } + }); + }} > {configFormat.format(-0.5)} @@ -185,14 +191,14 @@ export function BodyProportions({
      {l10n.getString(label)} - + {type === LabelType.GroupPart ? /* Make number rounding so it's based on .5 decimals */ percentageFormat.format(Math.round(value * 200) / 200) @@ -213,8 +219,9 @@ export function BodyProportions({ > {precise && ( - type === LabelType.GroupPart + onClick={() => { + selectNew(); + return type === LabelType.GroupPart ? dispatch({ type: ProportionChangeType.Ratio, value: 0.005, @@ -222,15 +229,16 @@ export function BodyProportions({ : dispatch({ type: ProportionChangeType.Linear, value: 0.5, - }) - } + }); + }} > {configFormat.format(+0.5)} )} - type === LabelType.GroupPart + onClick={() => { + selectNew(); + return type === LabelType.GroupPart ? dispatch({ type: ProportionChangeType.Ratio, value: 0.01, @@ -238,15 +246,16 @@ export function BodyProportions({ : dispatch({ type: ProportionChangeType.Linear, value: 1, - }) - } + }); + }} > {configFormat.format(+1)} {!precise && ( - type === LabelType.GroupPart + onClick={() => { + selectNew(); + return type === LabelType.GroupPart ? dispatch({ type: ProportionChangeType.Ratio, value: 0.05, @@ -254,8 +263,8 @@ export function BodyProportions({ : dispatch({ type: ProportionChangeType.Linear, value: 5, - }) - } + }); + }} > {configFormat.format(+5)} diff --git a/gui/src/components/onboarding/pages/body-proportions/ManualProportions.tsx b/gui/src/components/onboarding/pages/body-proportions/ManualProportions.tsx index 553dc7476..04b571074 100644 --- a/gui/src/components/onboarding/pages/body-proportions/ManualProportions.tsx +++ b/gui/src/components/onboarding/pages/body-proportions/ManualProportions.tsx @@ -11,11 +11,45 @@ import { useLocalization } from '@fluent/react'; import { useEffect, useMemo, useState } from 'react'; import { SkipSetupWarningModal } from '../../SkipSetupWarningModal'; import { SkipSetupButton } from '../../SkipSetupButton'; +import { useBreakpoint } from '../../../../hooks/breakpoint'; + +export function ButtonsControl() { + const { l10n } = useLocalization(); + const { state } = useOnboarding(); + const { sendRPCPacket } = useWebsocketAPI(); + + const resetAll = () => { + sendRPCPacket( + RpcMessage.SkeletonResetAllRequest, + new SkeletonResetAllRequestT() + ); + }; + + return ( + <> + + + {!state.alonePage && ( + + )} + + ); +} export function ManualProportionsPage() { + const { isMobile } = useBreakpoint('mobile'); const { l10n } = useLocalization(); const { applyProgress, skipSetup, state } = useOnboarding(); - const { sendRPCPacket } = useWebsocketAPI(); const [skipWarning, setSkipWarning] = useState(false); applyProgress(0.9); @@ -27,28 +61,21 @@ export function ManualProportionsPage() { }); const { precise, ratio } = watch(); - const resetAll = () => { - sendRPCPacket( - RpcMessage.SkeletonResetAllRequest, - new SkeletonResetAllRequestT() - ); - }; - useEffect(() => { localStorage.setItem('ratioMode', ratio.toString()); }, [ratio]); return ( <> -
      +
      setSkipWarning(true)} > -
      +
      -
      +
      {l10n.getString('onboarding-manual_proportions-title')} @@ -67,7 +94,13 @@ export function ManualProportionsPage() { name="precise" variant="toggle" > + {isMobile && ( +
      + +
      + )}
      +
      -
      - - - {!state.alonePage && ( - - )} -
      -
      -
      -
      -
      + {!isMobile && ( +
      + +
      + )}
      - path ? writeTextFile(path, JSON.stringify(data)) : undefined - ) - .catch((err) => { - console.error(err); - saveAs(blob, 'body-proportions.json'); - }); + if (isTauri) { + save({ + filters: [ + { + name: l10n.getString('onboarding-choose_proportions-file_type'), + extensions: ['json'], + }, + ], + defaultPath: 'body-proportions.json', + }) + .then((path) => + path ? writeTextFile(path, JSON.stringify(data)) : undefined + ) + .catch((err) => { + console.error(err); + }); + } else { + saveAs(blob, 'body-proportions.json'); + } } ); @@ -52,17 +57,17 @@ export function ProportionsChoose() { return ( <> -
      +
      setSkipWarning(true)} >
      -
      - - {l10n.getString('onboarding-choose_proportions')} - + + {l10n.getString('onboarding-choose_proportions')} + +
      -
      -
      -
      -
      -
      -
      - - {l10n.getString( - 'onboarding-choose_proportions-manual_proportions' - )} - - - {l10n.getString( - 'onboarding-choose_proportions-manual_proportions-subtitle' - )} - -
      -
      - - {l10n.getString( - 'onboarding-choose_proportions-manual_proportions-description' - )} - -
      +
      div]:grow' + )} + > +
      +
      +
      +
      + + {l10n.getString( + 'onboarding-choose_proportions-manual_proportions' + )} + + + {l10n.getString( + 'onboarding-choose_proportions-manual_proportions-subtitle' + )} +
      +
      + + {l10n.getString( + 'onboarding-choose_proportions-manual_proportions-description' + )} + +
      +
      - -
      +
      -
      -
      -
      -
      - setAnimated(() => true)} - onAnimationEnd={() => setAnimated(() => false)} - src="/images/slimetower.png" - className={classNames( - 'absolute w-1/3 -right-2 -top-32', - animated && 'animate-[bounce_1s_1]' - )} - > - - {l10n.getString( - 'onboarding-choose_proportions-auto_proportions' - )} - - - {l10n.getString( - 'onboarding-choose_proportions-auto_proportions-subtitle' - )} - -
      -
      - - {l10n.getString( - 'onboarding-choose_proportions-auto_proportions-description' - )} - -
      +
      +
      +
      +
      + setAnimated(() => true)} + onAnimationEnd={() => setAnimated(() => false)} + src="/images/slimetower.png" + className={classNames( + 'absolute w-[100px] -right-2 -top-24', + animated && 'animate-[bounce_1s_1]' + )} + > +
      + + {l10n.getString( + 'onboarding-choose_proportions-auto_proportions' + )} + + + {l10n.getString( + 'onboarding-choose_proportions-auto_proportions-subtitle' + )} + +
      +
      + + {l10n.getString( + 'onboarding-choose_proportions-auto_proportions-description' + )} +
      -
      +
      {!state.alonePage && ( - )}
      + {isMobile && ( +
      + +
      + )} +
      -
      +
      -
      - -
      + {!isMobile && ( +
      + +
      + )} ); } diff --git a/gui/src/components/onboarding/pages/body-proportions/autobone-steps/Recording.tsx b/gui/src/components/onboarding/pages/body-proportions/autobone-steps/Recording.tsx index 78348139e..83911f205 100644 --- a/gui/src/components/onboarding/pages/body-proportions/autobone-steps/Recording.tsx +++ b/gui/src/components/onboarding/pages/body-proportions/autobone-steps/Recording.tsx @@ -35,7 +35,7 @@ export function Recording({ nextStep }: { nextStep: () => void }) { )}
      -
        +
          <> {l10n .getString('onboarding-automatic_proportions-recording-steps') diff --git a/gui/src/components/onboarding/pages/body-proportions/autobone-steps/Requirements.tsx b/gui/src/components/onboarding/pages/body-proportions/autobone-steps/Requirements.tsx index d708f48cc..ae01f69bc 100644 --- a/gui/src/components/onboarding/pages/body-proportions/autobone-steps/Requirements.tsx +++ b/gui/src/components/onboarding/pages/body-proportions/autobone-steps/Requirements.tsx @@ -22,7 +22,7 @@ export function RequirementsStep({ 'onboarding-automatic_proportions-requirements-title' )} -
            +
              <> {l10n .getString( @@ -38,7 +38,7 @@ export function RequirementsStep({
      -
      +
      -
        +
          <> {l10n .getString('onboarding-automatic_proportions-recording-steps') @@ -54,7 +54,7 @@ export function StartRecording({
      -
      +
      + {!state.alonePage && ( + - {!state.alonePage && ( - - )} -
      -
      -
      - + )}
      +
      + +
      -
      +
      setSkipWarning(true)} >
      -
      +
      {l10n.getString('onboarding-choose_mounting')} @@ -35,99 +36,106 @@ export function MountingChoose() { {l10n.getString('onboarding-choose_mounting-description')}
      -
      -
      -
      -
      -
      -
      - - {l10n.getString( - 'onboarding-choose_mounting-manual_mounting' - )} - - - {l10n.getString( - 'onboarding-choose_mounting-manual_mounting-subtitle' - )} - -
      -
      - - {l10n.getString( - 'onboarding-choose_mounting-manual_mounting-description' - )} - -
      +
      div]:grow' + )} + > +
      +
      +
      +
      + + {l10n.getString( + 'onboarding-choose_mounting-manual_mounting' + )} + + + {l10n.getString( + 'onboarding-choose_mounting-manual_mounting-subtitle' + )} +
      +
      + + {l10n.getString( + 'onboarding-choose_mounting-manual_mounting-description' + )} + +
      +
      - -
      +
      -
      -
      -
      -
      - - - {l10n.getString( - 'onboarding-choose_mounting-auto_mounting' - )} - - - {l10n.getString( - 'onboarding-choose_mounting-auto_mounting-subtitle' - )} - -
      -
      - - {l10n.getString( - 'onboarding-choose_mounting-auto_mounting-description' - )} - -
      +
      +
      +
      +
      +
      + setAnimated(() => true)} + onAnimationEnd={() => setAnimated(() => false)} + src="/images/boxslime.png" + className={classNames( + 'absolute w-[100px] -right-2 -top-10', + animated && 'animate-[bounce_1s_1]' + )} + > + + {l10n.getString( + 'onboarding-choose_mounting-auto_mounting' + )} + + + {l10n.getString( + 'onboarding-choose_mounting-auto_mounting-subtitle' + )} + +
      +
      + + {l10n.getString( + 'onboarding-choose_mounting-auto_mounting-description' + )} +
      -
      +
      {!state.alonePage && (
      -
      - mounting reset ski pose -
      + {!isMobile && ( +
      + mounting reset ski pose +
      + )} ); } diff --git a/gui/src/components/onboarding/pages/mounting/mounting-steps/Preparation.tsx b/gui/src/components/onboarding/pages/mounting/mounting-steps/Preparation.tsx index eed8d9669..f017c7c3a 100644 --- a/gui/src/components/onboarding/pages/mounting/mounting-steps/Preparation.tsx +++ b/gui/src/components/onboarding/pages/mounting/mounting-steps/Preparation.tsx @@ -3,6 +3,7 @@ import { Button } from '../../../../commons/Button'; import { Typography } from '../../../../commons/Typography'; import { ResetButton } from '../../../../home/ResetButton'; import { useLocalization } from '@fluent/react'; +import { useBreakpoint } from '../../../../../hooks/breakpoint'; export function PreparationStep({ nextStep, @@ -13,11 +14,12 @@ export function PreparationStep({ prevStep: () => void; variant: 'onboarding' | 'alone'; }) { + const { isMobile } = useBreakpoint('mobile'); const { l10n } = useLocalization(); return ( - <> -
      +
      +
      {l10n.getString('onboarding-automatic_mounting-preparation-title')} @@ -35,9 +37,16 @@ export function PreparationStep({
      - -
      -
      + {isMobile && ( +
      + Reset position +
      + )} +
      -
      - Reset position -
      - + {!isMobile && ( +
      + Reset position +
      + )} +
      ); } diff --git a/gui/src/components/onboarding/pages/mounting/mounting-steps/PutTrackersOn.tsx b/gui/src/components/onboarding/pages/mounting/mounting-steps/PutTrackersOn.tsx index fd09780e2..14ad0382e 100644 --- a/gui/src/components/onboarding/pages/mounting/mounting-steps/PutTrackersOn.tsx +++ b/gui/src/components/onboarding/pages/mounting/mounting-steps/PutTrackersOn.tsx @@ -1,3 +1,4 @@ +import { useBreakpoint } from '../../../../../hooks/breakpoint'; import { useTrackers } from '../../../../../hooks/tracker'; import { BodyDisplay } from '../../../../commons/BodyDisplay'; import { Button } from '../../../../commons/Button'; @@ -12,12 +13,13 @@ export function PutTrackersOnStep({ nextStep: () => void; variant: 'alone' | 'onboarding'; }) { + const { isMobile } = useBreakpoint('mobile'); const { trackers } = useTrackers(); const { l10n } = useLocalization(); return ( - <> -
      +
      +
      {l10n.getString( @@ -36,8 +38,20 @@ export function PutTrackersOnStep({
      + {isMobile && ( +
      + +
      + )} +
      -
      +
      -
      - -
      - + {!isMobile && ( +
      + +
      + )} +
      ); } diff --git a/gui/src/components/onboarding/pages/trackers-assign/TrackerAssignment.tsx b/gui/src/components/onboarding/pages/trackers-assign/TrackerAssignment.tsx index 975e61aa8..c24f7c1c3 100644 --- a/gui/src/components/onboarding/pages/trackers-assign/TrackerAssignment.tsx +++ b/gui/src/components/onboarding/pages/trackers-assign/TrackerAssignment.tsx @@ -250,15 +250,17 @@ export function TrackersAssignPage() { onClose={() => closeChokerWarning(true)} accept={() => closeChokerWarning(false)} > -
      +
      setSkipWarning(true)} > -
      -
      -
      +
      +
      +
      +
      +
      {l10n.getString('onboarding-assign_trackers-title')} @@ -287,7 +289,7 @@ export function TrackersAssignPage() {
      )} -
      +
      {!state.alonePage && ( <>
      -
      +
      -
      -
      -
      -
      - + {l10n.getString('tracker_selection_menu-' + BodyPart[bodyPart])}
      @@ -57,7 +57,7 @@ export function TrackerSelectionMenu({
      @@ -67,7 +67,7 @@ export function TrackerSelectionMenu({ {l10n.getString('tracker_selection_menu-unassigned')} -
      +
      {unassignedTrackers.map((fd, index) => ( {l10n.getString('tracker_selection_menu-assigned')} -
      +
      {assignedTrackers.map((fd, index) => (
      -
      -
      -
      diff --git a/gui/src/components/settings/SettingsLayout.tsx b/gui/src/components/settings/SettingsLayout.tsx index 408bc0168..a4398b1be 100644 --- a/gui/src/components/settings/SettingsLayout.tsx +++ b/gui/src/components/settings/SettingsLayout.tsx @@ -1,22 +1,106 @@ -import { ReactNode } from 'react'; -import { useLayout } from '../../hooks/layout'; +import { ReactNode, useEffect } from 'react'; +import { useElemSize, useLayout } from '../../hooks/layout'; import { Navbar } from '../Navbar'; import { TopBar } from '../TopBar'; import { SettingsSidebar } from './SettingsSidebar'; +import { useBreakpoint } from '../../hooks/breakpoint'; +import { Dropdown } from '../commons/Dropdown'; +import { useForm } from 'react-hook-form'; +import { useLocalization } from '@fluent/react'; +import { useLocation, useNavigate } from 'react-router-dom'; + +export function SettingSelectorMobile() { + const { l10n } = useLocalization(); + const navigate = useNavigate(); + const { pathname } = useLocation(); + + const links: { label: string; value: { url: string; scrollTo?: string } }[] = + [ + { + label: l10n.getString('settings-sidebar-general'), + value: { url: '/settings/trackers', scrollTo: 'steamvr' }, + }, + { + label: l10n.getString('settings-sidebar-osc_router'), + value: { url: '/settings/osc/router', scrollTo: 'router' }, + }, + { + label: l10n.getString('settings-sidebar-osc_trackers'), + value: { url: '/settings/osc/vrchat', scrollTo: 'vrchat' }, + }, + { + label: 'VMC', + value: { url: '/settings/osc/vmc', scrollTo: 'vmc' }, + }, + { + label: l10n.getString('settings-sidebar-serial'), + value: { url: '/settings/serial' }, + }, + ]; + + const { control, watch, handleSubmit, setValue } = useForm<{ + link: string; + }>({ + defaultValues: { link: links[0].value.url }, + }); + + useEffect(() => { + // This works because the component gets mounted/unmounted when switching beween desktop or mobile layout + setValue('link', pathname, { shouldDirty: false, shouldTouch: false }); + }, []); + + useEffect(() => { + const subscription = watch(() => handleSubmit(onSubmit)()); + return () => subscription.unsubscribe(); + }, []); + + const onSubmit = ({ link }: { link: string }) => { + const item = links.find(({ value: { url } }) => url === link); + + if (!item) return; + navigate(item.value.url, { state: { scrollTo: item.value.scrollTo } }); + }; + + return ( +
      + ({ + label, + value, + }))} + variant="tertiary" + direction="down" + // There is always an option selected placholder is not used + placeholder="" + name="link" + > +
      + ); +} export function SettingsLayoutRoute({ children }: { children: ReactNode }) { const { layoutHeight, ref } = useLayout(); - + const { height, ref: navRef } = useElemSize(); + const { isMobile } = useBreakpoint('mobile'); return ( <>
      -
      - +
      + {!isMobile && }
      - -
      - {children} + {!isMobile && } +
      + {isMobile && } +
      + {children} +
      +
      {isMobile && }
      diff --git a/gui/src/components/settings/SettingsPageLayout.tsx b/gui/src/components/settings/SettingsPageLayout.tsx index 3f2503b5b..74201d9b1 100644 --- a/gui/src/components/settings/SettingsPageLayout.tsx +++ b/gui/src/components/settings/SettingsPageLayout.tsx @@ -1,7 +1,36 @@ import classNames from 'classnames'; -import { ReactNode } from 'react'; +import { ReactNode, useEffect, useRef } from 'react'; +import { useLocation } from 'react-router-dom'; export function SettingsPageLayout({ + children, + className, + ...props +}: { + children: ReactNode; +} & React.HTMLAttributes) { + const pageRef = useRef(null); + const { state } = useLocation(); + + useEffect(() => { + const typedState: { scrollTo: string } = state; + if (!pageRef.current || !typedState || !typedState.scrollTo) { + return; + } + const elem = pageRef.current.querySelector(`#${typedState.scrollTo}`); + if (elem) { + elem.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + }, [state]); + + return ( +
      + {children} +
      + ); +} + +export function SettingsPagePaneLayout({ children, className, icon, @@ -13,13 +42,13 @@ export function SettingsPageLayout({ return (
      -
      -
      +
      +
      {icon}
      diff --git a/gui/src/components/settings/SettingsSidebar.tsx b/gui/src/components/settings/SettingsSidebar.tsx index 1cb2cf706..7bfd9aa29 100644 --- a/gui/src/components/settings/SettingsSidebar.tsx +++ b/gui/src/components/settings/SettingsSidebar.tsx @@ -74,13 +74,9 @@ export function SettingsSidebar() { {l10n.getString('settings-sidebar-osc_router')} -
      -
      {l10n.getString('settings-sidebar-osc_trackers')} -
      -
      VMC diff --git a/gui/src/components/settings/pages/GeneralSettings.tsx b/gui/src/components/settings/pages/GeneralSettings.tsx index f565e2eee..c74f7bdb3 100644 --- a/gui/src/components/settings/pages/GeneralSettings.tsx +++ b/gui/src/components/settings/pages/GeneralSettings.tsx @@ -1,7 +1,6 @@ import { useLocalization } from '@fluent/react'; -import { useEffect, useRef } from 'react'; +import { useEffect } from 'react'; import { DefaultValues, useForm } from 'react-hook-form'; -import { useLocation } from 'react-router-dom'; import { ChangeSettingsRequestT, DriftCompensationSettingsT, @@ -28,7 +27,10 @@ import { NumberSelector } from '../../commons/NumberSelector'; import { Radio } from '../../commons/Radio'; import { ThemeSelector } from '../../commons/ThemeSelector'; import { Typography } from '../../commons/Typography'; -import { SettingsPageLayout } from '../SettingsPageLayout'; +import { + SettingsPageLayout, + SettingsPagePaneLayout, +} from '../SettingsPageLayout'; interface SettingsForm { trackers: { @@ -132,9 +134,9 @@ const defaultValues = { export function GeneralSettings() { const { l10n } = useLocalization(); const { config, setConfig } = useConfig(); - const { state } = useLocation(); + // const { state } = useLocation(); const { currentLocales } = useLocaleConfig(); - const pageRef = useRef(null); + // const pageRef = useRef(null); const percentageFormat = Intl.NumberFormat(currentLocales, { style: 'percent', @@ -304,681 +306,702 @@ export function GeneralSettings() { }); // Handle scrolling to selected page - useEffect(() => { - const typedState: { scrollTo: string } = state as any; - if (!pageRef.current || !typedState || !typedState.scrollTo) { - return; - } - const elem = pageRef.current.querySelector(`#${typedState.scrollTo}`); - if (elem) { - elem.scrollIntoView({ behavior: 'smooth' }); - } - }, [state]); + // useEffect(() => { + // const typedState: { scrollTo: string } = state as any; + // if (!pageRef.current || !typedState || !typedState.scrollTo) { + // return; + // } + // const elem = pageRef.current.querySelector(`#${typedState.scrollTo}`); + // if (elem) { + // elem.scrollIntoView({ behavior: 'smooth' }); + // } + // }, [state]); return ( -
      - } id="steamvr"> - <> - - {l10n.getString('settings-general-steamvr')} - - - {l10n.getString('settings-general-steamvr-subtitle')} - -
      - {l10n - .getString('settings-general-steamvr-description') - .split('\n') - .map((line, i) => ( - - {line} - - ))} -
      -
      - - - - - - -
      - -
      - } id="mechanics"> - <> - - {l10n.getString('settings-general-tracker_mechanics')} - - - {l10n.getString('settings-general-tracker_mechanics-filtering')} - -
      - {l10n - .getString( - 'settings-general-tracker_mechanics-filtering-description' - ) - .split('\n') - .map((line, i) => ( - - {line} - - ))} -
      - - {l10n.getString( - 'settings-general-tracker_mechanics-filtering-type' - )} - -
      - - - -
      -
      - percentageFormat.format(value)} - min={0.1} - max={1.0} - step={0.1} - /> -
      -
      - - {l10n.getString( - 'settings-general-tracker_mechanics-drift_compensation' - )} - -
      - {l10n - .getString( - 'settings-general-tracker_mechanics-drift_compensation-description' - ) - .split('\n') - .map((line, i) => ( - - {line} - - ))} -
      - -
      - percentageFormat.format(value)} - min={0.1} - max={1.0} - step={0.1} - /> -
      -
      - -
      - -
      - } id="fksettings"> - <> - - {l10n.getString('settings-general-fk_settings')} - -
      + + + } id="steamvr"> + <> + + {l10n.getString('settings-general-steamvr')} + - {l10n.getString( - 'settings-general-fk_settings-leg_tweak-skating_correction' - )} + {l10n.getString('settings-general-steamvr-subtitle')} - - {l10n.getString( - 'settings-general-fk_settings-leg_tweak-skating_correction-description' - )} - -
      -
      - - percentageFormat.format(value)} - min={0.1} - max={1.0} - step={0.1} - /> -
      - -
      - - {l10n.getString('settings-general-fk_settings-leg_fk')} - -
      -
      - - {l10n.getString( - 'settings-general-fk_settings-leg_tweak-floor_clip-description' - )} - -
      -
      - -
      -
      - - {l10n.getString( - 'settings-general-fk_settings-leg_tweak-foot_plant-description' - )} - -
      -
      - -
      -
      - - {l10n.getString( - 'settings-general-fk_settings-leg_tweak-toe_snap-description' - )} - -
      -
      - -
      - -
      - - {l10n.getString('settings-general-fk_settings-arm_fk')} - - - {l10n.getString( - 'settings-general-fk_settings-arm_fk-description' - )} - -
      -
      - -
      - {config?.debug && ( - <> -
      - - {l10n.getString( - 'settings-general-fk_settings-skeleton_settings' - )} - - - {l10n.getString( - 'settings-general-fk_settings-skeleton_settings-description' - )} - -
      -
      - - - -
      -
      - - {l10n.getString( - 'settings-general-fk_settings-vive_emulation-title' - )} - - - {l10n.getString( - 'settings-general-fk_settings-vive_emulation-description' - )} - -
      -
      - -
      - - )} - -
      - - } id="gestureControl"> - <> - - {l10n.getString('settings-general-gesture_control')} - - - {l10n.getString('settings-general-gesture_control-subtitle')} - -
      - - {l10n.getString('settings-general-gesture_control-description')} - -
      -
      - - - -
      -
      - `${Math.round(value * 10) / 10} s`} - min={0.2} - max={3.0} - step={0.2} - /> - `${Math.round(value * 10) / 10} s`} - min={0.2} - max={3.0} - step={0.2} - /> - `${Math.round(value * 10) / 10} s`} - min={0.2} - max={3.0} - step={0.2} - /> -
      -
      - - l10n.getString('settings-general-gesture_control-taps', { - amount: Math.round(value), - }) - } - min={2} - max={10} - step={1} - /> - - l10n.getString('settings-general-gesture_control-taps', { - amount: Math.round(value), - }) - } - min={2} - max={10} - step={1} - /> - - l10n.getString('settings-general-gesture_control-taps', { - amount: Math.round(value), - }) - } - min={2} - max={10} - step={1} - /> -
      - -
      - - } id="interface"> - <> - - {l10n.getString('settings-general-interface')} - - - - {l10n.getString('settings-general-interface-dev_mode')} - -
      - - {l10n.getString( - 'settings-general-interface-dev_mode-description' - )} - -
      -
      - -
      - - - {l10n.getString('settings-general-interface-serial_detection')} - -
      - - {l10n.getString( - 'settings-general-interface-serial_detection-description' - )} - -
      -
      - -
      - - - {l10n.getString('settings-general-interface-feedback_sound')} - -
      - - {l10n.getString( - 'settings-general-interface-feedback_sound-description' - )} - -
      -
      - -
      -
      - percentageFormat.format(value)} - min={0.1} - max={1.0} - step={0.1} - /> -
      -
      - - {l10n.getString('settings-general-interface-theme')} - -
      - - - - - - - - +
      + {l10n + .getString('settings-general-steamvr-description') + .split('\n') + .map((line, i) => ( + + {line} + + ))}
      -
      - - {l10n.getString('settings-general-interface-lang')} - -
      - - {l10n.getString('settings-general-interface-lang-description')} +
      + + + + + + +
      + + + } id="mechanics"> + <> + + {l10n.getString('settings-general-tracker_mechanics')} -
      -
      - -
      - - - + + {l10n.getString('settings-general-tracker_mechanics-filtering')} + +
      + {l10n + .getString( + 'settings-general-tracker_mechanics-filtering-description' + ) + .split('\n') + .map((line, i) => ( + + {line} + + ))} +
      + + {l10n.getString( + 'settings-general-tracker_mechanics-filtering-type' + )} + +
      + + + +
      +
      + percentageFormat.format(value)} + min={0.1} + max={1.0} + step={0.1} + /> +
      +
      + + {l10n.getString( + 'settings-general-tracker_mechanics-drift_compensation' + )} + +
      + {l10n + .getString( + 'settings-general-tracker_mechanics-drift_compensation-description' + ) + .split('\n') + .map((line, i) => ( + + {line} + + ))} +
      + +
      + percentageFormat.format(value)} + min={0.1} + max={1.0} + step={0.1} + /> +
      +
      + +
      + + + } + id="fksettings" + > + <> + + {l10n.getString('settings-general-fk_settings')} + +
      + + {l10n.getString( + 'settings-general-fk_settings-leg_tweak-skating_correction' + )} + + + {l10n.getString( + 'settings-general-fk_settings-leg_tweak-skating_correction-description' + )} + +
      +
      + + percentageFormat.format(value)} + min={0.1} + max={1.0} + step={0.1} + /> +
      + +
      + + {l10n.getString('settings-general-fk_settings-leg_fk')} + +
      +
      + + {l10n.getString( + 'settings-general-fk_settings-leg_tweak-floor_clip-description' + )} + +
      +
      + +
      +
      + + {l10n.getString( + 'settings-general-fk_settings-leg_tweak-foot_plant-description' + )} + +
      +
      + +
      +
      + + {l10n.getString( + 'settings-general-fk_settings-leg_tweak-toe_snap-description' + )} + +
      +
      + +
      + +
      + + {l10n.getString('settings-general-fk_settings-arm_fk')} + + + {l10n.getString( + 'settings-general-fk_settings-arm_fk-description' + )} + +
      +
      + +
      + {config?.debug && ( + <> +
      + + {l10n.getString( + 'settings-general-fk_settings-skeleton_settings' + )} + + + {l10n.getString( + 'settings-general-fk_settings-skeleton_settings-description' + )} + +
      +
      + + + +
      +
      + + {l10n.getString( + 'settings-general-fk_settings-vive_emulation-title' + )} + + + {l10n.getString( + 'settings-general-fk_settings-vive_emulation-description' + )} + +
      +
      + +
      + + )} + +
      + + } + id="gestureControl" + > + <> + + {l10n.getString('settings-general-gesture_control')} + + + {l10n.getString('settings-general-gesture_control-subtitle')} + +
      + + {l10n.getString('settings-general-gesture_control-description')} + +
      +
      + + + +
      +
      + `${Math.round(value * 10) / 10} s`} + min={0.2} + max={3.0} + step={0.2} + /> + `${Math.round(value * 10) / 10} s`} + min={0.2} + max={3.0} + step={0.2} + /> + `${Math.round(value * 10) / 10} s`} + min={0.2} + max={3.0} + step={0.2} + /> +
      +
      + + l10n.getString('settings-general-gesture_control-taps', { + amount: Math.round(value), + }) + } + min={2} + max={10} + step={1} + /> + + l10n.getString('settings-general-gesture_control-taps', { + amount: Math.round(value), + }) + } + min={2} + max={10} + step={1} + /> + + l10n.getString('settings-general-gesture_control-taps', { + amount: Math.round(value), + }) + } + min={2} + max={10} + step={1} + /> +
      + +
      + + } + id="interface" + > + <> + + {l10n.getString('settings-general-interface')} + + + + {l10n.getString('settings-general-interface-dev_mode')} + +
      + + {l10n.getString( + 'settings-general-interface-dev_mode-description' + )} + +
      +
      + +
      + + + {l10n.getString('settings-general-interface-serial_detection')} + +
      + + {l10n.getString( + 'settings-general-interface-serial_detection-description' + )} + +
      +
      + +
      + + + {l10n.getString('settings-general-interface-feedback_sound')} + +
      + + {l10n.getString( + 'settings-general-interface-feedback_sound-description' + )} + +
      +
      + +
      +
      + percentageFormat.format(value)} + min={0.1} + max={1.0} + step={0.1} + /> +
      +
      + + {l10n.getString('settings-general-interface-theme')} + +
      + + + + + + + + +
      +
      + + {l10n.getString('settings-general-interface-lang')} + +
      + + {l10n.getString('settings-general-interface-lang-description')} + +
      +
      + +
      + +
      + + ); } diff --git a/gui/src/components/settings/pages/OSCRouterSettings.tsx b/gui/src/components/settings/pages/OSCRouterSettings.tsx index 39a2f4685..b4fab07c0 100644 --- a/gui/src/components/settings/pages/OSCRouterSettings.tsx +++ b/gui/src/components/settings/pages/OSCRouterSettings.tsx @@ -1,7 +1,6 @@ import { Localized, useLocalization } from '@fluent/react'; -import { useEffect, useRef } from 'react'; +import { useEffect } from 'react'; import { useForm } from 'react-hook-form'; -import { useLocation } from 'react-router-dom'; import { ChangeSettingsRequestT, OSCRouterSettingsT, @@ -15,7 +14,10 @@ import { CheckBox } from '../../commons/Checkbox'; import { RouterIcon } from '../../commons/icon/RouterIcon'; import { Input } from '../../commons/Input'; import { Typography } from '../../commons/Typography'; -import { SettingsPageLayout } from '../SettingsPageLayout'; +import { + SettingsPageLayout, + SettingsPagePaneLayout, +} from '../SettingsPageLayout'; interface OSCRouterSettingsForm { router: { @@ -42,8 +44,6 @@ const defaultValues = { export function OSCRouterSettings() { const { l10n } = useLocalization(); const { sendRPCPacket, useRPCPacket } = useWebsocketAPI(); - const { state } = useLocation(); - const pageRef = useRef(null); const { reset, control, watch, handleSubmit } = useForm({ @@ -96,125 +96,115 @@ export function OSCRouterSettings() { reset(formData); }); - // Handle scrolling to selected page - useEffect(() => { - const typedState: { scrollTo: string } = state as any; - if (!pageRef.current || !typedState || !typedState.scrollTo) { - return; - } - const elem = pageRef.current.querySelector(`#${typedState.scrollTo}`); - if (elem) { - elem.scrollIntoView({ behavior: 'smooth' }); - } - }, [state]); - return ( -
      - } id="router"> - <> - - {l10n.getString('settings-osc-router')} - -
      - <> - {l10n - .getString('settings-osc-router-description') - .split('\n') - .map((line, i) => ( - - {line} - - ))} - -
      - - {l10n.getString('settings-osc-router-enable')} - -
      - - {l10n.getString('settings-osc-router-enable-description')} + + + } id="router"> + <> + + {l10n.getString('settings-osc-router')} -
      -
      - -
      - - {l10n.getString('settings-osc-router-network')} - -
      - <> - {l10n - .getString('settings-osc-router-network-description') - .split('\n') - .map((line, i) => ( - - {line} - - ))} - -
      -
      - - + <> + {l10n + .getString('settings-osc-router-description') + .split('\n') + .map((line, i) => ( + + {line} + + ))} + +
      + + {l10n.getString('settings-osc-router-enable')} + +
      + + {l10n.getString('settings-osc-router-enable-description')} + +
      +
      + +
      + + {l10n.getString('settings-osc-router-network')} + +
      + <> + {l10n + .getString('settings-osc-router-network-description') + .split('\n') + .map((line, i) => ( + + {line} + + ))} + +
      +
      + + + + + + +
      + + {l10n.getString('settings-osc-router-network-address')} + +
      + + {l10n.getString( + 'settings-osc-router-network-address-description' + )} + +
      +
      + - - - - -
      - - {l10n.getString('settings-osc-router-network-address')} - -
      - - {l10n.getString( - 'settings-osc-router-network-address-description' - )} - -
      -
      - -
      - -
      -
      +
      + + + +
      ); } diff --git a/gui/src/components/settings/pages/Serial.tsx b/gui/src/components/settings/pages/Serial.tsx index 100249d61..d6d57a625 100644 --- a/gui/src/components/settings/pages/Serial.tsx +++ b/gui/src/components/settings/pages/Serial.tsx @@ -21,6 +21,7 @@ import { Typography } from '../../commons/Typography'; import { Localized, useLocalization } from '@fluent/react'; import { BaseModal } from '../../commons/BaseModal'; import { WarningBox } from '../../commons/TipBox'; +import { useBreakpoint } from '../../../hooks/breakpoint'; export interface SerialForm { port: string; @@ -32,8 +33,8 @@ export function Serial() { layoutWidth, ref: consoleRef, } = useLayout(); + const { isMobile } = useBreakpoint('mobile'); const { l10n } = useLocalization(); - const { state } = useLocation(); const toolbarRef = useRef(null); @@ -203,7 +204,7 @@ export function Serial() { ref={consoleRef} className="overflow-x-auto overflow-y-auto" style={{ - height: layoutHeight - height - 30, + height: layoutHeight - height - 30 - (isMobile ? 88 : 0), width: layoutWidth - 24, }} > @@ -217,7 +218,7 @@ export function Serial() {
      -
      +
      @@ -230,19 +231,34 @@ export function Serial() { + {isMobile && ( + ({ + label: device.name?.toString() || 'error', + value: device.port?.toString() || 'error', + }))} + > + )}
      -
      + {!isMobile && ( ({ label: device.name?.toString() || 'error', value: device.port?.toString() || 'error', }))} > -
      + )}
      diff --git a/gui/src/components/settings/pages/VMCSettings.tsx b/gui/src/components/settings/pages/VMCSettings.tsx index 41ed8d48a..1c61711cf 100644 --- a/gui/src/components/settings/pages/VMCSettings.tsx +++ b/gui/src/components/settings/pages/VMCSettings.tsx @@ -1,7 +1,6 @@ import { Localized, useLocalization } from '@fluent/react'; -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useState } from 'react'; import { useForm } from 'react-hook-form'; -import { useLocation } from 'react-router-dom'; import { ChangeSettingsRequestT, RpcMessage, @@ -17,7 +16,10 @@ import { VMCIcon } from '../../commons/icon/VMCIcon'; import { Input } from '../../commons/Input'; import { Typography } from '../../commons/Typography'; import { magic } from '../../utils/formatting'; -import { SettingsPageLayout } from '../SettingsPageLayout'; +import { + SettingsPageLayout, + SettingsPagePaneLayout, +} from '../SettingsPageLayout'; interface VMCSettingsForm { vmc: { @@ -47,8 +49,6 @@ const defaultValues = { export function VMCSettings() { const { l10n } = useLocalization(); const { sendRPCPacket, useRPCPacket } = useWebsocketAPI(); - const { state } = useLocation(); - const pageRef = useRef(null); const [modelName, setModelName] = useState(null); const [flashLoaded, setFlashLoaded] = useState(false); @@ -119,170 +119,162 @@ export function VMCSettings() { reset(formData); }); - // Handle scrolling to selected page - useEffect(() => { - const typedState: { scrollTo: string } = state as any; - if (!pageRef.current || !typedState || !typedState.scrollTo) { - return; - } - const elem = pageRef.current.querySelector(`#${typedState.scrollTo}`); - if (elem) { - elem.scrollIntoView({ behavior: 'smooth' }); - } - }, [state]); - return ( -
      - } id="vmc"> - <> - - {l10n.getString('settings-osc-vmc')} - -
      - <> - {l10n - .getString('settings-osc-vmc-description') - .split('\n') - .map((line, i) => ( - - {line} - - ))} - -
      - - {l10n.getString('settings-osc-vmc-enable')} - -
      - - {l10n.getString('settings-osc-vmc-enable-description')} + + + } id="vmc"> + <> + + {l10n.getString('settings-osc-vmc')} -
      -
      - -
      - - {l10n.getString('settings-osc-vmc-network')} - -
      - <> - {l10n - .getString('settings-osc-vmc-network-description') - .split('\n') - .map((line, i) => ( - - {line} - - ))} - -
      -
      - - + <> + {l10n + .getString('settings-osc-vmc-description') + .split('\n') + .map((line, i) => ( + + {line} + + ))} + +
      + + {l10n.getString('settings-osc-vmc-enable')} + +
      + + {l10n.getString('settings-osc-vmc-enable-description')} + +
      +
      + +
      + + {l10n.getString('settings-osc-vmc-network')} + +
      + <> + {l10n + .getString('settings-osc-vmc-network-description') + .split('\n') + .map((line, i) => ( + + {line} + + ))} + +
      +
      + + + + + + +
      + + {l10n.getString('settings-osc-vmc-network-address')} + +
      + + {l10n.getString('settings-osc-vmc-network-address-description')} + +
      +
      + - - - + + {l10n.getString('settings-osc-vmc-vrm')} + +
      + + {l10n.getString('settings-osc-vmc-vrm-description')} + +
      +
      + + {modelName === null + ? l10n.getString('settings-osc-vmc-vrm-model_unloaded') + : l10n.getString('settings-osc-vmc-vrm-model_loaded', { + name: modelName, + titled: (!!modelName).toString(), + })} + +
      +
      + - -
      - - {l10n.getString('settings-osc-vmc-network-address')} - -
      - - {l10n.getString('settings-osc-vmc-network-address-description')} + name="vmc.vrmJson" + rules={{ + required: false, + }} + value="help" + label="settings-osc-vmc-vrm-file_select" + accept="model/gltf-binary, model/gltf+json, model/vrml, .vrm, .glb, .gltf" + > + {/* For some reason, linux (GNOME) is detecting the VRM file is a VRML */} +
      + + {l10n.getString('settings-osc-vmc-anchor_hip')} -
      -
      - -
      - {l10n.getString('settings-osc-vmc-vrm')} -
      - - {l10n.getString('settings-osc-vmc-vrm-description')} - -
      -
      - - {modelName === null - ? l10n.getString('settings-osc-vmc-vrm-model_unloaded') - : l10n.getString('settings-osc-vmc-vrm-model_loaded', { - name: modelName, - titled: (!!modelName).toString(), - })} - -
      -
      - - {/* For some reason, linux (GNOME) is detecting the VRM file is a VRML */} -
      - - {l10n.getString('settings-osc-vmc-anchor_hip')} - -
      - - {l10n.getString('settings-osc-vmc-anchor_hip-description')} - -
      -
      - -
      - -
      -
      +
      + + {l10n.getString('settings-osc-vmc-anchor_hip-description')} + +
      +
      + +
      + + + + ); } diff --git a/gui/src/components/settings/pages/VRCOSCSettings.tsx b/gui/src/components/settings/pages/VRCOSCSettings.tsx index c41d5156e..2609d64f2 100644 --- a/gui/src/components/settings/pages/VRCOSCSettings.tsx +++ b/gui/src/components/settings/pages/VRCOSCSettings.tsx @@ -1,7 +1,6 @@ import { Localized, useLocalization } from '@fluent/react'; -import { useEffect, useRef } from 'react'; +import { useEffect } from 'react'; import { useForm } from 'react-hook-form'; -import { useLocation } from 'react-router-dom'; import { ChangeSettingsRequestT, OSCSettingsT, @@ -16,7 +15,10 @@ import { CheckBox } from '../../commons/Checkbox'; import { VRCIcon } from '../../commons/icon/VRCIcon'; import { Input } from '../../commons/Input'; import { Typography } from '../../commons/Typography'; -import { SettingsPageLayout } from '../SettingsPageLayout'; +import { + SettingsPageLayout, + SettingsPagePaneLayout, +} from '../SettingsPageLayout'; interface VRCOSCSettingsForm { vrchat: { @@ -61,8 +63,6 @@ const defaultValues = { export function VRCOSCSettings() { const { l10n } = useLocalization(); const { sendRPCPacket, useRPCPacket } = useWebsocketAPI(); - const { state } = useLocation(); - const pageRef = useRef(null); const { reset, control, watch, handleSubmit } = useForm({ defaultValues: defaultValues, @@ -120,173 +120,165 @@ export function VRCOSCSettings() { reset(formData); }); - // Handle scrolling to selected page - useEffect(() => { - const typedState: { scrollTo: string } = state as any; - if (!pageRef.current || !typedState || !typedState.scrollTo) { - return; - } - const elem = pageRef.current.querySelector(`#${typedState.scrollTo}`); - if (elem) { - elem.scrollIntoView({ behavior: 'smooth' }); - } - }, [state]); - return ( -
      - } id="vrchat"> - <> - - {l10n.getString('settings-osc-vrchat')} - -
      - <> - {l10n - .getString('settings-osc-vrchat-description') - .split('\n') - .map((line, i) => ( - - {line} - - ))} - -
      - - {l10n.getString('settings-osc-vrchat-enable')} - -
      - - {l10n.getString('settings-osc-vrchat-enable-description')} + + + } id="vrchat"> + <> + + {l10n.getString('settings-osc-vrchat')} -
      -
      - -
      - - {l10n.getString('settings-osc-vrchat-network')} - -
      - - {l10n.getString('settings-osc-vrchat-network-description')} +
      + <> + {l10n + .getString('settings-osc-vrchat-description') + .split('\n') + .map((line, i) => ( + + {line} + + ))} + +
      + + {l10n.getString('settings-osc-vrchat-enable')} -
      -
      - - + + {l10n.getString('settings-osc-vrchat-enable-description')} + +
      +
      + +
      + + {l10n.getString('settings-osc-vrchat-network')} + +
      + + {l10n.getString('settings-osc-vrchat-network-description')} + +
      +
      + + + + + + +
      + + {l10n.getString('settings-osc-vrchat-network-address')} + +
      + + {l10n.getString( + 'settings-osc-vrchat-network-address-description' + )} + +
      +
      + - - - + + {l10n.getString('settings-osc-vrchat-network-trackers')} + +
      + + {l10n.getString( + 'settings-osc-vrchat-network-trackers-description' + )} + +
      +
      + - -
      - - {l10n.getString('settings-osc-vrchat-network-address')} - -
      - - {l10n.getString( - 'settings-osc-vrchat-network-address-description' - )} - -
      -
      - -
      - - {l10n.getString('settings-osc-vrchat-network-trackers')} - -
      - - {l10n.getString( - 'settings-osc-vrchat-network-trackers-description' - )} - -
      -
      - - - - - -
      - - - + name="vrchat.trackers.chest" + label={l10n.getString( + 'settings-osc-vrchat-network-trackers-chest' + )} + /> + + + + +
      + + + +
      ); } diff --git a/gui/src/components/tracker/SingleTrackerBodyAssignmentMenu.tsx b/gui/src/components/tracker/SingleTrackerBodyAssignmentMenu.tsx index 97abcdba9..cc443479e 100644 --- a/gui/src/components/tracker/SingleTrackerBodyAssignmentMenu.tsx +++ b/gui/src/components/tracker/SingleTrackerBodyAssignmentMenu.tsx @@ -9,6 +9,7 @@ import { BodyAssignment } from '../onboarding/BodyAssignment'; import { useLocalization } from '@fluent/react'; import { NeckWarningModal } from '../onboarding/NeckWarningModal'; import { useChokerWarning } from '../../hooks/choker-warning'; +import { useBreakpoint } from '../../hooks/breakpoint'; export function SingleTrackerBodyAssignmentMenu({ isOpen, @@ -19,6 +20,7 @@ export function SingleTrackerBodyAssignmentMenu({ onClose: () => void; onRoleSelected: (role: BodyPart) => void; }) { + const { isMobile } = useBreakpoint('mobile'); const { l10n } = useLocalization(); const { control, watch } = useForm<{ advanced: boolean }>({ defaultValues: { advanced: false }, @@ -41,51 +43,50 @@ export function SingleTrackerBodyAssignmentMenu({ 'fixed top-0 right-0 left-0 bottom-0 flex flex-col items-center w-full h-full justify-center bg-black bg-opacity-90 z-20' )} className={classNames( - 'focus:ring-transparent focus:ring-offset-transparent focus:outline-transparent outline-none mt-20 z-10' + 'focus:ring-transparent focus:ring-offset-transparent focus:outline-transparent outline-none mt-12 z-10 overflow-y-auto' )} >
      -
      -
      -
      - - {l10n.getString('body_assignment_menu')} - - - {l10n.getString('body_assignment_menu-description')} - - -
      - -
      +
      +
      + + {l10n.getString('body_assignment_menu')} + + + {l10n.getString('body_assignment_menu-description')} + + +
      +
      -
      - -
      - -
      +
      +
      + +
      +
      diff --git a/gui/src/components/tracker/TrackerCard.tsx b/gui/src/components/tracker/TrackerCard.tsx index a7dad0f76..ce4e70f77 100644 --- a/gui/src/components/tracker/TrackerCard.tsx +++ b/gui/src/components/tracker/TrackerCard.tsx @@ -72,7 +72,7 @@ function TrackerSmol({ const trackerName = useName(); return ( -
      +
      diff --git a/gui/src/components/tracker/TrackerPartCard.tsx b/gui/src/components/tracker/TrackerPartCard.tsx index e1717eeee..67fb1cfb9 100644 --- a/gui/src/components/tracker/TrackerPartCard.tsx +++ b/gui/src/components/tracker/TrackerPartCard.tsx @@ -78,7 +78,7 @@ export function TrackerPartCard({ (showCard && (
      setSelectRotation(false)} onDirectionSelected={onDirectionSelected} > -
      +
      {tracker && ( + +
      + ); +} diff --git a/gui/src/hooks/breakpoint.ts b/gui/src/hooks/breakpoint.ts new file mode 100644 index 000000000..6ee06db68 --- /dev/null +++ b/gui/src/hooks/breakpoint.ts @@ -0,0 +1,28 @@ +import resolveConfig from 'tailwindcss/resolveConfig'; +import { useMediaQuery } from 'react-responsive'; +import tailwindConfig from '../../tailwind.config'; + +const fullConfig = resolveConfig(tailwindConfig as any); +const breakpoints = tailwindConfig.theme.screens; + +type BreakpointKey = keyof typeof breakpoints; + +export function useBreakpoint(breakpointKey: K) { + // FIXME There is a flickering issue caused by this, because isMobile is not resolved fast enough + // one solution would be to have this solved only once on the appProvider and reuse the value all the time + const bool = useMediaQuery({ + query: fullConfig.theme.screens[breakpointKey].raw + ? fullConfig.theme.screens[breakpointKey].raw + : `(min-width: ${fullConfig.theme.screens[breakpointKey]})`, + }); + const capitalizedKey = + breakpointKey.toString()[0].toUpperCase() + breakpointKey.toString().substring(1); + type Key = `is${Capitalize}`; + return { + [`is${capitalizedKey}`]: bool, + } as Record; +} + +export function useIsTauri() { + return !!window.__TAURI_METADATA__; +} diff --git a/gui/src/hooks/layout.ts b/gui/src/hooks/layout.ts index 998b6609c..c1e1f05c1 100644 --- a/gui/src/hooks/layout.ts +++ b/gui/src/hooks/layout.ts @@ -1,4 +1,4 @@ -import { MutableRefObject, useEffect, useLayoutEffect, useRef, useState } from 'react'; +import { MutableRefObject, useLayoutEffect, useRef, useState } from 'react'; export function useLayout() { const [layoutHeight, setLayoutHeigt] = useState(window.innerHeight); @@ -47,7 +47,7 @@ export function useElemSize( }) ); - useEffect(() => { + useLayoutEffect(() => { if (ref.current) { observer.current.observe(ref.current); } diff --git a/gui/src/index.css b/gui/src/index.css index 0923823a3..f60c28247 100644 --- a/gui/src/index.css +++ b/gui/src/index.css @@ -5,8 +5,8 @@ body { font-variant-numeric: tabular-nums; font-family: 'poppins', sans-serif, 'Twemoji Chromium', emoji; - height: 100vh; width: 100vw; + height: 100vh; user-select: none; background: theme('colors.background.20'); diff --git a/gui/src/vite-env.d.ts b/gui/src/vite-env.d.ts index 9497b58e7..82b6b82a7 100644 --- a/gui/src/vite-env.d.ts +++ b/gui/src/vite-env.d.ts @@ -4,3 +4,5 @@ declare const __COMMIT_HASH__: string; declare const __VERSION_TAG__: string; declare const __GIT_CLEAN__: boolean; + +declare module 'tailwind-gradient-mask-image'; diff --git a/gui/tailwind.config.cjs b/gui/tailwind.config.ts similarity index 93% rename from gui/tailwind.config.cjs rename to gui/tailwind.config.ts index 66066afaa..f081ca039 100644 --- a/gui/tailwind.config.cjs +++ b/gui/tailwind.config.ts @@ -1,6 +1,10 @@ -const plugin = require('tailwindcss/plugin'); +import plugin from 'tailwindcss/plugin' +import forms from '@tailwindcss/forms' +import gradient from 'tailwind-gradient-mask-image' +import type { Config } from 'tailwindcss' -const rem = (pt) => `${pt / 16}rem`; + +const rem = (pt: number) => `${pt / 16}rem`; const colors = { 'blue-gray': { @@ -151,10 +155,11 @@ const colors = { }, }; -module.exports = { +const config = { content: ['./src/**/*.{js,jsx,ts,tsx}'], theme: { screens: { + mobile: { raw: 'not (min-width: 800px)' }, xs: '800px', sm: '900px', md: '1100px', @@ -197,7 +202,7 @@ module.exports = { DEFAULT: rem(12), }, fontWeight: { - DEFAULT: 500, + DEFAULT: '500', }, color: { DEFAULT: 'rgb(var(--default-color), )', @@ -227,10 +232,11 @@ module.exports = { }, }, plugins: [ - require('@tailwindcss/forms'), - require('tailwind-gradient-mask-image'), + forms, + gradient, plugin(function ({ addUtilities }) { - const textConfig = (fontSize, fontWeight) => ({ + + const textConfig = (fontSize: any, fontWeight: any) => ({ fontSize, fontWeight, }); @@ -245,4 +251,6 @@ module.exports = { }); }), ], -}; +} satisfies Config + +export default config; diff --git a/gui/tsconfig.json b/gui/tsconfig.json index 0418983b7..a3c179d36 100644 --- a/gui/tsconfig.json +++ b/gui/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "target": "es2020", "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": false, + "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, diff --git a/package-lock.json b/package-lock.json index 310f6586d..49408d300 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,6 +45,7 @@ "react-dom": "^18.0.0", "react-hook-form": "^7.29.0", "react-modal": "3.15.1", + "react-responsive": "^9.0.2", "react-router-dom": "^6.2.2", "semver": "^7.5.0", "solarxr-protocol": "file:../solarxr-protocol", @@ -4688,6 +4689,11 @@ "postcss": "^8.4" } }, + "node_modules/css-mediaquery": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/css-mediaquery/-/css-mediaquery-0.1.2.tgz", + "integrity": "sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q==" + }, "node_modules/css-prefers-color-scheme": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-6.0.3.tgz", @@ -6308,6 +6314,11 @@ "url": "https://github.com/sponsors/typicode" } }, + "node_modules/hyphenate-style-name": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz", + "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==" + }, "node_modules/identity-obj-proxy": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz", @@ -7032,6 +7043,14 @@ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, + "node_modules/matchmediaquery": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/matchmediaquery/-/matchmediaquery-0.3.1.tgz", + "integrity": "sha512-Hlk20WQHRIm9EE9luN1kjRjYXAQToHOIAHPJn9buxBwuhfTHoKUcX+lXBbxc85DVQfXYbEQ4HcwQdd128E3qHQ==", + "dependencies": { + "css-mediaquery": "^0.1.2" + } + }, "node_modules/memfs": { "version": "3.4.13", "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.13.tgz", @@ -8609,6 +8628,23 @@ "node": ">=0.10.0" } }, + "node_modules/react-responsive": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/react-responsive/-/react-responsive-9.0.2.tgz", + "integrity": "sha512-+4CCab7z8G8glgJoRjAwocsgsv6VA2w7JPxFWHRc7kvz8mec1/K5LutNC2MG28Mn8mu6+bu04XZxHv5gyfT7xQ==", + "dependencies": { + "hyphenate-style-name": "^1.0.0", + "matchmediaquery": "^0.3.0", + "prop-types": "^15.6.1", + "shallow-equal": "^1.2.1" + }, + "engines": { + "node": ">=0.10" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/react-router": { "version": "6.7.0", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.7.0.tgz", @@ -8957,6 +8993,11 @@ "randombytes": "^2.1.0" } }, + "node_modules/shallow-equal": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.1.tgz", + "integrity": "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -13030,6 +13071,11 @@ "postcss-selector-parser": "^6.0.9" } }, + "css-mediaquery": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/css-mediaquery/-/css-mediaquery-0.1.2.tgz", + "integrity": "sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q==" + }, "css-prefers-color-scheme": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-6.0.3.tgz", @@ -14208,6 +14254,11 @@ "integrity": "sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==", "dev": true }, + "hyphenate-style-name": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz", + "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==" + }, "identity-obj-proxy": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz", @@ -14716,6 +14767,14 @@ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, + "matchmediaquery": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/matchmediaquery/-/matchmediaquery-0.3.1.tgz", + "integrity": "sha512-Hlk20WQHRIm9EE9luN1kjRjYXAQToHOIAHPJn9buxBwuhfTHoKUcX+lXBbxc85DVQfXYbEQ4HcwQdd128E3qHQ==", + "requires": { + "css-mediaquery": "^0.1.2" + } + }, "memfs": { "version": "3.4.13", "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.13.tgz", @@ -15708,6 +15767,17 @@ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", "integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==" }, + "react-responsive": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/react-responsive/-/react-responsive-9.0.2.tgz", + "integrity": "sha512-+4CCab7z8G8glgJoRjAwocsgsv6VA2w7JPxFWHRc7kvz8mec1/K5LutNC2MG28Mn8mu6+bu04XZxHv5gyfT7xQ==", + "requires": { + "hyphenate-style-name": "^1.0.0", + "matchmediaquery": "^0.3.0", + "prop-types": "^15.6.1", + "shallow-equal": "^1.2.1" + } + }, "react-router": { "version": "6.7.0", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.7.0.tgz", @@ -15938,6 +16008,11 @@ "randombytes": "^2.1.0" } }, + "shallow-equal": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.1.tgz", + "integrity": "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==" + }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -16030,6 +16105,7 @@ "react-dom": "^18.0.0", "react-hook-form": "^7.29.0", "react-modal": "3.15.1", + "react-responsive": "^9.0.2", "react-router-dom": "^6.2.2", "semver": "^7.5.0", "solarxr-protocol": "file:../solarxr-protocol",