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 <urielfontan2002@gmail.com>

* Update gui/src/components/onboarding/pages/CalibrationTutorial.tsx

Co-authored-by: Uriel <urielfontan2002@gmail.com>

* Update gui/src/components/onboarding/pages/CalibrationTutorial.tsx

Co-authored-by: Uriel <urielfontan2002@gmail.com>

* Update gui/src/components/onboarding/pages/CalibrationTutorial.tsx

Co-authored-by: Uriel <urielfontan2002@gmail.com>

* Update gui/src/components/onboarding/pages/CalibrationTutorial.tsx

Co-authored-by: Uriel <urielfontan2002@gmail.com>

* Update gui/src/components/settings/pages/Serial.tsx

Co-authored-by: Uriel <urielfontan2002@gmail.com>

* 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 <devminer@devminer.xyz>

* 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 <urielfontan2002@gmail.com>
Co-authored-by: DevMiner <devminer@devminer.xyz>
This commit is contained in:
lucas lelievre
2023-06-12 00:50:54 +02:00
committed by GitHub
parent 841ea8ee94
commit 91c0ea85a6
62 changed files with 2581 additions and 2047 deletions

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, minimum-scale=1.0" />
<meta name="theme-color" content="#000000" />
<link rel="apple-touch-icon" href="/logo192.png" />

View File

@@ -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",

View File

@@ -90,8 +90,8 @@
"title": "SlimeVR",
"width": 1289,
"height": 709,
"minWidth": 880,
"minHeight": 740,
"minWidth": 393,
"minHeight": 851,
"resizable": true,
"fullscreen": false,
"decorations": false,

View File

