mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
4:3 UI switchover
This commit is contained in:
Vendored
+3
-1
@@ -1,6 +1,6 @@
|
||||
#configuration for roverd
|
||||
name: dummy2
|
||||
serverUrl: ws://192.168.0.84:8080/rover
|
||||
serverUrl: ws://127.0.0.1:8080/rover
|
||||
serial:
|
||||
device: /dev/ttyS0
|
||||
baud: 115200
|
||||
@@ -18,3 +18,5 @@ media:
|
||||
service: mediamtx.service
|
||||
healthUrl: http://127.0.0.1:9997/v3/paths/list
|
||||
healthInterval: 30s
|
||||
nightVision:
|
||||
enabled: false
|
||||
|
||||
Vendored
+22
@@ -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
@@ -11,8 +11,8 @@
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
|
||||
<title>Multi Roomba Rover</title>
|
||||
<script type="module" crossorigin src="/assets/index-DXkdiJPx.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BvvbAZLx.css">
|
||||
<script type="module" crossorigin src="/assets/index-BS8UNUHv.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CxMMWICN.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
+2
-9
@@ -61,16 +61,9 @@ function useLayoutMode() {
|
||||
function DesktopLayout({ layout, onOpenHelpOverlay }) {
|
||||
return (
|
||||
<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 />
|
||||
<div className="grid h-52 grid-cols-2 gap-0.5">
|
||||
<div className="h-full min-h-0">
|
||||
<UserListPanel fillHeight />
|
||||
</div>
|
||||
<div className="h-full min-h-0">
|
||||
<ChatPanel fillHeight />
|
||||
</div>
|
||||
</div>
|
||||
<TelemetryPanel />
|
||||
<LogPanel />
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-0.5 overflow-y-auto">
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useControlSystem } from '../controls/index.js';
|
||||
import { formatKeyLabel } from '../controls/keymapUtils.js';
|
||||
import TopDownMap from './TopDownMap.jsx';
|
||||
import RoverRoster from './RoverRoster.jsx';
|
||||
import ReplaySourcesPanel from './ReplaySourcesPanel.jsx';
|
||||
import DriveDockAction, { useDriveDockState } from './DriveDockAction.jsx';
|
||||
|
||||
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 {
|
||||
state: { roverId, keymap },
|
||||
} = useControlSystem();
|
||||
@@ -57,7 +56,7 @@ export default function ControlSummary({ showRoster = true }) {
|
||||
|
||||
return (
|
||||
<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="aspect-square h-full w-full">
|
||||
<TopDownMap sensors={sensors} />
|
||||
@@ -70,14 +69,6 @@ export default function ControlSummary({ showRoster = true }) {
|
||||
{!hideInlineControls ? <InlineCameraTilt keymap={keymap} /> : null}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useSession } from "../context/SessionContext";
|
||||
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 discordInvite = session?.discord?.invite || null;
|
||||
|
||||
@@ -12,7 +12,7 @@ export default function DiscordInviteButton({text = "Join our Discord!"}) {
|
||||
href={discordInvite}
|
||||
target="_blank"
|
||||
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
|
||||
// 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"
|
||||
>
|
||||
|
||||
@@ -131,7 +131,7 @@ export default function DriverVideoPanel({layoutFormat = 'desktop'}) {
|
||||
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>
|
||||
{/* colored button to visit the spectator page */}
|
||||
<p className="mt-2">
|
||||
|
||||
@@ -34,7 +34,7 @@ function EntityRow({ entity, connected, onToggle }) {
|
||||
type="button"
|
||||
onClick={() => onToggle(entity.id)}
|
||||
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="flex items-center gap-1 text-sm">
|
||||
@@ -83,11 +83,11 @@ export default function HomeAssistantControls() {
|
||||
|
||||
return (
|
||||
<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">
|
||||
<div className="flex items-center gap-1">
|
||||
<header className="flex flex-wrap items-start justify-between gap-0.5 text-sm text-slate-400">
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
<p>Light Controls</p>
|
||||
<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>On</span>
|
||||
{onKeyLabel ? <KeyPill label={onKeyLabel} /> : null}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useSession } from "../context/SessionContext";
|
||||
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 kofiLink = session?.kofi?.link || null;
|
||||
|
||||
@@ -12,7 +12,7 @@ export default function KoFiButton({ text = "Support me on Ko-fi!" }) {
|
||||
href={kofiLink}
|
||||
target="_blank"
|
||||
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" />
|
||||
{text}
|
||||
|
||||
@@ -31,16 +31,20 @@ export default function NicknameForm({ compact = false }) {
|
||||
}
|
||||
|
||||
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
|
||||
className="field-input flex-1"
|
||||
className="field-input flex-1 min-w-0"
|
||||
value={nicknameInput}
|
||||
onChange={(e) => setNicknameInput(e.target.value)}
|
||||
maxLength={32}
|
||||
placeholder="Enter a nickname"
|
||||
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'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -16,7 +16,7 @@ function normalizeSources(list = []) {
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
export default function ReplaySourcesPanel({ panelId = 'replay-sources' }) {
|
||||
export default function ReplaySourcesPanel({ panelId = 'replay-sources', fillHeight = false }) {
|
||||
const { session, triggerReplay } = useSession();
|
||||
const sources = normalizeSources(session?.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 (
|
||||
<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">
|
||||
<span>Replay Sources</span>
|
||||
<span>{sources.length}</span>
|
||||
</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="Room Cams" items={grouped.rooms} selected={selected} onToggle={toggleKey} />
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,45 @@
|
||||
import TelemetryPanel from './TelemetryPanel.jsx';
|
||||
import ControlSummary from './ControlSummary.jsx';
|
||||
import { RoverRosterPanel } from './ControlSummary.jsx';
|
||||
import RoomCameraPanel from './RoomCameraPanel.jsx';
|
||||
import HomeAssistantControls from './HomeAssistantControls.jsx';
|
||||
import SettingsPanel from './SettingsPanel.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 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 }) {
|
||||
return (
|
||||
@@ -18,10 +53,21 @@ export default function RightPaneTabs({ layout, onOpenHelpOverlay }) {
|
||||
<TabPanels>
|
||||
<TabPanel id="telemetry">
|
||||
<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 />
|
||||
<RoomCameraPanel defaultOrientation="horizontal" panelId="rightpane-telemetry" />
|
||||
<TelemetryPanel />
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel id="help">
|
||||
|
||||
@@ -5,9 +5,11 @@ export default function SessionSnapshot() {
|
||||
const { session } = useSession();
|
||||
const payload = useMemo(() => JSON.stringify(session ?? {}, null, 2), [session]);
|
||||
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>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,20 @@ import NicknameForm from './NicknameForm.jsx';
|
||||
import DiscordInviteButton from './DiscordInviteButton.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) {
|
||||
switch (role) {
|
||||
case 'admin':
|
||||
@@ -28,7 +42,13 @@ function formatLabel(user, selfId) {
|
||||
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 { value } = useSettingsNamespace('profile', { nickname: '' });
|
||||
const lastSyncedSocketRef = useRef(null);
|
||||
@@ -93,9 +113,19 @@ export default function UserListPanel({ hideNicknameForm = false, hideHeader = f
|
||||
return Math.ceil(ms / 1000);
|
||||
}, []);
|
||||
|
||||
const baseListClass = fillHeight ? 'flex-1 min-h-0 overflow-y-auto' : 'h-48 overflow-y-auto';
|
||||
const turnsListClass = isTurnsMode && fillHeight ? 'max-h-40 overflow-y-auto' : baseListClass;
|
||||
const usersListClass = isTurnsMode && fillHeight ? 'flex-1 min-h-0 overflow-y-auto' : baseListClass;
|
||||
const baseListClass = fillHeight
|
||||
? 'flex-1 min-h-0 overflow-y-auto'
|
||||
: 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 = () =>
|
||||
sorted.length === 0 ? (
|
||||
@@ -107,7 +137,7 @@ export default function UserListPanel({ hideNicknameForm = false, hideHeader = f
|
||||
return (
|
||||
<div
|
||||
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>
|
||||
{user.roverId ? (
|
||||
@@ -131,13 +161,13 @@ export default function UserListPanel({ hideNicknameForm = false, hideHeader = f
|
||||
>
|
||||
{!hideNicknameForm && (
|
||||
<div className="space-y-0.5">
|
||||
<div className="flex items-stretch gap-0.5">
|
||||
<div className="flex w-1/2 min-w-0">
|
||||
<div className="grid gap-0.5 md:grid-cols-[minmax(0,1.4fr)_minmax(0,1fr)]">
|
||||
<div className="min-w-0">
|
||||
<div className="surface flex w-full items-center">
|
||||
<NicknameForm />
|
||||
<NicknameForm compact={compact} />
|
||||
</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 />
|
||||
<KoFiButton />
|
||||
</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' : ''}`}>
|
||||
{!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 className="text-xs text-slate-500">
|
||||
{isTurnsMode ? Object.keys(turnQueues || {}).length : sorted.length}
|
||||
</span>
|
||||
</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 ? (
|
||||
Object.keys(turnQueues || {}).length === 0 ? (
|
||||
<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]
|
||||
: null;
|
||||
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">
|
||||
<p className="font-semibold text-slate-200">{rosterName(roverId)}</p>
|
||||
{remaining != null && (
|
||||
@@ -201,7 +231,7 @@ export default function UserListPanel({ hideNicknameForm = false, hideHeader = f
|
||||
return (
|
||||
<span
|
||||
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`}>
|
||||
{formatLabel(user, selfId)}
|
||||
|
||||
@@ -368,7 +368,7 @@ export default function VideoTile({
|
||||
return (
|
||||
<div className={`flex flex-col gap-0.5 ${fitParent ? 'h-full' : ''}`}>
|
||||
<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 ? (
|
||||
snapshotFeed?.objectUrl ? (
|
||||
|
||||
@@ -180,11 +180,11 @@ function FitViewportFrame({ children }) {
|
||||
<div
|
||||
className="relative flex items-center justify-center overflow-hidden bg-black"
|
||||
style={{
|
||||
width: 'min(100%, calc(100vh * 16 / 9))',
|
||||
height: 'min(100%, calc(100vw * 9 / 16))',
|
||||
width: 'min(100%, calc(100vh * 4 / 3))',
|
||||
height: 'min(100%, calc(100vw * 3 / 4))',
|
||||
maxWidth: '100%',
|
||||
maxHeight: '100%',
|
||||
aspectRatio: '16 / 9',
|
||||
aspectRatio: '4 / 3',
|
||||
}}
|
||||
>
|
||||
<div className="flex h-full w-full items-center justify-center overflow-hidden">{children}</div>
|
||||
|
||||
Reference in New Issue
Block a user