4:3 UI switchover

This commit is contained in:
legop3
2026-01-19 03:00:47 -05:00
parent 5e7581c82d
commit 4d699e1c4c
20 changed files with 274 additions and 181 deletions
+3 -1
View File
@@ -1,6 +1,6 @@
#configuration for roverd #configuration for roverd
name: dummy2 name: dummy2
serverUrl: ws://192.168.0.84:8080/rover serverUrl: ws://127.0.0.1:8080/rover
serial: serial:
device: /dev/ttyS0 device: /dev/ttyS0
baud: 115200 baud: 115200
@@ -18,3 +18,5 @@ media:
service: mediamtx.service service: mediamtx.service
healthUrl: http://127.0.0.1:9997/v3/paths/list healthUrl: http://127.0.0.1:9997/v3/paths/list
healthInterval: 30s healthInterval: 30s
nightVision:
enabled: false
+22
View File
@@ -0,0 +1,22 @@
#configuration for roverd
name: dummy3
serverUrl: ws://127.0.0.1:8080/rover
serial:
device: /dev/ttyS0
baud: 115200
brc:
gpioPin: 25
pulseEvery: 1m
pulseWidth: 1s
battery:
full: 2068
warn: 1700
urgent: 1650
maxWheelSpeed: 350
media:
manage: false
service: mediamtx.service
healthUrl: http://127.0.0.1:9997/v3/paths/list
healthInterval: 30s
nightVision:
enabled: false
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -11,8 +11,8 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" /> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" /> <meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
<title>Multi Roomba Rover</title> <title>Multi Roomba Rover</title>
<script type="module" crossorigin src="/assets/index-DXkdiJPx.js"></script> <script type="module" crossorigin src="/assets/index-BS8UNUHv.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BvvbAZLx.css"> <link rel="stylesheet" crossorigin href="/assets/index-CxMMWICN.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+2 -9
View File
@@ -61,16 +61,9 @@ function useLayoutMode() {
function DesktopLayout({ layout, onOpenHelpOverlay }) { function DesktopLayout({ layout, onOpenHelpOverlay }) {
return ( return (
<div className="flex h-full gap-0.5 overflow-hidden"> <div className="flex h-full gap-0.5 overflow-hidden">
<div className="flex min-w-0 flex-[1.8] flex-col gap-0.5 overflow-y-auto pr-0.5"> <div className="flex min-w-0 flex-[1.22] flex-col gap-0.5 overflow-y-auto pr-0.5">
<DriverVideoPanel /> <DriverVideoPanel />
<div className="grid h-52 grid-cols-2 gap-0.5"> <TelemetryPanel />
<div className="h-full min-h-0">
<UserListPanel fillHeight />
</div>
<div className="h-full min-h-0">
<ChatPanel fillHeight />
</div>
</div>
<LogPanel /> <LogPanel />
</div> </div>
<div className="flex min-w-0 flex-1 flex-col gap-0.5 overflow-y-auto"> <div className="flex min-w-0 flex-1 flex-col gap-0.5 overflow-y-auto">
+2 -11
View File
@@ -5,7 +5,6 @@ import { useControlSystem } from '../controls/index.js';
import { formatKeyLabel } from '../controls/keymapUtils.js'; import { formatKeyLabel } from '../controls/keymapUtils.js';
import TopDownMap from './TopDownMap.jsx'; import TopDownMap from './TopDownMap.jsx';
import RoverRoster from './RoverRoster.jsx'; import RoverRoster from './RoverRoster.jsx';
import ReplaySourcesPanel from './ReplaySourcesPanel.jsx';
import DriveDockAction, { useDriveDockState } from './DriveDockAction.jsx'; import DriveDockAction, { useDriveDockState } from './DriveDockAction.jsx';
export function RoverRosterPanel({ title = 'Rovers' }) { export function RoverRosterPanel({ title = 'Rovers' }) {
@@ -46,7 +45,7 @@ export function RoverRosterPanel({ title = 'Rovers' }) {
); );
} }
export default function ControlSummary({ showRoster = true }) { export default function ControlSummary() {
const { const {
state: { roverId, keymap }, state: { roverId, keymap },
} = useControlSystem(); } = useControlSystem();
@@ -57,7 +56,7 @@ export default function ControlSummary({ showRoster = true }) {
return ( return (
<section className="panel-section"> <section className="panel-section">
<div className="grid items-stretch gap-1 md:grid-cols-[minmax(0,2fr)_minmax(0,1fr)] md:min-h-[22rem]"> <div className="grid items-stretch gap-1 md:grid-cols-[minmax(0,1.1fr)_minmax(0,1fr)] md:min-h-[18rem]">
<div className="flex h-full w-full items-stretch justify-center"> <div className="flex h-full w-full items-stretch justify-center">
<div className="aspect-square h-full w-full"> <div className="aspect-square h-full w-full">
<TopDownMap sensors={sensors} /> <TopDownMap sensors={sensors} />
@@ -70,14 +69,6 @@ export default function ControlSummary({ showRoster = true }) {
{!hideInlineControls ? <InlineCameraTilt keymap={keymap} /> : null} {!hideInlineControls ? <InlineCameraTilt keymap={keymap} /> : null}
</div> </div>
</div> </div>
{showRoster ? (
<div className="grid gap-0.5 md:grid-cols-[minmax(0,1fr)_minmax(0,1.4fr)]">
<div>
<ReplaySourcesPanel />
</div>
<RoverRosterPanel />
</div>
) : null}
</section> </section>
); );
} }
+2 -2
View File
@@ -1,7 +1,7 @@
import { useSession } from "../context/SessionContext"; import { useSession } from "../context/SessionContext";
import { FaDiscord } from "react-icons/fa"; import { FaDiscord } from "react-icons/fa";
export default function DiscordInviteButton({text = "Join our Discord!"}) { export default function DiscordInviteButton({ text = "Join our Discord!", className = "" }) {
const { session } = useSession(); const { session } = useSession();
const discordInvite = session?.discord?.invite || null; const discordInvite = session?.discord?.invite || null;
@@ -12,7 +12,7 @@ export default function DiscordInviteButton({text = "Join our Discord!"}) {
href={discordInvite} href={discordInvite}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="inline-flex items-center w-full px-0.5 py-0.5 text-sm font-medium text-white rainbow-animate-bg transition justify-center" className={`inline-flex items-center w-full px-0.5 py-0.5 text-sm font-medium text-white rainbow-animate-bg transition justify-center ${className}`}
// animated rainbow backgound // animated rainbow backgound
// className="inline-flex items-center px-3 py-2 bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 text-white rounded hover:from-indigo-600 hover:via-purple-600 hover:to-pink-600 transition" // className="inline-flex items-center px-3 py-2 bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 text-white rounded hover:from-indigo-600 hover:via-purple-600 hover:to-pink-600 transition"
> >
+1 -1
View File
@@ -131,7 +131,7 @@ export default function DriverVideoPanel({layoutFormat = 'desktop'}) {
idleSkipSeconds={idleSkipSeconds} idleSkipSeconds={idleSkipSeconds}
/> />
) : ( ) : (
<div className="panel-muted content-center text-center text-sm text-slate-400 aspect-video"> <div className="panel-muted content-center text-center text-sm text-slate-400 aspect-[4/3]">
<p>You are not assigned to a rover.</p> <p>You are not assigned to a rover.</p>
{/* colored button to visit the spectator page */} {/* colored button to visit the spectator page */}
<p className="mt-2"> <p className="mt-2">
@@ -34,7 +34,7 @@ function EntityRow({ entity, connected, onToggle }) {
type="button" type="button"
onClick={() => onToggle(entity.id)} onClick={() => onToggle(entity.id)}
disabled={disableToggle} disabled={disableToggle}
className={`flex min-w-[12rem] flex-1 items-center justify-between gap-1 rounded px-1 py-0.5 text-left transition-colors ${toneStyles} disabled:opacity-60 disabled:hover:bg-inherit`} className={`flex min-w-[10rem] flex-1 items-center justify-between gap-1 rounded px-1 py-0.5 text-left transition-colors ${toneStyles} disabled:opacity-60 disabled:hover:bg-inherit`}
> >
<div className="min-w-0"> <div className="min-w-0">
<div className="flex items-center gap-1 text-sm"> <div className="flex items-center gap-1 text-sm">
@@ -83,11 +83,11 @@ export default function HomeAssistantControls() {
return ( return (
<section className="panel-section space-y-0.5 text-base"> <section className="panel-section space-y-0.5 text-base">
<header className="flex items-center justify-between gap-0.5 text-sm text-slate-400"> <header className="flex flex-wrap items-start justify-between gap-0.5 text-sm text-slate-400">
<div className="flex items-center gap-1"> <div className="flex flex-wrap items-center gap-1">
<p>Light Controls</p> <p>Light Controls</p>
<span className="text-xs text-slate-500">{entities.length}</span> <span className="text-xs text-slate-500">{entities.length}</span>
<div className="flex items-center gap-1 text-xs text-slate-300 background-black"> <div className="flex w-full flex-wrap items-center gap-1 text-xs text-slate-300">
<span className="flex items-center gap-0.5"> <span className="flex items-center gap-0.5">
<span>On</span> <span>On</span>
{onKeyLabel ? <KeyPill label={onKeyLabel} /> : null} {onKeyLabel ? <KeyPill label={onKeyLabel} /> : null}
+2 -2
View File
@@ -1,7 +1,7 @@
import { useSession } from "../context/SessionContext"; import { useSession } from "../context/SessionContext";
import { FaCoffee } from "react-icons/fa"; import { FaCoffee } from "react-icons/fa";
export default function KoFiButton({ text = "Support me on Ko-fi!" }) { export default function KoFiButton({ text = "Support me on Ko-fi!", className = "" }) {
const { session } = useSession(); const { session } = useSession();
const kofiLink = session?.kofi?.link || null; const kofiLink = session?.kofi?.link || null;
@@ -12,7 +12,7 @@ export default function KoFiButton({ text = "Support me on Ko-fi!" }) {
href={kofiLink} href={kofiLink}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="inline-flex items-center w-full px-0.5 py-0.5 text-sm font-medium text-white kofi-animate-bg transition justify-center" className={`inline-flex items-center w-full px-0.5 py-0.5 text-sm font-medium text-white kofi-animate-bg transition justify-center ${className}`}
> >
<FaCoffee className="mr-1" /> <FaCoffee className="mr-1" />
{text} {text}
+7 -3
View File
@@ -31,16 +31,20 @@ export default function NicknameForm({ compact = false }) {
} }
return ( return (
<form className="flex w-full gap-0.5" onSubmit={handleSave}> <form className="grid w-full min-w-0 grid-cols-[minmax(0,1fr)_auto] gap-0.5" onSubmit={handleSave}>
<input <input
className="field-input flex-1" className="field-input flex-1 min-w-0"
value={nicknameInput} value={nicknameInput}
onChange={(e) => setNicknameInput(e.target.value)} onChange={(e) => setNicknameInput(e.target.value)}
maxLength={32} maxLength={32}
placeholder="Enter a nickname" placeholder="Enter a nickname"
disabled={!canSetNickname} disabled={!canSetNickname}
/> />
<button type="submit" disabled={!canSetNickname || saving} className="button-dark disabled:opacity-50"> <button
type="submit"
disabled={!canSetNickname || saving}
className="button-dark shrink-0 whitespace-nowrap disabled:opacity-50"
>
{saving ? 'Saving…' : compact ? 'Set' : 'Save'} {saving ? 'Saving…' : compact ? 'Set' : 'Save'}
</button> </button>
</form> </form>
+6 -3
View File
@@ -16,7 +16,7 @@ function normalizeSources(list = []) {
.filter(Boolean); .filter(Boolean);
} }
export default function ReplaySourcesPanel({ panelId = 'replay-sources' }) { export default function ReplaySourcesPanel({ panelId = 'replay-sources', fillHeight = false }) {
const { session, triggerReplay } = useSession(); const { session, triggerReplay } = useSession();
const sources = normalizeSources(session?.replaySources || []); const sources = normalizeSources(session?.replaySources || []);
const { value: settings, save: saveSettings } = useSettingsNamespace('replaySources', {}); const { value: settings, save: saveSettings } = useSettingsNamespace('replaySources', {});
@@ -100,13 +100,16 @@ export default function ReplaySourcesPanel({ panelId = 'replay-sources' }) {
} }
}; };
const containerClass = fillHeight ? 'h-full flex flex-col' : '';
const listWrapClass = fillHeight ? 'flex-1 min-h-0 overflow-y-auto' : '';
return ( return (
<section className="panel-section p-0.75 text-sm"> <section className={`panel-section p-0.75 text-sm ${containerClass}`}>
<header className="panel-muted flex items-center justify-between text-xs"> <header className="panel-muted flex items-center justify-between text-xs">
<span>Replay Sources</span> <span>Replay Sources</span>
<span>{sources.length}</span> <span>{sources.length}</span>
</header> </header>
<div className="grid gap-0.5 md:grid-cols-2"> <div className={`grid gap-0.5 md:grid-cols-2 ${listWrapClass}`}>
<GroupList title="Rovers" items={grouped.rovers} selected={selected} onToggle={toggleKey} /> <GroupList title="Rovers" items={grouped.rovers} selected={selected} onToggle={toggleKey} />
<GroupList title="Room Cams" items={grouped.rooms} selected={selected} onToggle={toggleKey} /> <GroupList title="Room Cams" items={grouped.rooms} selected={selected} onToggle={toggleKey} />
</div> </div>
+50 -4
View File
@@ -1,10 +1,45 @@
import TelemetryPanel from './TelemetryPanel.jsx'; import { RoverRosterPanel } from './ControlSummary.jsx';
import ControlSummary from './ControlSummary.jsx';
import RoomCameraPanel from './RoomCameraPanel.jsx'; import RoomCameraPanel from './RoomCameraPanel.jsx';
import HomeAssistantControls from './HomeAssistantControls.jsx'; import HomeAssistantControls from './HomeAssistantControls.jsx';
import SettingsPanel from './SettingsPanel.jsx'; import SettingsPanel from './SettingsPanel.jsx';
import HelpPanel from './HelpPanel.jsx'; import HelpPanel from './HelpPanel.jsx';
import ChatPanel from './ChatPanel.jsx';
import UserListPanel, { NicknameLinksPanel } from './UserListPanel.jsx';
import ReplaySourcesPanel from './ReplaySourcesPanel.jsx';
import Tabs, { Tab, TabList, TabPanel, TabPanels } from './Tabs.jsx'; import Tabs, { Tab, TabList, TabPanel, TabPanels } from './Tabs.jsx';
import TopDownMap from './TopDownMap.jsx';
import DriveDockAction, { useDriveDockState } from './DriveDockAction.jsx';
import { useTelemetryFrame } from '../context/TelemetryContext.jsx';
import { useControlSystem } from '../controls/index.js';
function TopDownMapPanel() {
const {
state: { roverId },
} = useControlSystem();
const frame = useTelemetryFrame(roverId);
const sensors = frame?.sensors || {};
return (
<section className="panel-section">
<div className="aspect-square w-full">
<TopDownMap sensors={sensors} />
</div>
</section>
);
}
function DriveDockPanel() {
const {
state: { roverId },
} = useControlSystem();
const driveDockState = useDriveDockState(roverId);
return (
<section className="panel-section flex h-full flex-col">
<DriveDockAction layout="desktop" expand driveDockState={driveDockState} />
</section>
);
}
export default function RightPaneTabs({ layout, onOpenHelpOverlay }) { export default function RightPaneTabs({ layout, onOpenHelpOverlay }) {
return ( return (
@@ -18,10 +53,21 @@ export default function RightPaneTabs({ layout, onOpenHelpOverlay }) {
<TabPanels> <TabPanels>
<TabPanel id="telemetry"> <TabPanel id="telemetry">
<div className="space-y-0.5"> <div className="space-y-0.5">
<ControlSummary /> <div className="grid items-stretch gap-0.5 grid-cols-[minmax(0,1.35fr)_minmax(0,0.95fr)]">
<TopDownMapPanel />
<DriveDockPanel />
</div>
<div className="grid gap-0.5 grid-cols-[minmax(0,0.6fr)_minmax(0,1.1fr)_minmax(0,0.9fr)]">
<NicknameLinksPanel compact />
<RoverRosterPanel />
<ReplaySourcesPanel panelId="replay-sources-desktop" fillHeight />
</div>
<div className="grid items-stretch gap-0.5 grid-cols-[minmax(0,1.3fr)_minmax(0,0.7fr)] h-[14rem]">
<ChatPanel fillHeight />
<UserListPanel compact hideNicknameForm fillHeight />
</div>
<HomeAssistantControls /> <HomeAssistantControls />
<RoomCameraPanel defaultOrientation="horizontal" panelId="rightpane-telemetry" /> <RoomCameraPanel defaultOrientation="horizontal" panelId="rightpane-telemetry" />
<TelemetryPanel />
</div> </div>
</TabPanel> </TabPanel>
<TabPanel id="help"> <TabPanel id="help">
+4 -2
View File
@@ -5,9 +5,11 @@ export default function SessionSnapshot() {
const { session } = useSession(); const { session } = useSession();
const payload = useMemo(() => JSON.stringify(session ?? {}, null, 2), [session]); const payload = useMemo(() => JSON.stringify(session ?? {}, null, 2), [session]);
return ( return (
<div className="panel-section space-y-0.5 text-xs"> <div className="panel-section flex min-h-0 flex-col gap-0.5 text-xs">
<p className="text-sm text-slate-400">Session snapshot</p> <p className="text-sm text-slate-400">Session snapshot</p>
<pre className="surface h-64 overflow-y-auto font-mono text-[0.7rem] text-lime-300">{payload}</pre> <pre className="surface min-h-0 flex-1 overflow-y-auto font-mono text-[0.7rem] text-lime-300">
{payload}
</pre>
</div> </div>
); );
} }
+43 -13
View File
@@ -6,6 +6,20 @@ import NicknameForm from './NicknameForm.jsx';
import DiscordInviteButton from './DiscordInviteButton.jsx'; import DiscordInviteButton from './DiscordInviteButton.jsx';
import KoFiButton from './KoFiButton.jsx'; import KoFiButton from './KoFiButton.jsx';
export function NicknameLinksPanel({ compact = false }) {
return (
<section className="panel-section flex h-full min-h-0 flex-col gap-0.5 text-base">
<div className="surface flex w-full items-center">
<NicknameForm compact={compact} />
</div>
<div className="grid flex-1 min-h-0 gap-0.5 grid-rows-2">
<DiscordInviteButton className="h-full" />
<KoFiButton className="h-full" />
</div>
</section>
);
}
function roleColors(role) { function roleColors(role) {
switch (role) { switch (role) {
case 'admin': case 'admin':
@@ -28,7 +42,13 @@ function formatLabel(user, selfId) {
return base; return base;
} }
export default function UserListPanel({ hideNicknameForm = false, hideHeader = false, className = '', fillHeight = false }) { export default function UserListPanel({
hideNicknameForm = false,
hideHeader = false,
className = '',
fillHeight = false,
compact = false,
}) {
const { session, setNickname } = useSession(); const { session, setNickname } = useSession();
const { value } = useSettingsNamespace('profile', { nickname: '' }); const { value } = useSettingsNamespace('profile', { nickname: '' });
const lastSyncedSocketRef = useRef(null); const lastSyncedSocketRef = useRef(null);
@@ -93,9 +113,19 @@ export default function UserListPanel({ hideNicknameForm = false, hideHeader = f
return Math.ceil(ms / 1000); return Math.ceil(ms / 1000);
}, []); }, []);
const baseListClass = fillHeight ? 'flex-1 min-h-0 overflow-y-auto' : 'h-48 overflow-y-auto'; const baseListClass = fillHeight
const turnsListClass = isTurnsMode && fillHeight ? 'max-h-40 overflow-y-auto' : baseListClass; ? 'flex-1 min-h-0 overflow-y-auto'
const usersListClass = isTurnsMode && fillHeight ? 'flex-1 min-h-0 overflow-y-auto' : baseListClass; : compact
? 'h-28 overflow-y-auto'
: 'h-48 overflow-y-auto';
const turnsListClass =
isTurnsMode && fillHeight
? 'max-h-40 overflow-y-auto'
: isTurnsMode && compact
? 'max-h-32 overflow-y-auto'
: baseListClass;
const usersListClass =
isTurnsMode && fillHeight ? 'flex-1 min-h-0 overflow-y-auto' : baseListClass;
const renderUserList = () => const renderUserList = () =>
sorted.length === 0 ? ( sorted.length === 0 ? (
@@ -107,7 +137,7 @@ export default function UserListPanel({ hideNicknameForm = false, hideHeader = f
return ( return (
<div <div
key={user.socketId} key={user.socketId}
className="surface-muted flex items-center gap-1 text-sm" className={`surface-muted flex items-center gap-1 ${compact ? 'py-0.25 text-[0.8rem]' : 'text-sm'}`}
> >
<p className={`font-semibold ${roleColors(user.role)}`}>{formatLabel(user, selfId)}</p> <p className={`font-semibold ${roleColors(user.role)}`}>{formatLabel(user, selfId)}</p>
{user.roverId ? ( {user.roverId ? (
@@ -131,13 +161,13 @@ export default function UserListPanel({ hideNicknameForm = false, hideHeader = f
> >
{!hideNicknameForm && ( {!hideNicknameForm && (
<div className="space-y-0.5"> <div className="space-y-0.5">
<div className="flex items-stretch gap-0.5"> <div className="grid gap-0.5 md:grid-cols-[minmax(0,1.4fr)_minmax(0,1fr)]">
<div className="flex w-1/2 min-w-0"> <div className="min-w-0">
<div className="surface flex w-full items-center"> <div className="surface flex w-full items-center">
<NicknameForm /> <NicknameForm compact={compact} />
</div> </div>
</div> </div>
<div className="flex w-1/2 flex-col gap-0.5"> <div className="grid gap-0.5 sm:grid-cols-2 md:grid-cols-1">
<DiscordInviteButton /> <DiscordInviteButton />
<KoFiButton /> <KoFiButton />
</div> </div>
@@ -148,14 +178,14 @@ export default function UserListPanel({ hideNicknameForm = false, hideHeader = f
<div className={`space-y-0.5 ${fillHeight ? 'flex flex-1 min-h-0 flex-col' : ''}`}> <div className={`space-y-0.5 ${fillHeight ? 'flex flex-1 min-h-0 flex-col' : ''}`}>
{!hideHeader && ( {!hideHeader && (
<div className="flex items-center justify-between text-sm text-slate-400"> <div className={`flex items-center justify-between text-sm text-slate-400 ${compact ? 'text-xs' : ''}`}>
<span>{isTurnsMode ? 'Turn queues' : 'Users'}</span> <span>{isTurnsMode ? 'Turn queues' : 'Users'}</span>
<span className="text-xs text-slate-500"> <span className="text-xs text-slate-500">
{isTurnsMode ? Object.keys(turnQueues || {}).length : sorted.length} {isTurnsMode ? Object.keys(turnQueues || {}).length : sorted.length}
</span> </span>
</div> </div>
)} )}
<div className={`surface space-y-0.25 ${isTurnsMode ? turnsListClass : baseListClass}`}> <div className={`surface space-y-0.25 ${isTurnsMode ? turnsListClass : baseListClass} ${compact ? 'text-[0.8rem]' : ''}`}>
{isTurnsMode ? ( {isTurnsMode ? (
Object.keys(turnQueues || {}).length === 0 ? ( Object.keys(turnQueues || {}).length === 0 ? (
<p className="text-sm text-slate-500">No turn queues yet.</p> <p className="text-sm text-slate-500">No turn queues yet.</p>
@@ -173,7 +203,7 @@ export default function UserListPanel({ hideNicknameForm = false, hideHeader = f
: queue[0] : queue[0]
: null; : null;
return ( return (
<div key={roverId} className="surface-muted flex flex-col gap-0.25 text-sm"> <div key={roverId} className={`surface-muted flex flex-col gap-0.25 ${compact ? 'text-[0.8rem] py-0.25' : 'text-sm'}`}>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<p className="font-semibold text-slate-200">{rosterName(roverId)}</p> <p className="font-semibold text-slate-200">{rosterName(roverId)}</p>
{remaining != null && ( {remaining != null && (
@@ -201,7 +231,7 @@ export default function UserListPanel({ hideNicknameForm = false, hideHeader = f
return ( return (
<span <span
key={`${roverId}-${socketId}-${idx}`} key={`${roverId}-${socketId}-${idx}`}
className={`flex items-center gap-0.5 rounded px-1 text-[0.8rem] ${highlightClass}`} className={`flex items-center gap-0.5 rounded px-1 ${compact ? 'text-[0.7rem]' : 'text-[0.8rem]'} ${highlightClass}`}
> >
<span className={`${roleColors(user.role)} font-semibold`}> <span className={`${roleColors(user.role)} font-semibold`}>
{formatLabel(user, selfId)} {formatLabel(user, selfId)}
+1 -1
View File
@@ -368,7 +368,7 @@ export default function VideoTile({
return ( return (
<div className={`flex flex-col gap-0.5 ${fitParent ? 'h-full' : ''}`}> <div className={`flex flex-col gap-0.5 ${fitParent ? 'h-full' : ''}`}>
<div <div
className={`relative w-full overflow-hidden bg-black ${fitParent ? 'h-full flex-1' : 'aspect-video'}`} className={`relative w-full overflow-hidden bg-black ${fitParent ? 'h-full flex-1' : 'aspect-[4/3]'}`}
> >
{usingSnapshot ? ( {usingSnapshot ? (
snapshotFeed?.objectUrl ? ( snapshotFeed?.objectUrl ? (
+3 -3
View File
@@ -180,11 +180,11 @@ function FitViewportFrame({ children }) {
<div <div
className="relative flex items-center justify-center overflow-hidden bg-black" className="relative flex items-center justify-center overflow-hidden bg-black"
style={{ style={{
width: 'min(100%, calc(100vh * 16 / 9))', width: 'min(100%, calc(100vh * 4 / 3))',
height: 'min(100%, calc(100vw * 9 / 16))', height: 'min(100%, calc(100vw * 3 / 4))',
maxWidth: '100%', maxWidth: '100%',
maxHeight: '100%', maxHeight: '100%',
aspectRatio: '16 / 9', aspectRatio: '4 / 3',
}} }}
> >
<div className="flex h-full w-full items-center justify-center overflow-hidden">{children}</div> <div className="flex h-full w-full items-center justify-center overflow-hidden">{children}</div>