@@ -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 (
<>
<SerialDetectionModal></SerialDetectionModal>
@@ -66,15 +71,23 @@ function Layout() {
<Route
path="/"
element={
<MainLayoutRoute>
<MainLayoutRoute isMobile={isMobile}>
<Home />
</MainLayoutRoute>
}
/>
<Route
path="/vr-mode"
element={
<MainLayoutRoute isMobile={isMobile}>
<VRModePage />
</MainLayoutRoute>
}
/>
<Route
path="/tracker/:trackernum/:deviceid"
element={
<MainLayoutRoute background={false}>
<MainLayoutRoute background={false} isMobile={isMobile}>
<TrackerSettingsPage />
</MainLayoutRoute>
}
@@ -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) {

View File

@@ -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<HTMLDivElement>();
const { layoutHeight, ref } = useLayout<HTMLDivElement>();
const { layoutWidth, ref: refw } = useLayout<HTMLDivElement>();
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 (
<>
<TopBar></TopBar>
<div ref={ref} className="flex-grow" style={{ height: layoutHeight }}>
<div className="flex h-full pb-3">
<Navbar></Navbar>
<div
ref={ref}
className="flex-grow"
style={{ height: layoutHeight - height }}
>
<div className="flex h-full xs:pb-3">
{!isMobile && <Navbar></Navbar>}
<div
className="flex gap-2 pr-3 w-full"
className="flex gap-2 xs:pr-3 w-full"
ref={refw}
style={{ minWidth: layoutWidth }}
>
<div
className={classNames(
'flex flex-col rounded-xl w-full overflow-hidden',
'flex flex-col rounded-xl w-full overflow-hidden mobile:overflow-y-auto',
background && 'bg-background-70'
)}
>
{children}
</div>
{widgets && (
{!isMobile && widgets && (
<div className="flex flex-col px-2 min-w-[274px] w-[274px] gap-2 pt-2 rounded-xl overflow-y-auto bg-background-70">
<div className="grid grid-cols-2 gap-2 w-full [&>*:nth-child(odd):last-of-type]:col-span-full">
<ResetButton type={ResetType.Yaw} variant="big"></ResetButton>
<ResetButton
type={ResetType.Full}
variant="big"
></ResetButton>
{config?.debug && (
<ResetButton
type={ResetType.Mounting}
variant="big"
></ResetButton>
)}
<BVHButton></BVHButton>
<TrackingPauseButton></TrackingPauseButton>
{driftCompensationEnabled && (
<ClearDriftCompensationButton></ClearDriftCompensationButton>
)}
</div>
<div className="w-full">
<OverlayWidget></OverlayWidget>
</div>
<div className="w-full flex flex-col max-h-[33%] gap-3 overflow-y-auto mb-2">
{unprioritizedStatuses.map((status) => (
<Localized
id={`status_system-${StatusData[status.dataType]}`}
vars={parseStatusToLocale(status, trackers)}
key={status.id}
>
<TipBox whitespace={false} hideIcon={true}>
{`Warning, you should fix ${
StatusData[status.dataType]
}`}
</TipBox>
</Localized>
))}
</div>
{config?.debug && (
<div className="w-full">
<DeveloperModeWidget></DeveloperModeWidget>
</div>
)}
<WidgetsComponent></WidgetsComponent>
</div>
)}
</div>
</div>
<div ref={navRef}>{isMobile && <Navbar></Navbar>}</div>
</div>
</>
);

View File

@@ -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 (
<>
<NavButton to="/" icon={<CubeIcon></CubeIcon>}>
{l10n.getString('navbar-home')}
</NavButton>
<NavButton
to="/onboarding/trackers-assign"
state={{ alonePage: true }}
icon={<HumanIcon></HumanIcon>}
>
{l10n.getString('navbar-trackers_assign')}
</NavButton>
<NavButton
to="/onboarding/mounting/choose"
match="/onboarding/mounting/*"
state={{ alonePage: true }}
icon={<WrenchIcon></WrenchIcon>}
>
{l10n.getString('navbar-mounting')}
</NavButton>
<NavButton
to="/onboarding/body-proportions/choose"
match="/onboarding/body-proportions/*"
state={{ alonePage: true }}
icon={<RulerIcon></RulerIcon>}
>
{l10n.getString('navbar-body_proportions')}
</NavButton>
<NavButton to="/onboarding/home" icon={<SparkleIcon></SparkleIcon>}>
{l10n.getString('navbar-onboarding')}
</NavButton>
</>
);
}
export function Navbar() {
const { isMobile } = useBreakpoint('mobile');
const { l10n } = useLocalization();
return isMobile ? (
<div className="flex flex-row justify-around px-2 pt-2 bg-background-80 gap-2">
<MainLinks></MainLinks>
</div>
) : (
<div className="flex flex-col px-2 pt-2">
<div className="flex flex-col flex-grow gap-2">
<NavButton to="/" icon={<CubeIcon></CubeIcon>}>
{l10n.getString('navbar-home')}
</NavButton>
<NavButton
to="/onboarding/trackers-assign"
state={{ alonePage: true }}
icon={<HumanIcon></HumanIcon>}
>
{l10n.getString('navbar-trackers_assign')}
</NavButton>
<NavButton
to="/onboarding/mounting/choose"
match="/onboarding/mounting/*"
state={{ alonePage: true }}
icon={<WrenchIcon></WrenchIcon>}
>
{l10n.getString('navbar-mounting')}
</NavButton>
<NavButton
to="/onboarding/body-proportions/choose"
match="/onboarding/body-proportions/*"
state={{ alonePage: true }}
icon={<RulerIcon></RulerIcon>}
>
{l10n.getString('navbar-body_proportions')}
</NavButton>
<NavButton to="/onboarding/home" icon={<SparkleIcon></SparkleIcon>}>
{l10n.getString('navbar-onboarding')}
</NavButton>
<MainLinks></MainLinks>
</div>
<NavButton
to="/settings/trackers"

View File

@@ -18,6 +18,26 @@ import { open } from '@tauri-apps/api/shell';
import { GH_REPO, VersionContext, DOCS_SITE } from '../App';
import classNames from 'classnames';
import { QuestionIcon } from './commons/icon/QuestionIcon';
import { useBreakpoint, useIsTauri } from '../hooks/breakpoint';
import { GearIcon } from './commons/icon/GearIcon';
export function VersionTag() {
return (
<div
className={classNames(
'flex justify-around flex-col text-standard-bold',
'text-status-success bg-status-success bg-opacity-20 rounded-lg',
'px-3 select-text cursor-pointer'
)}
onClick={() => {
const url = `https://github.com/${GH_REPO}/releases`;
open(url).catch(() => window.open(url, '_blank'));
}}
>
{(__VERSION_TAG__ || __COMMIT_HASH__) + (__GIT_CLEAN__ ? '' : '-dirty')}
</div>
);
}
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<string | null>(null);
@@ -44,108 +66,147 @@ export function TopBar({
);
return (
<div data-tauri-drag-region className="flex gap-2 h-[38px] z-50">
<div
className="flex px-2 pb-1 mt-3 justify-around z-50"
data-tauri-drag-region
>
<div className="flex gap-2" data-tauri-drag-region>
<NavLink
to="/"
className="flex justify-around flex-col select-all"
data-tauri-drag-region
>
<SlimeVRIcon></SlimeVRIcon>
</NavLink>
<div className="flex justify-around flex-col" data-tauri-drag-region>
<Typography>SlimeVR</Typography>
</div>
<div
className={classNames(
'flex justify-around flex-col text-standard-bold',
'text-status-success bg-status-success bg-opacity-20 rounded-lg',
'px-3 select-text cursor-pointer'
)}
onClick={() => {
const url = `https://github.com/${GH_REPO}/releases`;
open(url).catch(() => window.open(url, '_blank'));
}}
>
{(__VERSION_TAG__ || __COMMIT_HASH__) +
(__GIT_CLEAN__ ? '' : '-dirty')}
</div>
{doesMatchSettings && (
<div
className={classNames(
'flex justify-around flex-col text-standard-bold text-status-special',
'bg-status-special bg-opacity-20 rounded-lg px-3 select-text'
)}
>
{localIp || 'unknown local ip'}
</div>
)}
{version && (
<div
className="cursor-pointer"
onClick={() => {
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'));
}}
>
<DownloadIcon></DownloadIcon>
</div>
)}
</div>
</div>
<div
className="flex flex-grow items-center h-full justify-center z-50"
data-tauri-drag-region
>
<>
<div data-tauri-drag-region className="flex gap-2 h-[38px] z-50">
<div
className="flex max-w-xl h-full items-center w-full"
className="flex px-2 pb-1 mt-3 justify-around z-50"
data-tauri-drag-region
>
{progress !== undefined && (
<ProgressBar progress={progress} height={3} parts={3}></ProgressBar>
<div className="flex gap-2 mobile:w-5" data-tauri-drag-region>
<NavLink
to="/"
className="flex justify-around flex-col select-all"
data-tauri-drag-region
>
<SlimeVRIcon></SlimeVRIcon>
</NavLink>
{(isTauri || !isMobile) && (
<div
className={classNames('flex justify-around flex-col')}
data-tauri-drag-region
>
<Typography>SlimeVR</Typography>
</div>
)}
{!isMobile && (
<>
<VersionTag></VersionTag>
{doesMatchSettings && (
<div
className={classNames(
'flex justify-around flex-col text-standard-bold text-status-special',
'bg-status-special bg-opacity-20 rounded-lg px-3 select-text'
)}
>
{localIp || 'unknown local ip'}
</div>
)}
</>
)}
{version && (
<div
className="cursor-pointer"
onClick={() => {
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'));
}}
>
<DownloadIcon></DownloadIcon>
</div>
)}
</div>
</div>
<div
className="flex flex-grow items-center h-full justify-center z-50"
data-tauri-drag-region
>
{!isMobile && (
<>
<div
className="flex max-w-xl h-full items-center w-full"
data-tauri-drag-region
>
{progress !== undefined && (
<ProgressBar
progress={progress}
height={3}
parts={3}
></ProgressBar>
)}
</div>
</>
)}
{!isTauri && (
<div className="flex flex-row gap-2">
<div
className="flex justify-around flex-col xs:hidden"
data-tauri-drag-region
>
<Typography variant="section-title">SlimeVR</Typography>
</div>
</div>
)}
</div>
<div
className="flex justify-end items-center px-2 gap-2 z-50"
data-tauri-drag-region
>
<NavLink
to="/settings/trackers"
className="flex justify-around flex-col select-all fill-background-50"
data-tauri-drag-region
state={{ scrollTo: 'steamvr' }}
>
<GearIcon></GearIcon>
</NavLink>
{!isMobile && (
<div
className={classNames(
'flex items-center justify-center stroke-window-icon',
'hover:bg-background-60 rounded-full w-7 h-7 cursor-pointer'
)}
onClick={() =>
open(DOCS_SITE).catch(() => window.open(DOCS_SITE, '_blank'))
}
>
<QuestionIcon></QuestionIcon>
</div>
)}
{isTauri && (
<>
<div
className="flex items-center justify-center hover:bg-background-60 rounded-full w-7 h-7"
onClick={() => appWindow.minimize()}
>
<MinimiseIcon></MinimiseIcon>
</div>
<div
className="flex items-center justify-center hover:bg-background-60 rounded-full w-7 h-7"
onClick={() => appWindow.toggleMaximize()}
>
<MaximiseIcon></MaximiseIcon>
</div>
<div
className="flex items-center justify-center hover:bg-background-60 rounded-full w-7 h-7"
onClick={() => appWindow.close()}
>
<CloseIcon></CloseIcon>
</div>
</>
)}
</div>
</div>
<div
className="flex justify-end items-center px-2 gap-2 z-50"
data-tauri-drag-region
>
<div
className={classNames(
'flex items-center justify-center stroke-window-icon',
'hover:bg-background-60 rounded-full w-7 h-7 cursor-pointer'
)}
onClick={() =>
open(DOCS_SITE).catch(() => window.open(DOCS_SITE, '_blank'))
}
>
<QuestionIcon></QuestionIcon>
{isMobile && progress !== undefined && (
<div className="flex gap-2 px-2 h-6 mb-2 justify-center flex-col border-b border-accent-background-30">
<ProgressBar progress={progress} height={3} parts={3}></ProgressBar>
</div>
<div
className="flex items-center justify-center hover:bg-background-60 rounded-full w-7 h-7"
onClick={() => appWindow.minimize()}
>
<MinimiseIcon></MinimiseIcon>
</div>
<div
className="flex items-center justify-center hover:bg-background-60 rounded-full w-7 h-7"
onClick={() => appWindow.toggleMaximize()}
>
<MaximiseIcon></MaximiseIcon>
</div>
<div
className="flex items-center justify-center hover:bg-background-60 rounded-full w-7 h-7"
onClick={() => appWindow.close()}
>
<CloseIcon></CloseIcon>
</div>
</div>
</div>
)}
</>
);
}

View File

@@ -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 (
<>
<div className="grid grid-cols-2 gap-2 w-full [&>*:nth-child(odd):last-of-type]:col-span-full">
<ResetButton type={ResetType.Yaw} variant="big"></ResetButton>
<ResetButton type={ResetType.Full} variant="big"></ResetButton>
{config?.debug && (
<ResetButton type={ResetType.Mounting} variant="big"></ResetButton>
)}
<BVHButton></BVHButton>
<TrackingPauseButton></TrackingPauseButton>
{driftCompensationEnabled && (
<ClearDriftCompensationButton></ClearDriftCompensationButton>
)}
</div>
<div className="w-full">
<OverlayWidget></OverlayWidget>
</div>
<div className="w-full flex flex-col max-h-[33%] gap-3 overflow-y-auto mb-2">
{unprioritizedStatuses.map((status) => (
<Localized
id={`status_system-${StatusData[status.dataType]}`}
vars={parseStatusToLocale(status, trackers)}
key={status.id}
>
<TipBox whitespace={false} hideIcon={true}>
{`Warning, you should fix ${StatusData[status.dataType]}`}
</TipBox>
</Localized>
))}
</div>
{config?.debug && (
<div className="w-full">
<DeveloperModeWidget></DeveloperModeWidget>
</div>
)}
</>
);
}

View File

@@ -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'
)
}

View File

@@ -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<HTMLDivElement | null>(null);
const leftContainerRef = useRef<HTMLDivElement | null>(null);
const rightContainerRef = useRef<HTMLDivElement | null>(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'
)}
>
<PersonFrontIcon width={width}></PersonFrontIcon>

View File

@@ -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<any>;
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)}
></div>
)}
<div className="relative w-fit">
<div
className={classNames(
'relative',
display === 'fit' && 'w-fit',
display === 'block' && 'w-full'
)}
>
<div
className={classNames(
'min-h-[35px] text-background-10 px-5 py-2.5 rounded-md focus:ring-4 text-center',
'min-h-[35px] text-background-10 px-5 py-2.5 rounded-md focus:ring-4 text-center dropdown',
'flex cursor-pointer',
variant == 'primary' &&
'bg-background-60 hover:bg-background-50',
variant == 'secondary' &&
'bg-background-70 hover:bg-background-60'
'bg-background-70 hover:bg-background-60',
variant == 'tertiary' &&
'bg-accent-background-30 hover:bg-accent-background-20'
)}
onClick={() => 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'
)}
>
<ul className="py-1 text-sm text-background-20 flex flex-col pr-2">
<ul className="py-1 text-sm flex flex-col ">
{items.map((item) => (
<li
className={classNames(
'py-2 px-4 hover:text-background-10 min-w-max cursor-pointer',
variant == 'primary' && 'hover:bg-background-50',
variant == 'secondary' && 'hover:bg-background-60'
'py-2 px-4 min-w-max cursor-pointer pr-2',
variant == 'primary' &&
'hover:bg-background-50 text-background-20 hover:text-background-10',
variant == 'secondary' &&
'hover:bg-background-60 text-background-20 hover:text-background-10',
variant == 'tertiary' &&
value !== item.value &&
'bg-accent-background-30 hover:bg-accent-background-20',
variant == 'tertiary' &&
value === item.value &&
'bg-accent-background-20'
)}
onClick={() => {
onChange(item.value);

View File

@@ -60,7 +60,7 @@ export function WarningBox({
<div className="flex flex-col">
<Typography
color="text-background-60"
whitespace={whitespace ? 'whitespace-pre' : undefined}
whitespace={whitespace ? 'whitespace-pre-line' : undefined}
>
{children}
</Typography>

View File

@@ -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' &&

View File

@@ -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 (
<div className="overflow-y-auto flex flex-col gap-2">
<div className="flex flex-col flex-wrap gap-3 px-4 pt-4 lg:flex-row">
{filteredStatuses
.filter(([, status]) => status.prioritized)
.map(([, status]) => (
<div className="md:w-1/2 w-full" key={status.id}>
<div className="relative h-full">
<NavLink
to="/vr-mode"
className="xs:hidden absolute z-50 h-12 w-12 rounded-full bg-accent-background-30 bottom-3 right-3 flex justify-center items-center fill-background-10"
>
<HeadsetIcon></HeadsetIcon>
</NavLink>
<div className="h-full overflow-y-auto">
<div className="px-2 pt-4 gap-3 w-full grid md:grid-cols-2 mobile:grid-cols-1">
{filteredStatuses
.filter(([, status]) => status.prioritized)
.map(([, status]) => (
<Localized
key={status.id}
id={`status_system-${StatusData[status.dataType]}`}
vars={parseStatusToLocale(status, trackers)}
>
@@ -53,42 +61,44 @@ export function Home() {
{`Warning, you should fix ${StatusData[status.dataType]}`}
</WarningBox>
</Localized>
))}
</div>
<div className="overflow-y-auto flex flex-col gap-2">
{trackers.length === 0 && (
<div className="flex px-5 pt-5 justify-center">
<Typography variant="standard">
{l10n.getString('home-no_trackers')}
</Typography>
</div>
))}
</div>
{trackers.length === 0 && (
<div className="flex px-5 pt-5 justify-center">
<Typography variant="standard">
{l10n.getString('home-no_trackers')}
</Typography>
</div>
)}
)}
{!config?.debug && trackers.length > 0 && (
<div className="grid sm:grid-cols-1 md:grid-cols-2 gap-3 px-4 my-4">
{trackers.map(({ tracker, device }, index) => (
<TrackerCard
key={index}
tracker={tracker}
device={device}
onClick={() => sendToSettings(tracker)}
smol
interactable
warning={Object.values(statuses).some((status) =>
trackerStatusRelated(tracker, status)
)}
/>
))}
{!config?.debug && trackers.length > 0 && (
<div className="grid sm:grid-cols-1 md:grid-cols-2 gap-3 px-2 my-2">
{trackers.map(({ tracker, device }, index) => (
<TrackerCard
key={index}
tracker={tracker}
device={device}
onClick={() => sendToSettings(tracker)}
smol
interactable
warning={Object.values(statuses).some((status) =>
trackerStatusRelated(tracker, status)
)}
/>
))}
</div>
)}
{config?.debug && trackers.length > 0 && (
<div className="px-2 pt-5 overflow-y-scroll overflow-x-auto">
<TrackersTable
flatTrackers={trackers}
clickedTracker={(tracker) => sendToSettings(tracker)}
></TrackersTable>
</div>
)}
</div>
)}
{config?.debug && trackers.length > 0 && (
<div className="px-2 pt-5 overflow-y-scroll overflow-x-auto">
<TrackersTable
flatTrackers={trackers}
clickedTracker={(tracker) => sendToSettings(tracker)}
></TrackersTable>
</div>
)}
</div>
</div>
);
}

View File

@@ -34,12 +34,14 @@ export function BodyAssignment({
rolesWithErrors = {},
highlightedRoles = [],
onlyAssigned = false,
width,
}: {
advanced: boolean;
onlyAssigned?: boolean;
rolesWithErrors?: Partial<Record<BodyPart, BodyPartError>>;
highlightedRoles?: BodyPart[];
onRoleSelected: (role: BodyPart) => void;
width?: number;
}) {
const { useAssignedTrackers } = useTrackers();
@@ -74,6 +76,7 @@ export function BodyAssignment({
return (
<>
<BodyInteractions
width={width}
assignedRoles={assignedRoles}
highlightedRoles={highlightedRoles}
onSelectRole={onRoleSelected}

View File

@@ -3,9 +3,11 @@ import { useLayout } from '../../hooks/layout';
import { useOnboarding } from '../../hooks/onboarding';
import { MainLayoutRoute } from '../MainLayout';
import { TopBar } from '../TopBar';
import { useBreakpoint } from '../../hooks/breakpoint';
export function OnboardingLayout({ children }: { children: ReactNode }) {
const { layoutHeight, ref } = useLayout<HTMLDivElement>();
const { isMobile } = useBreakpoint('mobile');
const { state } = useOnboarding();
return !state.alonePage ? (
@@ -13,15 +15,15 @@ export function OnboardingLayout({ children }: { children: ReactNode }) {
<TopBar progress={state.progress}></TopBar>
<div
ref={ref}
className="flex-grow pt-10 mx-4"
className="flex-grow xs:pt-10 mobile:pt-2"
style={{ height: layoutHeight }}
>
{children}
</div>
</>
) : (
<MainLayoutRoute widgets={false}>
<div className="flex-grow pt-10 mx-4">{children}</div>
<MainLayoutRoute widgets={false} isMobile={isMobile}>
<div className="flex-grow xs:pt-10 mobile:pt-2">{children}</div>
</MainLayoutRoute>
);
}

View File

@@ -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}
>

View File

@@ -39,7 +39,7 @@ export function SkipSetupWarningModal({
overlayClassName={props.overlayClassName}
>
<div className="flex w-full h-full flex-col ">
<div className="flex w-full flex-col flex-grow items-center gap-3">
<div className="flex flex-col flex-grow items-center gap-3">
<Localized id="onboarding-setup_warning" elems={{ b: <b></b> }}>
<WarningBox>
<b>Warning:</b> The setup is needed for good tracking, this is

View File

@@ -40,7 +40,7 @@ export function StepContainer({
return (
<div
className={classNames(
'step-container transition-transform duration-500 w-full p-8 rounded-lg flex gap-4 h-full',
'step-container transition-transform duration-500 relative w-full xs:p-8 mobile:p-2 rounded-lg flex gap-4 h-full',
!active && 'opacity-40 pointer-events-none',
variant === 'onboarding' && 'bg-background-70',
variant === 'alone' && 'bg-background-60'
@@ -51,7 +51,7 @@ export function StepContainer({
}}
>
{type === 'numbered' && (
<div className="flex flex-col">
<div className="xs:flex xs:flex-col mobile:absolute mobile:-top-3 mobile:-right-4">
<div className="bg-accent-background-40 rounded-full h-8 w-8 flex flex-col items-center justify-center">
<Typography variant="section-title" bold>
{step + 1}

View File

@@ -77,11 +77,11 @@ export function CalibrationTutorialPage() {
modalVisible={skipWarning}
onClick={() => setSkipWarning(true)}
></SkipSetupButton>
<div className="flex w-full h-full justify-center px-20 gap-14">
<div className="flex gap-8 self-center">
<div className="flex w-full h-full justify-center xs:px-20 mobile:px-5 pb-5 gap-14">
<div className="flex gap-4 self-center mobile:z-10">
<div className="flex flex-col max-w-md gap-3">
<div>
<Typography variant="main-title">
<Typography variant="mobile-title">
{l10n.getString('onboarding-calibration_tutorial')}
</Typography>
<Typography variant="vr-accessible" italic>
@@ -97,6 +97,11 @@ export function CalibrationTutorialPage() {
</Typography>
</Localized>
<div>
<div className="xs:hidden flex flex-row justify-center">
<div className="stroke-none fill-background-10 ">
<TaybolIcon width="220"></TaybolIcon>
</div>
</div>
<div className="flex justify-center">
<LoaderIcon slimeState={slimeStatus}></LoaderIcon>
</div>
@@ -116,11 +121,11 @@ export function CalibrationTutorialPage() {
<div className="flex justify-center">
<Typography variant="section-title">{progressText}</Typography>
</div>
<div className="flex gap-3">
<div className="flex gap-3 mobile:flex-col">
<Button
variant="secondary"
to="/onboarding/wifi-creds"
className="mr-auto"
className="xs:mr-auto"
>
{l10n.getString('onboarding-previous_step')}
</Button>
@@ -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')}
</Button>
@@ -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() {
</div>
</div>
</div>
<div className="flex self-center w-[32rem]">
<div className="stroke-none fill-background-10">
<div className="mobile:hidden flex self-center w-[32rem] mobile:absolute">
<div className="stroke-none xs:fill-background-10 mobile:fill-background-50 mobile:blur-sm">
<TaybolIcon width="500"></TaybolIcon>
{/* <img src="/images/taybol.png"></img> */}
</div>
</div>
</div>

View File

@@ -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<HTMLDivElement>();
const { trackers, useConnectedTrackers } = useTrackers();
const { layoutHeight, ref } = isMobile
? { layoutHeight: 0, ref: undefined }
: useLayout<HTMLDivElement>();
const { useConnectedTrackers } = useTrackers();
const { applyProgress, state, skipSetup } = useOnboarding();
const navigate = useNavigate();
const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
@@ -128,13 +132,13 @@ export function ConnectTrackersPage() {
}, [provisioningStatus]);
return (
<div className="flex flex-col items-center relative">
<div className="flex flex-col h-full items-center relative overflow-y-auto px-4 pb-4">
<SkipSetupButton
visible={!state.alonePage}
modalVisible={skipWarning}
onClick={() => setSkipWarning(true)}
></SkipSetupButton>
<div className="flex gap-10 w-full max-w-7xl ">
<div className="flex gap-10 mobile:flex-col w-full xs:max-w-7xl">
<div className="flex flex-col w-full max-w-sm">
<Typography variant="main-title">
{l10n.getString('onboarding-connect_tracker-title')}
@@ -225,7 +229,7 @@ export function ConnectTrackersPage() {
</Button>
</div>
</div>
<div className="flex flex-col flex-grow">
<div className="flex flex-col xs:flex-grow">
<Typography color="secondary" bold>
{l10n.getString('onboarding-connect_tracker-connected_trackers', {
amount: connectedTrackers.length,
@@ -233,20 +237,20 @@ export function ConnectTrackersPage() {
</Typography>
<div
className="flex-grow overflow-y-scroll"
className="xs:flex-grow xs:overflow-y-scroll"
ref={ref}
style={{ height: layoutHeight - BOTTOM_HEIGHT }}
style={isMobile ? { height: layoutHeight - BOTTOM_HEIGHT } : {}}
>
<div className="grid lg:grid-cols-2 md:grid-cols-1 gap-2 mx-3 pt-3">
<div className="grid lg:grid-cols-2 md:grid-cols-1 gap-2 xs:mx-3 pt-3">
{Array.from({
...connectedTrackers,
length: Math.max(trackers.length, 20),
length: Math.max(connectedTrackers.length, isMobile ? 1 : 20),
}).map((tracker, index) => (
<div key={index}>
{!tracker && (
<div
className={classNames(
'rounded-xl h-16',
'rounded-xl h-16 mobile:animate-pulse',
state.alonePage
? 'bg-background-80'
: 'bg-background-70'

View File

@@ -17,7 +17,7 @@ export function HomePage() {
return (
<>
<div className="flex flex-col gap-5 h-full items-center w-full justify-center relative">
<div className="flex flex-col gap-5 h-full items-center w-full justify-center relative px-4">
<SkipSetupButton
visible={true}
modalVisible={skipWarning}
@@ -25,14 +25,14 @@ export function HomePage() {
></SkipSetupButton>
<div className="flex flex-col gap-5 items-center z-10 scale-150 mb-20">
<SlimeVRIcon></SlimeVRIcon>
<Typography variant="main-title">
<Typography variant="mobile-title">
{l10n.getString('onboarding-home')}
</Typography>
<Button variant="primary" to="/onboarding/wifi-creds">
{l10n.getString('onboarding-home-start')}
</Button>
</div>
<div className="absolute right-0 bottom-4 z-50">
<div className="absolute right-4 bottom-4 z-50">
<LangSelector />
</div>
<div

View File

@@ -18,8 +18,10 @@ import { useTrackers } from '../../../hooks/tracker';
import { BodyDisplay } from '../../commons/BodyDisplay';
import { useWebsocketAPI } from '../../../hooks/websocket-api';
import classNames from 'classnames';
import { useBreakpoint } from '../../../hooks/breakpoint';
export function ResetTutorialPage() {
const { isMobile } = useBreakpoint('mobile');
const { l10n } = useLocalization();
const { applyProgress, skipSetup } = useOnboarding();
const [skipWarning, setSkipWarning] = useState(false);
@@ -119,80 +121,79 @@ export function ResetTutorialPage() {
];
return (
<div className="flex flex-col gap-5 h-full items-center w-full justify-center relative">
<div className="flex flex-col gap-5 h-full items-center w-full xs:justify-center relative overflow-y-auto">
<SkipSetupButton
visible={true}
modalVisible={skipWarning}
onClick={() => setSkipWarning(true)}
></SkipSetupButton>
<div className="flex flex-col w-full h-full justify-center px-20">
<div className="flex gap-8 self-center">
<div className="flex flex-col gap-3 w-96 self-center">
<Typography variant="main-title">
{l10n.getString('onboarding-reset_tutorial')}
</Typography>
<Typography color="secondary">
{l10n.getString('onboarding-reset_tutorial-description')}
</Typography>
<div className="flex">
<Button variant="secondary" to="/onboarding/mounting/choose">
{l10n.getString('onboarding-previous_step')}
</Button>
<div className="flex xs:flex-row mobile:flex-col w-full h-full xs:justify-center xs:px-20 mobile:px-4 gap-8 self-center">
<div className="flex flex-col gap-3 xs:w-96 self-center">
<Typography variant="main-title">
{l10n.getString('onboarding-reset_tutorial')}
</Typography>
<Typography color="secondary">
{l10n.getString('onboarding-reset_tutorial-description')}
</Typography>
<div className="flex">
<Button variant="secondary" to="/onboarding/mounting/choose">
{l10n.getString('onboarding-previous_step')}
</Button>
<Button
hidden={curIndex + 1 >= order.length}
variant="secondary"
className="ml-auto"
onClick={() => {
setCurIndex(curIndex + 1);
}}
>
{l10n.getString('onboarding-reset_tutorial-skip')}
</Button>
<Button
hidden={curIndex + 1 >= order.length}
variant="secondary"
className="ml-auto"
onClick={() => {
setCurIndex(curIndex + 1);
}}
>
{l10n.getString('onboarding-reset_tutorial-skip')}
</Button>
<Button
variant="primary"
to="/onboarding/body-proportions/choose"
className={classNames(
'ml-auto',
order.length > curIndex + 1 && 'hidden'
)}
>
{l10n.getString('onboarding-continue')}
</Button>
</div>
<div
<Button
variant="primary"
to="/onboarding/body-proportions/choose"
className={classNames(
'self-center w-72 md:hidden mt-10 ml-auto border-background-10',
'border-l-4 pl-4',
curIndex < order.length && 'visible',
curIndex >= order.length && 'hidden'
'ml-auto',
order.length > curIndex + 1 && 'hidden'
)}
>
<Typography whitespace="whitespace-pre-line" color="secondary">
{l10n.getString(`onboarding-reset_tutorial-${curIndex}`, {
taps: tapSettings[curIndex],
})}
</Typography>
</div>
{l10n.getString('onboarding-continue')}
</Button>
</div>
<div className="flex flex-row">
<BodyDisplay
trackers={[order[curIndex]]}
hideUnassigned={true}
></BodyDisplay>
<div
className={classNames(
'self-center w-72 md-max:hidden',
curIndex >= order.length && 'hidden'
)}
>
<Typography whitespace="whitespace-pre-line" color="secondary">
{l10n.getString(`onboarding-reset_tutorial-${curIndex}`, {
taps: tapSettings[curIndex],
})}
</Typography>
</div>
<div
className={classNames(
'self-center xs:w-72 md:hidden xs:mt-10 mobile:mt-5 xs:ml-auto border-background-10',
'border-l-4 pl-4',
curIndex < order.length && 'visible',
curIndex >= order.length && 'hidden'
)}
>
<Typography whitespace="whitespace-pre-line" color="secondary">
{l10n.getString(`onboarding-reset_tutorial-${curIndex}`, {
taps: tapSettings[curIndex],
})}
</Typography>
</div>
</div>
<div className="flex flex-row justify-center">
<BodyDisplay
width={isMobile ? 160 : undefined}
trackers={[order[curIndex]]}
hideUnassigned={true}
></BodyDisplay>
<div
className={classNames(
'self-center w-72 md-max:hidden',
curIndex >= order.length && 'hidden'
)}
>
<Typography whitespace="whitespace-pre-line" color="secondary">
{l10n.getString(`onboarding-reset_tutorial-${curIndex}`, {
taps: tapSettings[curIndex],
})}
</Typography>
</div>
</div>
</div>

View File

@@ -28,13 +28,13 @@ export function WifiCredsPage() {
className="flex flex-col w-full h-full"
onSubmit={handleSubmit(submitWifiCreds)}
>
<div className="flex flex-col w-full h-full justify-center items-center relative">
<div className="flex flex-col w-full h-full xs:justify-center items-center relative ">
<SkipSetupButton
visible={true}
modalVisible={skipWarning}
onClick={() => setSkipWarning(true)}
></SkipSetupButton>
<div className="flex gap-10">
<div className="flex mobile:flex-col xs:gap-10 px-4">
<div className="flex flex-col max-w-sm">
<Typography variant="main-title">
{l10n.getString('onboarding-wifi_creds')}

View File

@@ -29,67 +29,69 @@ export function AssignmentTutorialPage() {
modalVisible={skipWarning}
onClick={() => setSkipWarning(true)}
></SkipSetupButton>
<div className="flex flex-col w-full h-full justify-center px-20 gap-3">
<div className="mt-10 self-center">
<Typography variant="main-title">
{l10n.getString('onboarding-assignment_tutorial')}
</Typography>
</div>
<div className="flex flex-col self-center justify-center gap-5">
<div className="flex gap-12 flex-row self-center justify-center">
<div className="flex flex-col gap-5 w-1/4">
<div>
<Typography variant="section-title">
{l10n.getString(
'onboarding-assignment_tutorial-first_step'
)}
</Typography>
</div>
<div className="stroke-background-10 fill-background-10">
<StickerSlime width="65%"></StickerSlime>
</div>
</div>
<div className="flex flex-col gap-10 w-1/4">
<div>
<Typography variant="section-title">
{l10n.getString(
'onboarding-assignment_tutorial-second_step'
)}
</Typography>
</div>
<div className="fill-background-10 stroke-background-10">
<TrackerArrow width="75%"></TrackerArrow>
</div>
<div>
<Typography variant="section-title">
{l10n.getString(
'onboarding-assignment_tutorial-second_step-continuation'
)}
</Typography>
</div>
<div className="fill-background-10 stroke-background-10">
<ExtensionArrow width="75%"></ExtensionArrow>
</div>
</div>
<div className="flex flex-col w-full h-full justify-center xs:px-20 gap-3 pb-2">
<div className="overflow-y-auto px-4">
<div className="mb-10 self-center">
<Typography variant="main-title">
{l10n.getString('onboarding-assignment_tutorial')}
</Typography>
</div>
<div className="flex">
<Button
variant="secondary"
to={
bnoExists
? '/onboarding/calibration-tutorial'
: '/onboarding/wifi-creds'
}
>
{l10n.getString('onboarding-previous_step')}
</Button>
<Button
variant="primary"
to="/onboarding/trackers-assign"
className="ml-auto"
>
{l10n.getString('onboarding-assignment_tutorial-done')}
</Button>
<div className="flex flex-col self-center justify-center gap-5 px-2">
<div className="flex gap-12 xs:flex-row mobile:flex-col self-center justify-center">
<div className="flex flex-col gap-5 xs:w-1/4">
<div>
<Typography variant="section-title">
{l10n.getString(
'onboarding-assignment_tutorial-first_step'
)}
</Typography>
</div>
<div className="stroke-background-10 fill-background-10 flex justify-center">
<StickerSlime width="65%"></StickerSlime>
</div>
</div>
<div className="flex flex-col gap-10 xs:w-1/4">
<div>
<Typography variant="section-title">
{l10n.getString(
'onboarding-assignment_tutorial-second_step'
)}
</Typography>
</div>
<div className="fill-background-10 stroke-background-10 flex justify-center">
<TrackerArrow width="75%"></TrackerArrow>
</div>
<div>
<Typography variant="section-title">
{l10n.getString(
'onboarding-assignment_tutorial-second_step-continuation'
)}
</Typography>
</div>
<div className="fill-background-10 stroke-background-10 flex justify-center">
<ExtensionArrow width="75%"></ExtensionArrow>
</div>
</div>
</div>
<div className="flex">
<Button
variant="secondary"
to={
bnoExists
? '/onboarding/calibration-tutorial'
: '/onboarding/wifi-creds'
}
>
{l10n.getString('onboarding-previous_step')}
</Button>
<Button
variant="primary"
to="/onboarding/trackers-assign"
className="ml-auto"
>
{l10n.getString('onboarding-assignment_tutorial-done')}
</Button>
</div>
</div>
</div>
</div>

View File

@@ -39,7 +39,7 @@ export function AutomaticProportionsPage() {
return (
<AutoboneContextC.Provider value={context}>
<div className="flex flex-col gap-5 h-full items-center w-full justify-center relative">
<div className="flex flex-col gap-5 h-full items-center w-full xs:justify-center relative px-4 pb-4">
<SkipSetupButton
visible={!state.alonePage}
modalVisible={skipWarning}
@@ -70,21 +70,19 @@ export function AutomaticProportionsPage() {
></StepperSlider>
</div>
</div>
<div className="w-full pb-4 flex flex-row">
<div className="flex flex-grow gap-3">
<Button
variant="secondary"
onClick={startCountdown}
disabled={isCounting}
>
<div className="relative">
<div className="opacity-0 h-0">
{l10n.getString('reset-reset_all')}
</div>
{!isCounting ? l10n.getString('reset-reset_all') : timer}
<div className="w-full pb-4 flex flex-grow flex-row mobile:justify-center">
<Button
variant="secondary"
onClick={startCountdown}
disabled={isCounting}
>
<div className="relative">
<div className="opacity-0 h-0">
{l10n.getString('reset-reset_all')}
</div>
</Button>
</div>
{!isCounting ? l10n.getString('reset-reset_all') : timer}
</div>
</Button>
</div>
</div>
<SkipSetupWarningModal

View File

@@ -8,6 +8,7 @@ import {
} from '../../../../hooks/manual-proportions';
import { useLocaleConfig } from '../../../../i18n/config';
import { Typography } from '../../../commons/Typography';
import { useBreakpoint } from '../../../../hooks/breakpoint';
function IncrementButton({
children,
@@ -20,10 +21,10 @@ function IncrementButton({
<div
onClick={onClick}
className={classNames(
'p-3 rounded-lg w-16 h-16 flex flex-col justify-center items-center bg-background-60 hover:bg-opacity-50'
'p-3 rounded-lg xs:w-16 xs:h-16 mobile:w-10 flex flex-col justify-center items-center bg-background-60 hover:bg-opacity-50'
)}
>
<Typography variant="main-title" bold>
<Typography variant="mobile-title" bold>
{children}
</Typography>
</div>
@@ -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({
<div className="relative w-full">
<div
className={classNames(
'flex flex-col overflow-y-scroll overflow-x-hidden max-h-[450px] h-[54vh]',
'w-full px-1 gap-3 gradient-mask-b-90'
'flex flex-col xs:overflow-y-scroll xs:overflow-x-hidden xs:max-h-[450px] xs:h-[54vh]',
'w-full px-1 gap-3 xs:gradient-mask-b-90'
)}
>
<>
@@ -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 && (
<IncrementButton
onClick={() =>
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)}
</IncrementButton>
)}
<IncrementButton
onClick={() =>
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)}
</IncrementButton>
{precise && (
<IncrementButton
onClick={() =>
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)}
</IncrementButton>
@@ -185,14 +191,14 @@ export function BodyProportions({
<div
key={label}
className={classNames(
'p-3 rounded-lg h-16 flex w-full items-center justify-between px-6 transition-colors duration-300 bg-background-60',
'p-3 rounded-lg xs:h-16 flex w-full items-center justify-between xs:px-6 mobile:px-3 transition-colors duration-300 bg-background-60',
(selected && 'opacity-100') || 'opacity-50'
)}
>
<Typography variant="section-title" bold>
{l10n.getString(label)}
</Typography>
<Typography variant="main-title" bold>
<Typography variant="mobile-title" bold>
{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 && (
<IncrementButton
onClick={() =>
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)}
</IncrementButton>
)}
<IncrementButton
onClick={() =>
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)}
</IncrementButton>
{!precise && (
<IncrementButton
onClick={() =>
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)}
</IncrementButton>

View File

@@ -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 (
<>
<Button
variant="secondary"
state={{ alonePage: state.alonePage }}
to="/onboarding/body-proportions/choose"
>
{l10n.getString('onboarding-previous_step')}
</Button>
<Button variant="secondary" onClick={resetAll}>
{l10n.getString('reset-reset_all')}
</Button>
{!state.alonePage && (
<Button variant="primary" className="ml-auto" to="/onboarding/done">
{l10n.getString('onboarding-continue')}
</Button>
)}
</>
);
}
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 (
<>
<div className="flex flex-col gap-5 h-full items-center w-full justify-center relative">
<div className="flex flex-col gap-5 h-full items-center w-full xs:justify-center overflow-y-auto relative">
<SkipSetupButton
visible={!state.alonePage}
modalVisible={skipWarning}
onClick={() => setSkipWarning(true)}
></SkipSetupButton>
<div className="flex flex-col w-full h-full max-w-5xl justify-center">
<div className="flex flex-col w-full h-full xs:max-w-5xl xs:justify-center">
<div className="flex gap-8 justify-center">
<div className="flex flex-col w-full max-w-2xl gap-3 items-center">
<div className="flex flex-col w-full xs:max-w-2xl gap-3 items-center">
<div className="flex flex-col">
<Typography variant="main-title">
{l10n.getString('onboarding-manual_proportions-title')}
@@ -67,7 +94,13 @@ export function ManualProportionsPage() {
name="precise"
variant="toggle"
></CheckBox>
{isMobile && (
<div className="flex gap-3 justify-between">
<ButtonsControl></ButtonsControl>
</div>
)}
</div>
<BodyProportions
precise={precise}
type={ratio ? 'ratio' : 'linear'}
@@ -78,31 +111,11 @@ export function ManualProportionsPage() {
<PersonFrontIcon width={200}></PersonFrontIcon>
</div>
</div>
<div className="flex gap-3 mt-5">
<Button
variant="secondary"
state={{ alonePage: state.alonePage }}
to="/onboarding/body-proportions/choose"
>
{l10n.getString('onboarding-previous_step')}
</Button>
<Button variant="secondary" onClick={resetAll}>
{l10n.getString('reset-reset_all')}
</Button>
{!state.alonePage && (
<Button
variant="primary"
className="ml-auto"
to="/onboarding/done"
>
{l10n.getString('onboarding-continue')}
</Button>
)}
</div>
</div>
<div className="w-full py-4 flex flex-row">
<div className="flex flex-grow gap-3"></div>
<div className="flex gap-3"></div>
{!isMobile && (
<div className="flex gap-3 mt-5 mx-4">
<ButtonsControl></ButtonsControl>
</div>
)}
</div>
</div>
<SkipSetupWarningModal

View File

@@ -15,8 +15,10 @@ import { useWebsocketAPI } from '../../../../hooks/websocket-api';
import saveAs from 'file-saver';
import { save } from '@tauri-apps/api/dialog';
import { writeTextFile } from '@tauri-apps/api/fs';
import { useIsTauri } from '../../../../hooks/breakpoint';
export function ProportionsChoose() {
const isTauri = useIsTauri();
const { l10n } = useLocalization();
const { applyProgress, skipSetup, state } = useOnboarding();
const [skipWarning, setSkipWarning] = useState(false);
@@ -29,22 +31,25 @@ export function ProportionsChoose() {
const blob = new Blob([JSON.stringify(data)], {
type: 'application/json',
});
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);
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 (
<>
<div className="flex flex-col gap-5 h-full items-center w-full justify-center relative">
<div className="flex flex-col gap-5 h-full items-center w-full xs:justify-center mobile:overflow-y-auto relative px-4 pb-4">
<SkipSetupButton
visible={!state.alonePage}
modalVisible={skipWarning}
onClick={() => setSkipWarning(true)}
></SkipSetupButton>
<div className="flex flex-col gap-4 justify-center">
<div className="w-[666px]">
<Typography variant="main-title">
{l10n.getString('onboarding-choose_proportions')}
</Typography>
<Typography variant="main-title">
{l10n.getString('onboarding-choose_proportions')}
</Typography>
<div className="xs:w-10/12 xs:max-w-[666px]">
<Typography
variant="standard"
color="secondary"
@@ -71,111 +76,109 @@ export function ProportionsChoose() {
{l10n.getString('onboarding-choose_proportions-description')}
</Typography>
</div>
<div className={classNames('h-full w-[760px] min-w-[760px]')}>
<div className="flex flex-row gap-4 [&>div]:grow">
<div
className={classNames(
'rounded-lg p-4 flex flex-row',
!state.alonePage && 'bg-background-70',
state.alonePage && 'bg-background-60'
)}
>
<div className="flex flex-col gap-4">
<div className="flex flex-grow flex-col gap-4 max-w-sm">
<div>
<Typography variant="main-title" bold>
{l10n.getString(
'onboarding-choose_proportions-manual_proportions'
)}
</Typography>
<Typography variant="vr-accessible" italic>
{l10n.getString(
'onboarding-choose_proportions-manual_proportions-subtitle'
)}
</Typography>
</div>
<div>
<Typography color="secondary">
{l10n.getString(
'onboarding-choose_proportions-manual_proportions-description'
)}
</Typography>
</div>
<div
className={classNames(
'grid xs:grid-cols-2 w-full xs:flex-row mobile:flex-col gap-4 [&>div]:grow'
)}
>
<div
className={classNames(
'rounded-lg p-4 flex flex-row flex-grow',
!state.alonePage && 'bg-background-70',
state.alonePage && 'bg-background-60'
)}
>
<div className="flex flex-col gap-4">
<div className="flex flex-grow flex-col gap-4 max-w-sm">
<div>
<Typography variant="main-title" bold>
{l10n.getString(
'onboarding-choose_proportions-manual_proportions'
)}
</Typography>
<Typography variant="vr-accessible" italic>
{l10n.getString(
'onboarding-choose_proportions-manual_proportions-subtitle'
)}
</Typography>
</div>
<div>
<Typography color="secondary">
{l10n.getString(
'onboarding-choose_proportions-manual_proportions-description'
)}
</Typography>
</div>
</div>
<Button
variant={!state.alonePage ? 'secondary' : 'tertiary'}
to="/onboarding/body-proportions/manual"
className="self-start mt-auto"
state={{ alonePage: state.alonePage }}
>
{l10n.getString('onboarding-automatic_proportions-manual')}
</Button>
</div>
<Button
variant={!state.alonePage ? 'secondary' : 'tertiary'}
to="/onboarding/body-proportions/manual"
className="self-start mt-auto"
state={{ alonePage: state.alonePage }}
>
{l10n.getString('onboarding-automatic_proportions-manual')}
</Button>
</div>
<div
className={classNames(
'rounded-lg p-4 flex flex-row relative',
!state.alonePage && 'bg-background-70',
state.alonePage && 'bg-background-60'
)}
>
<div className="flex flex-col gap-4">
<div className="flex flex-grow flex-col gap-4 max-w-sm">
<div>
<img
onMouseEnter={() => setAnimated(() => true)}
onAnimationEnd={() => setAnimated(() => false)}
src="/images/slimetower.png"
className={classNames(
'absolute w-1/3 -right-2 -top-32',
animated && 'animate-[bounce_1s_1]'
)}
></img>
<Typography variant="main-title" bold>
{l10n.getString(
'onboarding-choose_proportions-auto_proportions'
)}
</Typography>
<Typography variant="vr-accessible" italic>
{l10n.getString(
'onboarding-choose_proportions-auto_proportions-subtitle'
)}
</Typography>
</div>
<div>
<Typography color="secondary">
{l10n.getString(
'onboarding-choose_proportions-auto_proportions-description'
)}
</Typography>
</div>
</div>
<div
className={classNames(
'rounded-lg p-4 flex flex-row relative',
!state.alonePage && 'bg-background-70',
state.alonePage && 'bg-background-60'
)}
>
<div className="flex flex-col gap-4">
<div className="flex flex-grow flex-col gap-4 max-w-sm">
<img
onMouseEnter={() => setAnimated(() => true)}
onAnimationEnd={() => setAnimated(() => false)}
src="/images/slimetower.png"
className={classNames(
'absolute w-[100px] -right-2 -top-24',
animated && 'animate-[bounce_1s_1]'
)}
></img>
<div>
<Typography variant="main-title" bold>
{l10n.getString(
'onboarding-choose_proportions-auto_proportions'
)}
</Typography>
<Typography variant="vr-accessible" italic>
{l10n.getString(
'onboarding-choose_proportions-auto_proportions-subtitle'
)}
</Typography>
</div>
<div>
<Typography color="secondary">
{l10n.getString(
'onboarding-choose_proportions-auto_proportions-description'
)}
</Typography>
</div>
<Button
variant="primary"
to="/onboarding/body-proportions/auto"
className="self-start mt-auto"
state={{ alonePage: state.alonePage }}
>
{l10n.getString('onboarding-manual_proportions-auto')}
</Button>
</div>
<Button
variant="primary"
to="/onboarding/body-proportions/auto"
className="self-start mt-auto"
state={{ alonePage: state.alonePage }}
>
{l10n.getString('onboarding-manual_proportions-auto')}
</Button>
</div>
</div>
</div>
<div className="flex flex-row">
{!state.alonePage && (
<Button
variant="secondary"
className="ml-4"
to="/onboarding/reset-tutorial"
>
<Button variant="secondary" to="/onboarding/reset-tutorial">
{l10n.getString('onboarding-previous_step')}
</Button>
)}
<Button
variant={!state.alonePage ? 'secondary' : 'tertiary'}
className="ml-auto mr-4"
className="ml-auto"
onClick={() =>
sendRPCPacket(
RpcMessage.SkeletonConfigRequest,

View File

@@ -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,6 +13,7 @@ export function PutTrackersOnStep({
nextStep: () => void;
variant: 'alone' | 'onboarding';
}) {
const { isMobile } = useBreakpoint('mobile');
const { l10n } = useLocalization();
const { trackers } = useTrackers();
@@ -36,8 +38,20 @@ export function PutTrackersOnStep({
</div>
</div>
{isMobile && (
<div className="flex flex-col pt-1 items-center fill-background-50 justify-center px-16">
<BodyDisplay
trackers={trackers}
width={120}
dotsSize={15}
variant="dots"
hideUnassigned={true}
/>
</div>
)}
<div className="flex flex-col gap-3">
<div className="flex gap-3">
<div className="flex gap-3 mobile:justify-between">
<Button
variant={variant === 'onboarding' ? 'secondary' : 'tertiary'}
to="/onboarding/body-proportions/choose"
@@ -53,15 +67,17 @@ export function PutTrackersOnStep({
</div>
</div>
</div>
<div className="flex flex-col pt-1 items-center fill-background-50 justify-center px-16">
<BodyDisplay
trackers={trackers}
width={150}
dotsSize={15}
variant="dots"
hideUnassigned={true}
/>
</div>
{!isMobile && (
<div className="flex flex-col pt-1 items-center fill-background-50 justify-center px-16">
<BodyDisplay
trackers={trackers}
width={150}
dotsSize={15}
variant="dots"
hideUnassigned={true}
/>
</div>
)}
</>
);
}

View File

@@ -35,7 +35,7 @@ export function Recording({ nextStep }: { nextStep: () => void }) {
)}
</Typography>
</div>
<ol className="list-decimal">
<ol className="list-decimal mobile:px-4">
<>
{l10n
.getString('onboarding-automatic_proportions-recording-steps')

View File

@@ -22,7 +22,7 @@ export function RequirementsStep({
'onboarding-automatic_proportions-requirements-title'
)}
</Typography>
<ul className="list-disc">
<ul className="list-disc mobile:px-4">
<>
{l10n
.getString(
@@ -38,7 +38,7 @@ export function RequirementsStep({
</ul>
</div>
<div className="flex gap-3">
<div className="flex gap-3 mobile:justify-between">
<Button
variant={variant === 'onboarding' ? 'secondary' : 'tertiary'}
onClick={prevStep}

View File

@@ -37,7 +37,7 @@ export function StartRecording({
)}
</Typography>
</div>
<ol className="list-decimal">
<ol className="list-decimal mobile:px-4">
<>
{l10n
.getString('onboarding-automatic_proportions-recording-steps')
@@ -54,7 +54,7 @@ export function StartRecording({
</div>
</div>
<div className="flex gap-3">
<div className="flex gap-3 mobile:justify-between">
<Button
variant={variant === 'onboarding' ? 'secondary' : 'tertiary'}
onClick={prevStep}

View File

@@ -25,14 +25,14 @@ export function AutomaticMountingPage() {
return (
<>
<div className="flex flex-col gap-2 h-full items-center w-full justify-center relative">
<div className="flex flex-col gap-2 h-full items-center w-full xs:justify-center relative overflow-y-auto overflow-x-hidden px-4 pb-4">
<SkipSetupButton
visible={!state.alonePage}
modalVisible={skipWarning}
onClick={() => setSkipWarning(true)}
></SkipSetupButton>
<div className="flex flex-col w-full h-full justify-center max-w-3xl gap-5">
<div className="flex flex-col max-w-lg gap-3">
<div className="flex flex-col w-full h-full xs:justify-center xs:max-w-3xl gap-5">
<div className="flex flex-col xs:max-w-lg gap-3">
<Typography variant="main-title">
{l10n.getString('onboarding-automatic_mounting-title')}
</Typography>
@@ -40,7 +40,7 @@ export function AutomaticMountingPage() {
{l10n.getString('onboarding-automatic_mounting-description')}
</Typography>
</div>
<div className="flex">
<div className="flex pb-4">
<StepperSlider
variant={state.alonePage ? 'alone' : 'onboarding'}
steps={steps}

View File

@@ -13,8 +13,10 @@ import { MountingSelectionMenu } from './MountingSelectionMenu';
import { useLocalization } from '@fluent/react';
import { SkipSetupWarningModal } from '../../SkipSetupWarningModal';
import { SkipSetupButton } from '../../SkipSetupButton';
import { useBreakpoint } from '../../../../hooks/breakpoint';
export function ManualMountingPage() {
const { isMobile } = useBreakpoint('mobile');
const { l10n } = useLocalization();
const { applyProgress, skipSetup, state } = useOnboarding();
const { sendRPCPacket } = useWebsocketAPI();
@@ -64,45 +66,44 @@ export function ManualMountingPage() {
onClose={() => setSelectRole(BodyPart.NONE)}
onDirectionSelected={onDirectionSelected}
></MountingSelectionMenu>
<div className="flex flex-col gap-5 h-full items-center w-full justify-center relative">
<div className="flex flex-col gap-5 h-full items-center w-full xs:justify-center relative overflow-y-auto">
<SkipSetupButton
visible={!state.alonePage}
modalVisible={skipWarning}
onClick={() => setSkipWarning(true)}
></SkipSetupButton>
<div className="flex flex-col w-full h-full justify-center items-center">
<div className="flex md:gap-8">
<div className="flex flex-col w-full max-w-md gap-3">
<Typography variant="main-title">
{l10n.getString('onboarding-manual_mounting')}
</Typography>
<Typography color="secondary">
{l10n.getString('onboarding-manual_mounting-description')}
</Typography>
<TipBox>{l10n.getString('tips-find_tracker')}</TipBox>
<div className="flex flex-row gap-3 mt-auto">
<Button
variant="secondary"
to="/onboarding/mounting/choose"
state={state}
>
{l10n.getString('onboarding-previous_step')}
<div className="flex xs:flex-row mobile:flex-col h-full px-8 xs:w-full xs:justify-center mobile:px-4 items-center">
<div className="flex flex-col w-full xs:max-w-sm gap-3">
<Typography variant="main-title">
{l10n.getString('onboarding-manual_mounting')}
</Typography>
<Typography color="secondary">
{l10n.getString('onboarding-manual_mounting-description')}
</Typography>
<TipBox>{l10n.getString('tips-find_tracker')}</TipBox>
<div className="flex flex-row gap-3 mt-auto">
<Button
variant="secondary"
to="/onboarding/mounting/choose"
state={state}
>
{l10n.getString('onboarding-previous_step')}
</Button>
{!state.alonePage && (
<Button variant="primary" to="/onboarding/reset-tutorial">
{l10n.getString('onboarding-manual_mounting-next')}
</Button>
{!state.alonePage && (
<Button variant="primary" to="/onboarding/reset-tutorial">
{l10n.getString('onboarding-manual_mounting-next')}
</Button>
)}
</div>
</div>
<div className="flex flex-col flex-grow gap-3 rounded-xl fill-background-50">
<BodyAssignment
onlyAssigned={true}
advanced={true}
onRoleSelected={setSelectRole}
></BodyAssignment>
)}
</div>
</div>
<div className="flex flex-row justify-center">
<BodyAssignment
width={isMobile ? 160 : undefined}
onlyAssigned={true}
advanced={true}
onRoleSelected={setSelectRole}
></BodyAssignment>
</div>
</div>
</div>
<SkipSetupWarningModal

View File

@@ -10,20 +10,21 @@ import { Button } from '../../../commons/Button';
export function MountingChoose() {
const { l10n } = useLocalization();
const { applyProgress, skipSetup, state } = useOnboarding();
const [animated, setAnimated] = useState(false);
const [skipWarning, setSkipWarning] = useState(false);
applyProgress(0.65);
return (
<>
<div className="flex flex-col gap-5 h-full items-center w-full justify-center relative">
<div className="flex flex-col gap-5 h-full items-center w-full xs:justify-center relative overflow-y-auto px-4 pb-4">
<SkipSetupButton
visible={!state.alonePage}
modalVisible={skipWarning}
onClick={() => setSkipWarning(true)}
></SkipSetupButton>
<div className="flex flex-col gap-4 justify-center">
<div className="w-[666px]">
<div className="xs:w-10/12 xs:max-w-[666px]">
<Typography variant="main-title">
{l10n.getString('onboarding-choose_mounting')}
</Typography>
@@ -35,99 +36,106 @@ export function MountingChoose() {
{l10n.getString('onboarding-choose_mounting-description')}
</Typography>
</div>
<div className={classNames('h-full w-[760px] min-w-[760px]')}>
<div className="flex flex-row gap-4 [&>div]:grow">
<div
className={classNames(
'rounded-lg p-4 flex flex-row',
!state.alonePage && 'bg-background-70',
state.alonePage && 'bg-background-60'
)}
>
<div className="flex flex-col gap-4">
<div className="flex flex-grow flex-col gap-4 max-w-sm">
<div>
<Typography variant="main-title" bold>
{l10n.getString(
'onboarding-choose_mounting-manual_mounting'
)}
</Typography>
<Typography variant="vr-accessible" italic>
{l10n.getString(
'onboarding-choose_mounting-manual_mounting-subtitle'
)}
</Typography>
</div>
<div>
<Typography color="secondary">
{l10n.getString(
'onboarding-choose_mounting-manual_mounting-description'
)}
</Typography>
</div>
<div
className={classNames(
'grid xs:grid-cols-2 w-full xs:flex-row mobile:flex-col gap-4 [&>div]:grow'
)}
>
<div
className={classNames(
'rounded-lg p-4 flex',
!state.alonePage && 'bg-background-70',
state.alonePage && 'bg-background-60'
)}
>
<div className="flex flex-col gap-4">
<div className="flex flex-grow flex-col gap-4 max-w-sm">
<div>
<Typography variant="main-title" bold>
{l10n.getString(
'onboarding-choose_mounting-manual_mounting'
)}
</Typography>
<Typography variant="vr-accessible" italic>
{l10n.getString(
'onboarding-choose_mounting-manual_mounting-subtitle'
)}
</Typography>
</div>
<div>
<Typography color="secondary">
{l10n.getString(
'onboarding-choose_mounting-manual_mounting-description'
)}
</Typography>
</div>
</div>
<Button
variant={!state.alonePage ? 'secondary' : 'tertiary'}
to="/onboarding/mounting/manual"
className="self-start mt-auto"
state={{ alonePage: state.alonePage }}
>
{l10n.getString(
'onboarding-automatic_mounting-manual_mounting'
)}
</Button>
</div>
<Button
variant={!state.alonePage ? 'secondary' : 'tertiary'}
to="/onboarding/mounting/manual"
className="self-start mt-auto"
state={{ alonePage: state.alonePage }}
>
{l10n.getString(
'onboarding-automatic_mounting-manual_mounting'
)}
</Button>
</div>
<div
className={classNames(
'rounded-lg p-4 flex flex-row relative',
!state.alonePage && 'bg-background-70',
state.alonePage && 'bg-background-60'
)}
>
<div className="flex flex-col gap-4">
<div className="flex flex-grow flex-col gap-4 max-w-sm">
<div>
<img
src="/images/boxslime.png"
className="absolute w-1/3 -right-10 -top-16"
></img>
<Typography variant="main-title" bold>
{l10n.getString(
'onboarding-choose_mounting-auto_mounting'
)}
</Typography>
<Typography variant="vr-accessible" italic>
{l10n.getString(
'onboarding-choose_mounting-auto_mounting-subtitle'
)}
</Typography>
</div>
<div>
<Typography color="secondary">
{l10n.getString(
'onboarding-choose_mounting-auto_mounting-description'
)}
</Typography>
</div>
</div>
<div
className={classNames(
'rounded-lg p-4 flex flex-row relative',
!state.alonePage && 'bg-background-70',
state.alonePage && 'bg-background-60'
)}
>
<div className="flex flex-col gap-4">
<div className="flex flex-grow flex-col gap-4 max-w-sm">
<div>
<img
onMouseEnter={() => setAnimated(() => true)}
onAnimationEnd={() => setAnimated(() => false)}
src="/images/boxslime.png"
className={classNames(
'absolute w-[100px] -right-2 -top-10',
animated && 'animate-[bounce_1s_1]'
)}
></img>
<Typography variant="main-title" bold>
{l10n.getString(
'onboarding-choose_mounting-auto_mounting'
)}
</Typography>
<Typography variant="vr-accessible" italic>
{l10n.getString(
'onboarding-choose_mounting-auto_mounting-subtitle'
)}
</Typography>
</div>
<div>
<Typography color="secondary">
{l10n.getString(
'onboarding-choose_mounting-auto_mounting-description'
)}
</Typography>
</div>
<Button
variant="primary"
to="/onboarding/mounting/auto"
className="self-start mt-auto"
state={{ alonePage: state.alonePage }}
>
{l10n.getString('onboarding-manual_mounting-auto_mounting')}
</Button>
</div>
<Button
variant="primary"
to="/onboarding/mounting/auto"
className="self-start mt-auto"
state={{ alonePage: state.alonePage }}
>
{l10n.getString('onboarding-manual_mounting-auto_mounting')}
</Button>
</div>
</div>
</div>
{!state.alonePage && (
<Button
variant="secondary"
className="self-start ml-4"
className="self-start"
to="/onboarding/trackers-assign"
>
{l10n.getString('onboarding-previous_step')}

View File

@@ -19,7 +19,7 @@ function MoutingOrientationCard({
return (
<div
onClick={onClick}
className="h-32 bg-background-60 rounded-md flex justify-between p-4 hover:bg-background-50"
className="xs:h-32 mobile:h-20 bg-background-60 rounded-md flex justify-between p-4 hover:bg-background-50"
>
<div className="flex flex-col justify-center">
<Typography variant="main-title">{orientation}</Typography>
@@ -56,7 +56,7 @@ export function MountingSelectionMenu({
'fixed top-0 right-0 left-0 bottom-0 flex flex-col items-center w-full h-full 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-20 z-10 px-2'
)}
>
<div className="flex w-full h-full flex-col ">
@@ -64,11 +64,11 @@ export function MountingSelectionMenu({
{l10n.getString('mounting_selection_menu')}
</Typography>
<div
className="flex w-full flex-col flex-grow items-center gap-3 justify-center"
className="flex w-full flex-col flex-grow items-center gap-3 xs:justify-center"
ref={refTrackers}
style={{ height: trackersHeight - optionsHeight }}
>
<div className="grid grid-cols-2 grid-rows-2 gap-6 w-full">
<div className="grid xs:grid-cols-2 xs:grid-rows-2 mobile:grid-cols-1 gap-6 w-full">
<MoutingOrientationCard
orientation={l10n.getString('tracker-rotation-left')}
onClick={() => onDirectionSelected(rotationToQuatMap.LEFT)}

View File

@@ -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 MountingResetStep({
nextStep,
@@ -13,6 +14,7 @@ export function MountingResetStep({
prevStep: () => void;
variant: 'onboarding' | 'alone';
}) {
const { isMobile } = useBreakpoint('mobile');
const { l10n } = useLocalization();
return (
@@ -38,7 +40,17 @@ export function MountingResetStep({
</div>
</div>
<div className="flex gap-3">
{isMobile && (
<div className="flex flex-col pt-1 items-center fill-background-50 justify-center px-12">
<img
src="/images/mounting-reset-pose.png"
width={125}
alt="mounting reset ski pose"
/>
</div>
)}
<div className="flex gap-3 mobile:justify-between">
<Button
variant={variant === 'onboarding' ? 'secondary' : 'tertiary'}
onClick={prevStep}
@@ -52,13 +64,15 @@ export function MountingResetStep({
></ResetButton>
</div>
</div>
<div className="flex flex-col pt-1 items-center fill-background-50 justify-center px-12">
<img
src="/images/mounting-reset-pose.png"
width={125}
alt="mounting reset ski pose"
/>
</div>
{!isMobile && (
<div className="flex flex-col pt-1 items-center fill-background-50 justify-center px-12">
<img
src="/images/mounting-reset-pose.png"
width={125}
alt="mounting reset ski pose"
/>
</div>
)}
</>
);
}

View File

@@ -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 (
<>
<div className="flex flex-col flex-grow">
<div className="flex mobile:flex-col items-center w-full">
<div className="flex flex-col flex-grow justify-between">
<div className="flex flex-col gap-4 max-w-sm">
<Typography variant="main-title" bold>
{l10n.getString('onboarding-automatic_mounting-preparation-title')}
@@ -35,9 +37,16 @@ export function PreparationStep({
</Typography>
</div>
</div>
<div className="flex flex-grow items-center"></div>
<div className="flex gap-3">
{isMobile && (
<div className="flex flex-col pt-1 items-center fill-background-50 justify-center px-12">
<img
src="/images/reset-pose.png"
width={100}
alt="Reset position"
/>
</div>
)}
<div className="flex gap-3 mobile:justify-between">
<Button
variant={variant === 'onboarding' ? 'secondary' : 'tertiary'}
onClick={prevStep}
@@ -51,9 +60,11 @@ export function PreparationStep({
></ResetButton>
</div>
</div>
<div className="flex flex-col pt-1 items-center fill-background-50 justify-center px-12">
<img src="/images/reset-pose.png" width={90} alt="Reset position" />
</div>
</>
{!isMobile && (
<div className="flex flex-col pt-1 items-center fill-background-50 justify-center px-12">
<img src="/images/reset-pose.png" width={90} alt="Reset position" />
</div>
)}
</div>
);
}

View File

@@ -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 (
<>
<div className="flex flex-col flex-grow">
<div className="flex mobile:flex-col items-center w-full">
<div className="flex flex-col flex-grow gap-2">
<div className="flex flex-grow flex-col gap-4 max-w-sm">
<Typography variant="main-title" bold>
{l10n.getString(
@@ -36,8 +38,20 @@ export function PutTrackersOnStep({
</div>
</div>
{isMobile && (
<div className="flex flex-col pt-1 items-center fill-background-50 justify-center px-16">
<BodyDisplay
trackers={trackers}
width={150}
dotsSize={15}
variant="dots"
hideUnassigned={true}
/>
</div>
)}
<div className="flex flex-col gap-3">
<div className="flex gap-3">
<div className="flex gap-3 mobile:justify-between">
<Button
variant={variant === 'onboarding' ? 'secondary' : 'tertiary'}
to="/onboarding/mounting/choose"
@@ -53,15 +67,17 @@ export function PutTrackersOnStep({
</div>
</div>
</div>
<div className="flex flex-col pt-1 items-center fill-background-50 justify-center px-16">
<BodyDisplay
trackers={trackers}
width={150}
dotsSize={15}
variant="dots"
hideUnassigned={true}
/>
</div>
</>
{!isMobile && (
<div className="flex flex-col pt-1 items-center fill-background-50 justify-center px-16">
<BodyDisplay
trackers={trackers}
width={150}
dotsSize={15}
variant="dots"
hideUnassigned={true}
/>
</div>
)}
</div>
);
}

View File

@@ -250,15 +250,17 @@ export function TrackersAssignPage() {
onClose={() => closeChokerWarning(true)}
accept={() => closeChokerWarning(false)}
></NeckWarningModal>
<div className="flex flex-col gap-5 h-full items-center w-full justify-center relative">
<div className="relative mx-4 top-4">
<SkipSetupButton
visible={!state.alonePage}
modalVisible={skipWarning}
onClick={() => setSkipWarning(true)}
></SkipSetupButton>
<div className="flex flex-col w-full h-full justify-center items-center">
<div className="flex md:gap-8">
<div className="flex flex-col max-w-sm gap-3">
</div>
<div className="flex flex-col gap-5 h-full items-center w-full justify-center">
<div className="flex flex-col w-full overflow-y-auto px-4 xs:items-center">
<div className="flex mobile:flex-col md:gap-8 mobile:gap-4 mobile:pb-4">
<div className="flex flex-col xs:max-w-sm gap-3">
<Typography variant="main-title">
{l10n.getString('onboarding-assign_trackers-title')}
</Typography>
@@ -287,7 +289,7 @@ export function TrackersAssignPage() {
</div>
</div>
)}
<div className="flex flex-row mt-auto">
<div className="flex flex-row">
{!state.alonePage && (
<>
<Button
@@ -310,8 +312,9 @@ export function TrackersAssignPage() {
)}
</div>
</div>
<div className="flex flex-col flex-grow gap-3 rounded-xl fill-background-50">
<div className="flex flex-col rounded-xl fill-background-50">
<BodyAssignment
width={150}
onlyAssigned={false}
highlightedRoles={firstError?.affectedRoles || []}
rolesWithErrors={rolesWithErrors}
@@ -321,10 +324,6 @@ export function TrackersAssignPage() {
</div>
</div>
</div>
<div className="w-full pb-4 flex flex-row">
<div className="flex flex-grow gap-3"></div>
<div className="flex gap-3"></div>
</div>
</div>
<SkipSetupWarningModal
accept={skipSetup}

View File

@@ -49,7 +49,7 @@ export function TrackerSelectionMenu({
>
<div className="flex w-full h-full flex-col ">
<div className="flex w-full flex-col flex-grow items-center gap-3">
<Typography variant="main-title" bold>
<Typography variant="mobile-title" bold>
{l10n.getString('tracker_selection_menu-' + BodyPart[bodyPart])}
</Typography>
<div className="w-full max-w-sm">
@@ -57,7 +57,7 @@ export function TrackerSelectionMenu({
</div>
<div className="relative">
<div
className="w-full h-full min-w-[700px] overflow-y-auto p-2 pt-0 flex flex-col gap-6"
className="w-full h-full xs:min-w-[700px] overflow-y-auto p-2 pt-0 flex flex-col gap-6"
ref={refTrackers}
style={{ height: trackersHeight - optionsHeight }}
>
@@ -67,7 +67,7 @@ export function TrackerSelectionMenu({
<Typography>
{l10n.getString('tracker_selection_menu-unassigned')}
</Typography>
<div className="grid grid-cols-2 gap-3">
<div className="grid xs:grid-cols-2 mobile:grid-cols-1 gap-3">
{unassignedTrackers.map((fd, index) => (
<TrackerCard
key={index}
@@ -88,7 +88,7 @@ export function TrackerSelectionMenu({
<Typography>
{l10n.getString('tracker_selection_menu-assigned')}
</Typography>
<div className=" grid grid-cols-2 gap-3">
<div className=" grid xs:grid-cols-2 mobile:grid-cols-1 gap-3">
{assignedTrackers.map((fd, index) => (
<TrackerCard
key={index}
@@ -106,9 +106,6 @@ export function TrackerSelectionMenu({
</div>
</div>
</div>
<div className="absolute px-2 pr-4 bottom-0 h-10 w-full border-b-[1px] border-background-40">
<div className="w-full h-full bg-gradient-to-b from-transparent to-black opacity-50"></div>
</div>
</div>
</div>
</div>

View File

@@ -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 (
<div className="fixed top-12 z-50 px-4 w-full">
<Dropdown
control={control}
display="block"
items={links.map(({ label, value: { url: value } }) => ({
label,
value,
}))}
variant="tertiary"
direction="down"
// There is always an option selected placholder is not used
placeholder=""
name="link"
></Dropdown>
</div>
);
}
export function SettingsLayoutRoute({ children }: { children: ReactNode }) {
const { layoutHeight, ref } = useLayout<HTMLDivElement>();
const { height, ref: navRef } = useElemSize<HTMLDivElement>();
const { isMobile } = useBreakpoint('mobile');
return (
<>
<TopBar></TopBar>
<div ref={ref} className="flex-grow" style={{ height: layoutHeight }}>
<div className="flex h-full pb-3">
<Navbar></Navbar>
<div className="flex h-full xs:pb-3">
{!isMobile && <Navbar></Navbar>}
<div className="h-full w-full gap-2 flex">
<SettingsSidebar></SettingsSidebar>
<div className="w-full flex flex-col overflow-y-auto pr-1 mr-1">
{children}
{!isMobile && <SettingsSidebar></SettingsSidebar>}
<div className="w-full flex flex-col">
{isMobile && <SettingSelectorMobile></SettingSelectorMobile>}
<div
className="flex flex-col overflow-y-auto xs:pr-1 xs:mr-1 mobile:pt-7 pb-3"
style={{ minHeight: layoutHeight - height }}
>
{children}
</div>
<div ref={navRef}>{isMobile && <Navbar></Navbar>}</div>
</div>
</div>
</div>

View File

@@ -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<HTMLDivElement>) {
const pageRef = useRef<HTMLDivElement | null>(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 (
<div ref={pageRef} className={className} {...props}>
{children}
</div>
);
}
export function SettingsPagePaneLayout({
children,
className,
icon,
@@ -13,13 +42,13 @@ export function SettingsPageLayout({
return (
<div
className={classNames(
'bg-background-70 rounded-lg p-8 flex gap-8 w-full',
'mobile:scroll-mt-7 bg-background-70 rounded-lg px-4 py-8 flex xs:gap-4 w-full relative',
className
)}
{...props}
>
<div className="flex">
<div className="w-10 h-10 bg-accent-background-40 flex justify-center items-center rounded-full fill-background-10">
<div className="flex mobile:absolute mobile:right-4">
<div className=" w-10 h-10 bg-accent-background-40 flex justify-center items-center rounded-full fill-background-10">
{icon}
</div>
</div>

View File

@@ -74,13 +74,9 @@ export function SettingsSidebar() {
<SettingsLink to="/settings/osc/router" scrollTo="router">
{l10n.getString('settings-sidebar-osc_router')}
</SettingsLink>
</div>
<div className="flex flex-col gap-2">
<SettingsLink to="/settings/osc/vrchat" scrollTo="vrchat">
{l10n.getString('settings-sidebar-osc_trackers')}
</SettingsLink>
</div>
<div className="flex flex-col gap-2">
<SettingsLink to="/settings/osc/vmc" scrollTo="vmc">
VMC
</SettingsLink>

File diff suppressed because it is too large Load Diff

View File

@@ -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<HTMLFormElement | null>(null);
const { reset, control, watch, handleSubmit } =
useForm<OSCRouterSettingsForm>({
@@ -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 (
<form className="flex flex-col gap-2 w-full" ref={pageRef}>
<SettingsPageLayout icon={<RouterIcon></RouterIcon>} id="router">
<>
<Typography variant="main-title">
{l10n.getString('settings-osc-router')}
</Typography>
<div className="flex flex-col pt-2 pb-4">
<>
{l10n
.getString('settings-osc-router-description')
.split('\n')
.map((line, i) => (
<Typography color="secondary" key={i}>
{line}
</Typography>
))}
</>
</div>
<Typography bold>
{l10n.getString('settings-osc-router-enable')}
</Typography>
<div className="flex flex-col pb-2">
<Typography color="secondary">
{l10n.getString('settings-osc-router-enable-description')}
<SettingsPageLayout>
<form className="flex flex-col gap-2 w-full">
<SettingsPagePaneLayout icon={<RouterIcon></RouterIcon>} id="router">
<>
<Typography variant="main-title">
{l10n.getString('settings-osc-router')}
</Typography>
</div>
<div className="grid grid-cols-2 gap-3 pb-5">
<CheckBox
variant="toggle"
outlined
control={control}
name="router.oscSettings.enabled"
label={l10n.getString('settings-osc-router-enable-label')}
/>
</div>
<Typography bold>
{l10n.getString('settings-osc-router-network')}
</Typography>
<div className="flex flex-col pb-2">
<>
{l10n
.getString('settings-osc-router-network-description')
.split('\n')
.map((line, i) => (
<Typography color="secondary" key={i}>
{line}
</Typography>
))}
</>
</div>
<div className="grid grid-cols-2 gap-3 pb-5">
<Localized
id="settings-osc-router-network-port_in"
attrs={{ placeholder: true, label: true }}
>
<Input
type="number"
<div className="flex flex-col pt-2 pb-4">
<>
{l10n
.getString('settings-osc-router-description')
.split('\n')
.map((line, i) => (
<Typography color="secondary" key={i}>
{line}
</Typography>
))}
</>
</div>
<Typography bold>
{l10n.getString('settings-osc-router-enable')}
</Typography>
<div className="flex flex-col pb-2">
<Typography color="secondary">
{l10n.getString('settings-osc-router-enable-description')}
</Typography>
</div>
<div className="grid grid-cols-2 gap-3 pb-5">
<CheckBox
variant="toggle"
outlined
control={control}
rules={{ required: true }}
name="router.oscSettings.portIn"
placeholder="9002"
name="router.oscSettings.enabled"
label={l10n.getString('settings-osc-router-enable-label')}
/>
</div>
<Typography bold>
{l10n.getString('settings-osc-router-network')}
</Typography>
<div className="flex flex-col pb-2">
<>
{l10n
.getString('settings-osc-router-network-description')
.split('\n')
.map((line, i) => (
<Typography color="secondary" key={i}>
{line}
</Typography>
))}
</>
</div>
<div className="grid grid-cols-2 gap-3 pb-5">
<Localized
id="settings-osc-router-network-port_in"
attrs={{ placeholder: true, label: true }}
>
<Input
type="number"
control={control}
rules={{ required: true }}
name="router.oscSettings.portIn"
placeholder="9002"
label=""
></Input>
</Localized>
<Localized
id="settings-osc-router-network-port_out"
attrs={{ placeholder: true, label: true }}
>
<Input
type="number"
control={control}
rules={{ required: true }}
name="router.oscSettings.portOut"
placeholder="9000"
label=""
></Input>
</Localized>
</div>
<Typography bold>
{l10n.getString('settings-osc-router-network-address')}
</Typography>
<div className="flex flex-col pb-2">
<Typography color="secondary">
{l10n.getString(
'settings-osc-router-network-address-description'
)}
</Typography>
</div>
<div className="grid gap-3 pb-5">
<Input
type="text"
control={control}
rules={{
required: true,
pattern:
/^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|$)){4}$/i,
}}
name="router.oscSettings.address"
placeholder={l10n.getString(
'settings-osc-router-network-address-placeholder'
)}
label=""
></Input>
</Localized>
<Localized
id="settings-osc-router-network-port_out"
attrs={{ placeholder: true, label: true }}
>
<Input
type="number"
control={control}
rules={{ required: true }}
name="router.oscSettings.portOut"
placeholder="9000"
label=""
></Input>
</Localized>
</div>
<Typography bold>
{l10n.getString('settings-osc-router-network-address')}
</Typography>
<div className="flex flex-col pb-2">
<Typography color="secondary">
{l10n.getString(
'settings-osc-router-network-address-description'
)}
</Typography>
</div>
<div className="grid gap-3 pb-5">
<Input
type="text"
control={control}
rules={{
required: true,
pattern:
/^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|$)){4}$/i,
}}
name="router.oscSettings.address"
placeholder={l10n.getString(
'settings-osc-router-network-address-placeholder'
)}
label=""
></Input>
</div>
</>
</SettingsPageLayout>
</form>
</div>
</>
</SettingsPagePaneLayout>
</form>
</SettingsPageLayout>
);
}

View File

@@ -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<HTMLDivElement>();
const { isMobile } = useBreakpoint('mobile');
const { l10n } = useLocalization();
const { state } = useLocation();
const toolbarRef = useRef<HTMLDivElement>(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() {
</div>
<div className="" ref={toolbarRef}>
<div className="border-t-2 pt-2 border-background-60 border-solid m-2 gap-2 flex flex-row">
<div className="flex flex-grow gap-2">
<div className="flex flex-grow gap-2 mobile:grid mobile:grid-cols-2 mobile:grid-rows-2">
<Button variant="quaternary" onClick={reboot}>
{l10n.getString('settings-serial-reboot')}
</Button>
@@ -230,19 +231,34 @@ export function Serial() {
<Button variant="quaternary" onClick={getInfos}>
{l10n.getString('settings-serial-get_infos')}
</Button>
{isMobile && (
<Dropdown
control={control}
name="port"
display="block"
placeholder={l10n.getString(
'settings-serial-serial_select'
)}
items={serialDevices.map((device) => ({
label: device.name?.toString() || 'error',
value: device.port?.toString() || 'error',
}))}
></Dropdown>
)}
</div>
<div className="flex justify-end">
{!isMobile && (
<Dropdown
control={control}
name="port"
display="fit"
placeholder={l10n.getString('settings-serial-serial_select')}
items={serialDevices.map((device) => ({
label: device.name?.toString() || 'error',
value: device.port?.toString() || 'error',
}))}
></Dropdown>
</div>
)}
</div>
</div>
</div>

View File

@@ -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<HTMLFormElement | null>(null);
const [modelName, setModelName] = useState<string | null>(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 (
<form className="flex flex-col gap-2 w-full" ref={pageRef}>
<SettingsPageLayout icon={<VMCIcon></VMCIcon>} id="vmc">
<>
<Typography variant="main-title">
{l10n.getString('settings-osc-vmc')}
</Typography>
<div className="flex flex-col pt-2 pb-4">
<>
{l10n
.getString('settings-osc-vmc-description')
.split('\n')
.map((line, i) => (
<Typography color="secondary" key={i}>
{line}
</Typography>
))}
</>
</div>
<Typography bold>
{l10n.getString('settings-osc-vmc-enable')}
</Typography>
<div className="flex flex-col pb-2">
<Typography color="secondary">
{l10n.getString('settings-osc-vmc-enable-description')}
<SettingsPageLayout>
<form className="flex flex-col gap-2 w-full">
<SettingsPagePaneLayout icon={<VMCIcon></VMCIcon>} id="vmc">
<>
<Typography variant="main-title">
{l10n.getString('settings-osc-vmc')}
</Typography>
</div>
<div className="grid grid-cols-2 gap-3 pb-5">
<CheckBox
variant="toggle"
outlined
control={control}
name="vmc.oscSettings.enabled"
label={l10n.getString('settings-osc-vmc-enable-label')}
/>
</div>
<Typography bold>
{l10n.getString('settings-osc-vmc-network')}
</Typography>
<div className="flex flex-col pb-2">
<>
{l10n
.getString('settings-osc-vmc-network-description')
.split('\n')
.map((line, i) => (
<Typography color="secondary" key={i}>
{line}
</Typography>
))}
</>
</div>
<div className="grid grid-cols-2 gap-3 pb-5">
<Localized
id="settings-osc-vmc-network-port_in"
attrs={{ placeholder: true, label: true }}
>
<Input
type="number"
<div className="flex flex-col pt-2 pb-4">
<>
{l10n
.getString('settings-osc-vmc-description')
.split('\n')
.map((line, i) => (
<Typography color="secondary" key={i}>
{line}
</Typography>
))}
</>
</div>
<Typography bold>
{l10n.getString('settings-osc-vmc-enable')}
</Typography>
<div className="flex flex-col pb-2">
<Typography color="secondary">
{l10n.getString('settings-osc-vmc-enable-description')}
</Typography>
</div>
<div className="grid grid-cols-2 gap-3 pb-5">
<CheckBox
variant="toggle"
outlined
control={control}
name="vmc.oscSettings.portIn"
rules={{ required: true }}
placeholder="9002"
name="vmc.oscSettings.enabled"
label={l10n.getString('settings-osc-vmc-enable-label')}
/>
</div>
<Typography bold>
{l10n.getString('settings-osc-vmc-network')}
</Typography>
<div className="flex flex-col pb-2">
<>
{l10n
.getString('settings-osc-vmc-network-description')
.split('\n')
.map((line, i) => (
<Typography color="secondary" key={i}>
{line}
</Typography>
))}
</>
</div>
<div className="grid grid-cols-2 gap-3 pb-5">
<Localized
id="settings-osc-vmc-network-port_in"
attrs={{ placeholder: true, label: true }}
>
<Input
type="number"
control={control}
name="vmc.oscSettings.portIn"
rules={{ required: true }}
placeholder="9002"
label=""
></Input>
</Localized>
<Localized
id="settings-osc-vmc-network-port_out"
attrs={{ placeholder: true, label: true }}
>
<Input
type="number"
control={control}
name="vmc.oscSettings.portOut"
rules={{ required: true }}
placeholder="9000"
label=""
></Input>
</Localized>
</div>
<Typography bold>
{l10n.getString('settings-osc-vmc-network-address')}
</Typography>
<div className="flex flex-col pb-2">
<Typography color="secondary">
{l10n.getString('settings-osc-vmc-network-address-description')}
</Typography>
</div>
<div className="grid gap-3 pb-5">
<Input
type="text"
control={control}
name="vmc.oscSettings.address"
rules={{
required: true,
pattern:
/^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|$)){4}$/i,
}}
placeholder={l10n.getString(
'settings-osc-vmc-network-address-placeholder'
)}
label=""
></Input>
</Localized>
<Localized
id="settings-osc-vmc-network-port_out"
attrs={{ placeholder: true, label: true }}
>
<Input
type="number"
</div>
<Typography bold>
{l10n.getString('settings-osc-vmc-vrm')}
</Typography>
<div className="flex flex-col pb-2">
<Typography color="secondary">
{l10n.getString('settings-osc-vmc-vrm-description')}
</Typography>
</div>
<div className="flex flex-col pb-2">
<Typography color={flashLoaded ? 'primary' : 'secondary'}>
{modelName === null
? l10n.getString('settings-osc-vmc-vrm-model_unloaded')
: l10n.getString('settings-osc-vmc-vrm-model_loaded', {
name: modelName,
titled: (!!modelName).toString(),
})}
</Typography>
</div>
<div className="grid gap-3 pb-5">
<FileInput
control={control}
name="vmc.oscSettings.portOut"
rules={{ required: true }}
placeholder="9000"
label=""
></Input>
</Localized>
</div>
<Typography bold>
{l10n.getString('settings-osc-vmc-network-address')}
</Typography>
<div className="flex flex-col pb-2">
<Typography color="secondary">
{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"
></FileInput>
{/* For some reason, linux (GNOME) is detecting the VRM file is a VRML */}
</div>
<Typography bold>
{l10n.getString('settings-osc-vmc-anchor_hip')}
</Typography>
</div>
<div className="grid gap-3 pb-5">
<Input
type="text"
control={control}
name="vmc.oscSettings.address"
rules={{
required: true,
pattern:
/^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|$)){4}$/i,
}}
placeholder={l10n.getString(
'settings-osc-vmc-network-address-placeholder'
)}
label=""
></Input>
</div>
<Typography bold>{l10n.getString('settings-osc-vmc-vrm')}</Typography>
<div className="flex flex-col pb-2">
<Typography color="secondary">
{l10n.getString('settings-osc-vmc-vrm-description')}
</Typography>
</div>
<div className="flex flex-col pb-2">
<Typography color={flashLoaded ? 'primary' : 'secondary'}>
{modelName === null
? l10n.getString('settings-osc-vmc-vrm-model_unloaded')
: l10n.getString('settings-osc-vmc-vrm-model_loaded', {
name: modelName,
titled: (!!modelName).toString(),
})}
</Typography>
</div>
<div className="grid gap-3 pb-5">
<FileInput
control={control}
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"
></FileInput>
{/* For some reason, linux (GNOME) is detecting the VRM file is a VRML */}
</div>
<Typography bold>
{l10n.getString('settings-osc-vmc-anchor_hip')}
</Typography>
<div className="flex flex-col pb-2">
<Typography color="secondary">
{l10n.getString('settings-osc-vmc-anchor_hip-description')}
</Typography>
</div>
<div className="grid grid-cols-2 gap-3 pb-5">
<CheckBox
variant="toggle"
outlined
control={control}
name="vmc.anchorHip"
label={l10n.getString('settings-osc-vmc-anchor_hip-label')}
/>
</div>
</>
</SettingsPageLayout>
</form>
<div className="flex flex-col pb-2">
<Typography color="secondary">
{l10n.getString('settings-osc-vmc-anchor_hip-description')}
</Typography>
</div>
<div className="grid grid-cols-2 gap-3 pb-5">
<CheckBox
variant="toggle"
outlined
control={control}
name="vmc.anchorHip"
label={l10n.getString('settings-osc-vmc-anchor_hip-label')}
/>
</div>
</>
</SettingsPagePaneLayout>
</form>
</SettingsPageLayout>
);
}

View File

@@ -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<HTMLFormElement | null>(null);
const { reset, control, watch, handleSubmit } = useForm<VRCOSCSettingsForm>({
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 (
<form className="flex flex-col gap-2 w-full" ref={pageRef}>
<SettingsPageLayout icon={<VRCIcon></VRCIcon>} id="vrchat">
<>
<Typography variant="main-title">
{l10n.getString('settings-osc-vrchat')}
</Typography>
<div className="flex flex-col pt-2 pb-4">
<>
{l10n
.getString('settings-osc-vrchat-description')
.split('\n')
.map((line, i) => (
<Typography color="secondary" key={i}>
{line}
</Typography>
))}
</>
</div>
<Typography bold>
{l10n.getString('settings-osc-vrchat-enable')}
</Typography>
<div className="flex flex-col pb-2">
<Typography color="secondary">
{l10n.getString('settings-osc-vrchat-enable-description')}
<SettingsPageLayout>
<form className="flex flex-col gap-2 w-full">
<SettingsPagePaneLayout icon={<VRCIcon></VRCIcon>} id="vrchat">
<>
<Typography variant="main-title">
{l10n.getString('settings-osc-vrchat')}
</Typography>
</div>
<div className="grid grid-cols-2 gap-3 pb-5">
<CheckBox
variant="toggle"
outlined
control={control}
name="vrchat.oscSettings.enabled"
label={l10n.getString('settings-osc-vrchat-enable-label')}
/>
</div>
<Typography bold>
{l10n.getString('settings-osc-vrchat-network')}
</Typography>
<div className="flex flex-col pb-2">
<Typography color="secondary">
{l10n.getString('settings-osc-vrchat-network-description')}
<div className="flex flex-col pt-2 pb-4">
<>
{l10n
.getString('settings-osc-vrchat-description')
.split('\n')
.map((line, i) => (
<Typography color="secondary" key={i}>
{line}
</Typography>
))}
</>
</div>
<Typography bold>
{l10n.getString('settings-osc-vrchat-enable')}
</Typography>
</div>
<div className="grid grid-cols-2 gap-3 pb-5">
<Localized
id="settings-osc-vrchat-network-port_in"
attrs={{ placeholder: true, label: true }}
>
<Input
type="number"
<div className="flex flex-col pb-2">
<Typography color="secondary">
{l10n.getString('settings-osc-vrchat-enable-description')}
</Typography>
</div>
<div className="grid grid-cols-2 gap-3 pb-5">
<CheckBox
variant="toggle"
outlined
control={control}
name="vrchat.oscSettings.portIn"
rules={{ required: true }}
placeholder="9001"
name="vrchat.oscSettings.enabled"
label={l10n.getString('settings-osc-vrchat-enable-label')}
/>
</div>
<Typography bold>
{l10n.getString('settings-osc-vrchat-network')}
</Typography>
<div className="flex flex-col pb-2">
<Typography color="secondary">
{l10n.getString('settings-osc-vrchat-network-description')}
</Typography>
</div>
<div className="grid grid-cols-2 gap-3 pb-5">
<Localized
id="settings-osc-vrchat-network-port_in"
attrs={{ placeholder: true, label: true }}
>
<Input
type="number"
control={control}
name="vrchat.oscSettings.portIn"
rules={{ required: true }}
placeholder="9001"
label=""
></Input>
</Localized>
<Localized
id="settings-osc-vrchat-network-port_out"
attrs={{ placeholder: true, label: true }}
>
<Input
type="number"
control={control}
name="vrchat.oscSettings.portOut"
rules={{ required: true }}
placeholder="9000"
label=""
></Input>
</Localized>
</div>
<Typography bold>
{l10n.getString('settings-osc-vrchat-network-address')}
</Typography>
<div className="flex flex-col pb-2">
<Typography color="secondary">
{l10n.getString(
'settings-osc-vrchat-network-address-description'
)}
</Typography>
</div>
<div className="grid gap-3 pb-5">
<Input
type="text"
control={control}
name="vrchat.oscSettings.address"
rules={{
required: true,
pattern:
/^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|$)){4}$/i,
}}
placeholder={l10n.getString(
'settings-osc-vrchat-network-address-placeholder'
)}
label=""
></Input>
</Localized>
<Localized
id="settings-osc-vrchat-network-port_out"
attrs={{ placeholder: true, label: true }}
>
<Input
type="number"
</div>
<Typography bold>
{l10n.getString('settings-osc-vrchat-network-trackers')}
</Typography>
<div className="flex flex-col pb-2">
<Typography color="secondary">
{l10n.getString(
'settings-osc-vrchat-network-trackers-description'
)}
</Typography>
</div>
<div className="grid grid-cols-2 gap-3 pb-5">
<CheckBox
variant="toggle"
outlined
control={control}
name="vrchat.oscSettings.portOut"
rules={{ required: true }}
placeholder="9000"
label=""
></Input>
</Localized>
</div>
<Typography bold>
{l10n.getString('settings-osc-vrchat-network-address')}
</Typography>
<div className="flex flex-col pb-2">
<Typography color="secondary">
{l10n.getString(
'settings-osc-vrchat-network-address-description'
)}
</Typography>
</div>
<div className="grid gap-3 pb-5">
<Input
type="text"
control={control}
name="vrchat.oscSettings.address"
rules={{
required: true,
pattern:
/^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|$)){4}$/i,
}}
placeholder={l10n.getString(
'settings-osc-vrchat-network-address-placeholder'
)}
label=""
></Input>
</div>
<Typography bold>
{l10n.getString('settings-osc-vrchat-network-trackers')}
</Typography>
<div className="flex flex-col pb-2">
<Typography color="secondary">
{l10n.getString(
'settings-osc-vrchat-network-trackers-description'
)}
</Typography>
</div>
<div className="grid grid-cols-2 gap-3 pb-5">
<CheckBox
variant="toggle"
outlined
control={control}
name="vrchat.trackers.chest"
label={l10n.getString(
'settings-osc-vrchat-network-trackers-chest'
)}
/>
<CheckBox
variant="toggle"
outlined
control={control}
name="vrchat.trackers.waist"
label={l10n.getString('settings-osc-vrchat-network-trackers-hip')}
/>
<CheckBox
variant="toggle"
outlined
control={control}
name="vrchat.trackers.knees"
label={l10n.getString(
'settings-osc-vrchat-network-trackers-knees'
)}
/>
<CheckBox
variant="toggle"
outlined
control={control}
name="vrchat.trackers.feet"
label={l10n.getString(
'settings-osc-vrchat-network-trackers-feet'
)}
/>
<CheckBox
variant="toggle"
outlined
control={control}
name="vrchat.trackers.elbows"
label={l10n.getString(
'settings-osc-vrchat-network-trackers-elbows'
)}
/>
</div>
</>
</SettingsPageLayout>
</form>
name="vrchat.trackers.chest"
label={l10n.getString(
'settings-osc-vrchat-network-trackers-chest'
)}
/>
<CheckBox
variant="toggle"
outlined
control={control}
name="vrchat.trackers.waist"
label={l10n.getString(
'settings-osc-vrchat-network-trackers-hip'
)}
/>
<CheckBox
variant="toggle"
outlined
control={control}
name="vrchat.trackers.knees"
label={l10n.getString(
'settings-osc-vrchat-network-trackers-knees'
)}
/>
<CheckBox
variant="toggle"
outlined
control={control}
name="vrchat.trackers.feet"
label={l10n.getString(
'settings-osc-vrchat-network-trackers-feet'
)}
/>
<CheckBox
variant="toggle"
outlined
control={control}
name="vrchat.trackers.elbows"
label={l10n.getString(
'settings-osc-vrchat-network-trackers-elbows'
)}
/>
</div>
</>
</SettingsPagePaneLayout>
</form>
</SettingsPageLayout>
);
}

View File

@@ -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'
)}
>
<div className="flex w-full h-full flex-col gap-10 px-3">
<div className="flex flex-col w-full h-full justify-center items-center">
<div className="flex gap-8">
<div className="flex flex-col max-w-sm gap-3">
<Typography variant="main-title" bold>
{l10n.getString('body_assignment_menu')}
</Typography>
<Typography color="secondary">
{l10n.getString('body_assignment_menu-description')}
</Typography>
<CheckBox
control={control}
label={l10n.getString(
'body_assignment_menu-show_advanced_locations'
)}
name="advanced"
variant="toggle"
></CheckBox>
<div className="flex">
<Button
variant="secondary"
to="/onboarding/trackers-assign"
state={{ alonePage: true }}
>
{l10n.getString('body_assignment_menu-manage_trackers')}
</Button>
</div>
<div className="flex xs:flex-row h-full xs:gap-8 mobile:flex-col xs:justify-center items-center">
<div className="flex flex-col xs:max-w-sm gap-3">
<Typography variant="mobile-title" bold>
{l10n.getString('body_assignment_menu')}
</Typography>
<Typography color="secondary">
{l10n.getString('body_assignment_menu-description')}
</Typography>
<CheckBox
control={control}
label={l10n.getString(
'body_assignment_menu-show_advanced_locations'
)}
name="advanced"
variant="toggle"
></CheckBox>
<div className="flex">
<Button
variant="secondary"
to="/onboarding/trackers-assign"
state={{ alonePage: true }}
>
{l10n.getString('body_assignment_menu-manage_trackers')}
</Button>
</div>
<div className="flex flex-col flex-grow gap-3 rounded-xl fill-background-50">
<BodyAssignment
onlyAssigned={false}
advanced={advanced}
onRoleSelected={tryOpenChokerWarning}
></BodyAssignment>
<div className="flex justify-center">
<Button
variant="secondary"
onClick={() => onRoleSelected(BodyPart.NONE)}
>
{l10n.getString('body_assignment_menu-unassign_tracker')}
</Button>
</div>
</div>
<div className="flex flex-col xs:flex-grow gap-3 rounded-xl fill-background-50 py-2">
<BodyAssignment
width={isMobile ? 160 : undefined}
onlyAssigned={false}
advanced={advanced}
onRoleSelected={tryOpenChokerWarning}
></BodyAssignment>
<div className="flex justify-center">
<Button
variant="secondary"
onClick={() => onRoleSelected(BodyPart.NONE)}
>
{l10n.getString('body_assignment_menu-unassign_tracker')}
</Button>
</div>
</div>
</div>

View File

@@ -72,7 +72,7 @@ function TrackerSmol({
const trackerName = useName();
return (
<div className="flex rounded-md py-3 px-5 w-full gap-4 h-16">
<div className="flex rounded-md py-3 px-4 w-full gap-4 h-16">
<div className="flex flex-col justify-center items-center fill-background-10">
<BodyPartIcon bodyPart={tracker.info?.bodyPart}></BodyPartIcon>
</div>

View File

@@ -78,7 +78,7 @@ export function TrackerPartCard({
(showCard && (
<div
className={classNames(
'flex flex-col gap-1 control w-32 hover:bg-background-50 px-2 py-1 rounded-md relative',
'flex flex-col gap-1 control xs:w-32 hover:bg-background-50 px-2 py-1 rounded-md relative',
direction === 'left' ? 'items-start' : 'items-end'
)}
id={BodyPart[role]}

View File

@@ -165,7 +165,7 @@ export function TrackerSettingsPage() {
onClose={() => setSelectRotation(false)}
onDirectionSelected={onDirectionSelected}
></MountingSelectionMenu>
<div className="flex gap-2 md:h-full max-md:flex-wrap md:flex-row ">
<div className="flex gap-2 md:h-full max-md:flex-wrap md:flex-row xs:flex-col mobile:flex-col">
<div className="flex flex-col w-full md:max-w-xs gap-2">
{tracker && (
<TrackerCard

View File

@@ -0,0 +1,9 @@
import { WidgetsComponent } from '../WidgetsComponent';
export function VRModePage() {
return (
<div className="p-2 flex flex-col gap-2 h-full">
<WidgetsComponent></WidgetsComponent>
</div>
);
}

View File

@@ -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<K extends BreakpointKey>(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<K>}`;
return {
[`is${capitalizedKey}`]: bool,
} as Record<Key, boolean>;
}
export function useIsTauri() {
return !!window.__TAURI_METADATA__;
}

View File

@@ -1,4 +1,4 @@
import { MutableRefObject, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { MutableRefObject, useLayoutEffect, useRef, useState } from 'react';
export function useLayout<T extends HTMLElement>() {
const [layoutHeight, setLayoutHeigt] = useState(window.innerHeight);
@@ -47,7 +47,7 @@ export function useElemSize<T extends HTMLElement>(
})
);
useEffect(() => {
useLayoutEffect(() => {
if (ref.current) {
observer.current.observe(ref.current);
}

View File

@@ -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');

View File

@@ -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';

View File

@@ -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), <alpha-value>)',
@@ -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;

View File

@@ -2,7 +2,7 @@
"compilerOptions": {
"target": "es2020",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": false,
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,

76
package-lock.json generated
View File

@@ -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",