mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 09:31:20 -04:00
pre cardcolor
This commit is contained in:
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-status-bar-style" content="black-translucent" />
|
||||||
<meta name="apple-mobile-web-app-title" content="Roomba Rover" />
|
<meta name="apple-mobile-web-app-title" content="Roomba Rover" />
|
||||||
<title>Roomba Rover</title>
|
<title>Roomba Rover</title>
|
||||||
<script type="module" crossorigin src="/assets/index-4eskKUF4.js"></script>
|
<script type="module" crossorigin src="/assets/index-BBFCmHB8.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-B8wCAHw2.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-Dva9v7lV.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useSocket } from '../../context/SocketContext.jsx';
|
|||||||
import { useSettingsNamespace } from '../../settings/index.js';
|
import { useSettingsNamespace } from '../../settings/index.js';
|
||||||
import { AUDIO_SETTINGS_DEFAULTS } from '../../settings/namespaces.js';
|
import { AUDIO_SETTINGS_DEFAULTS } from '../../settings/namespaces.js';
|
||||||
import ButtonBoxTile from '../ButtonBoxTile/index.jsx';
|
import ButtonBoxTile from '../ButtonBoxTile/index.jsx';
|
||||||
|
import CardFrame from '../CardFrame/index.jsx';
|
||||||
|
|
||||||
const FLASH_MS = 420;
|
const FLASH_MS = 420;
|
||||||
const REWARD_FLASH_MS = 1200;
|
const REWARD_FLASH_MS = 1200;
|
||||||
@@ -126,10 +127,7 @@ export default function ButtonBoxPanel() {
|
|||||||
}, [effectiveAlertVolume, socket]);
|
}, [effectiveAlertVolume, socket]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="panel-section space-y-0.5 text-base">
|
<CardFrame title="Button Box" bodyClassName="space-y-0.5 text-base">
|
||||||
<header className="panel-title-row">
|
|
||||||
<p className="panel-title-text">Button Box</p>
|
|
||||||
</header>
|
|
||||||
<div className="grid grid-cols-4 gap-0.5">
|
<div className="grid grid-cols-4 gap-0.5">
|
||||||
{buttons.map((button) => {
|
{buttons.map((button) => {
|
||||||
const id = Number(button.id);
|
const id = Number(button.id);
|
||||||
@@ -155,6 +153,6 @@ export default function ButtonBoxPanel() {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</CardFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
function cx(...values) {
|
||||||
|
return values.filter(Boolean).join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CardFrame({
|
||||||
|
title = '',
|
||||||
|
meta = null,
|
||||||
|
actions = null,
|
||||||
|
hideHeader = false,
|
||||||
|
className = '',
|
||||||
|
headerClassName = '',
|
||||||
|
bodyClassName = '',
|
||||||
|
fillHeight = false,
|
||||||
|
children,
|
||||||
|
}) {
|
||||||
|
const showHeader = !hideHeader && (title || meta != null || actions);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className={cx(
|
||||||
|
'panel-section overflow-hidden border border-neutral-600/55 bg-neutral-900/95 shadow-[0_1px_0_rgba(255,255,255,0.04)_inset,0_10px_24px_rgba(0,0,0,0.3)]',
|
||||||
|
fillHeight && 'flex h-full min-h-0 flex-col',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{showHeader ? (
|
||||||
|
<header
|
||||||
|
className={cx(
|
||||||
|
'flex items-center justify-between gap-0.5 border-b border-neutral-600/45 bg-gradient-to-r from-neutral-900 via-neutral-800 to-neutral-700 px-0.5 py-0.5',
|
||||||
|
headerClassName,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex min-w-0 items-center gap-0.5">
|
||||||
|
{title ? <p className="m-0 text-[0.78rem] font-semibold leading-none text-neutral-100">{title}</p> : null}
|
||||||
|
{meta != null ? <span className="text-[0.68rem] font-medium leading-none text-neutral-300">{meta}</span> : null}
|
||||||
|
</div>
|
||||||
|
{actions ? <div className="flex flex-wrap items-center justify-end gap-0.5">{actions}</div> : null}
|
||||||
|
</header>
|
||||||
|
) : null}
|
||||||
|
<div className={cx('', fillHeight && 'flex flex-1 min-h-0 flex-col', bodyClassName)}>{children}</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import { useSessionSelector } from '../../context/SessionContext.jsx';
|
|||||||
import { useSettingsNamespace } from '../../settings/index.js';
|
import { useSettingsNamespace } from '../../settings/index.js';
|
||||||
import ChatMessageRow from '../ChatMessageRow/index.jsx';
|
import ChatMessageRow from '../ChatMessageRow/index.jsx';
|
||||||
import ChatTypingRow from '../ChatTypingRow/index.jsx';
|
import ChatTypingRow from '../ChatTypingRow/index.jsx';
|
||||||
|
import CardFrame from '../CardFrame/index.jsx';
|
||||||
|
|
||||||
const FLITE_VOICES = ['kal', 'rms', 'slt', 'ksp', 'bdl'];
|
const FLITE_VOICES = ['kal', 'rms', 'slt', 'ksp', 'bdl'];
|
||||||
const ESPEAK_PITCHES = Array.from({ length: 10 }, (_, idx) => idx * 10);
|
const ESPEAK_PITCHES = Array.from({ length: 10 }, (_, idx) => idx * 10);
|
||||||
@@ -106,12 +107,12 @@ export default function ChatPanel({
|
|||||||
const listClass = fillHeight ? 'flex-1 min-h-0 overflow-y-auto' : 'h-48 overflow-y-auto';
|
const listClass = fillHeight ? 'flex-1 min-h-0 overflow-y-auto' : 'h-48 overflow-y-auto';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={`panel-section space-y-0.5 text-base ${fillHeight ? 'flex h-full flex-col overflow-hidden' : ''}`}>
|
<CardFrame
|
||||||
{title ? (
|
title={title}
|
||||||
<div className="panel-title-row">
|
hideHeader={!title}
|
||||||
<span className="panel-title-text">{title}</span>
|
fillHeight={fillHeight}
|
||||||
</div>
|
bodyClassName="space-y-0.5 text-base"
|
||||||
) : null}
|
>
|
||||||
<div className={`surface overflow-y-auto space-y-0.5 px-0 ${listClass}`} ref={listRef}>
|
<div className={`surface overflow-y-auto space-y-0.5 px-0 ${listClass}`} ref={listRef}>
|
||||||
{sorted.length === 0 && typingRows.length === 0 ? (
|
{sorted.length === 0 && typingRows.length === 0 ? (
|
||||||
<p className="text-sm text-slate-500">No messages yet.</p>
|
<p className="text-sm text-slate-500">No messages yet.</p>
|
||||||
@@ -218,6 +219,6 @@ export default function ChatPanel({
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
</section>
|
</CardFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useEffect, useMemo, useRef, useState } from 'react';
|
|||||||
import { useSessionActions, useSessionSelector } from '../../context/SessionContext.jsx';
|
import { useSessionActions, useSessionSelector } from '../../context/SessionContext.jsx';
|
||||||
import { useControlSystem } from '../../controls/index.js';
|
import { useControlSystem } from '../../controls/index.js';
|
||||||
import { formatKeyLabel } from '../../controls/keymapUtils.js';
|
import { formatKeyLabel } from '../../controls/keymapUtils.js';
|
||||||
|
import CardFrame from '../CardFrame/index.jsx';
|
||||||
|
|
||||||
function StatusBadge({ label, tone = 'muted' }) {
|
function StatusBadge({ label, tone = 'muted' }) {
|
||||||
const styles =
|
const styles =
|
||||||
@@ -255,46 +256,41 @@ export default function HomeAssistantControls() {
|
|||||||
|
|
||||||
if (!ha?.enabled) {
|
if (!ha?.enabled) {
|
||||||
return (
|
return (
|
||||||
<section className="panel-section space-y-0.5 text-sm text-slate-400">
|
<CardFrame title="Room Controls" bodyClassName="space-y-0.5 text-sm text-slate-400">
|
||||||
<p className="panel-title-text">Room Controls</p>
|
|
||||||
<p className="text-slate-500">Not configured on the server.</p>
|
<p className="text-slate-500">Not configured on the server.</p>
|
||||||
</section>
|
</CardFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entities.length === 0) {
|
if (entities.length === 0) {
|
||||||
return (
|
return (
|
||||||
<section className="panel-section space-y-0.5 text-sm text-slate-400">
|
<CardFrame title="Room Controls" bodyClassName="space-y-0.5 text-sm text-slate-400">
|
||||||
<p className="panel-title-text">Room Controls</p>
|
|
||||||
<p className="text-slate-500">No lights or switches configured.</p>
|
<p className="text-slate-500">No lights or switches configured.</p>
|
||||||
</section>
|
</CardFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const connected = Boolean(ha?.connected);
|
const connected = Boolean(ha?.connected);
|
||||||
|
|
||||||
|
const actions = (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center gap-0.5 text-[0.68rem] text-slate-400">
|
||||||
|
<span className="flex items-center gap-0.5">
|
||||||
|
<span>On</span>
|
||||||
|
{onKeyLabel ? <KeyPill label={onKeyLabel} /> : null}
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-0.5">
|
||||||
|
<span>Off</span>
|
||||||
|
{offKeyLabel ? <KeyPill label={offKeyLabel} /> : null}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{controlsLocked ? <StatusBadge label={lockState === 'off' ? 'Locked Off' : 'Locked On'} tone="warn" /> : null}
|
||||||
|
<StatusBadge label={connected ? 'Connected' : 'Offline'} tone={connected ? 'success' : 'warn'} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="panel-section space-y-0.5 text-base">
|
<CardFrame title="Room Controls" meta={entities.length} actions={actions} bodyClassName="space-y-0.5 text-base">
|
||||||
<header className="panel-title-row">
|
|
||||||
<div className="panel-title-left">
|
|
||||||
<p className="panel-title-text">Room Controls</p>
|
|
||||||
<span className="panel-title-meta">{entities.length}</span>
|
|
||||||
</div>
|
|
||||||
<div className="panel-title-right">
|
|
||||||
<div className="panel-title-actions">
|
|
||||||
<span className="flex items-center gap-0.5">
|
|
||||||
<span className="panel-title-meta">On</span>
|
|
||||||
{onKeyLabel ? <KeyPill label={onKeyLabel} /> : null}
|
|
||||||
</span>
|
|
||||||
<span className="flex items-center gap-0.5">
|
|
||||||
<span className="panel-title-meta">Off</span>
|
|
||||||
{offKeyLabel ? <KeyPill label={offKeyLabel} /> : null}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{controlsLocked ? <StatusBadge label={lockState === 'off' ? 'Locked Off' : 'Locked On'} tone="warn" /> : null}
|
|
||||||
<StatusBadge label={connected ? 'Connected' : 'Offline'} tone={connected ? 'success' : 'warn'} />
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
{controlsLocked ? (
|
{controlsLocked ? (
|
||||||
<p className="rounded border border-amber-600/60 bg-amber-900/40 px-1 py-0.5 text-xs text-amber-100">
|
<p className="rounded border border-amber-600/60 bg-amber-900/40 px-1 py-0.5 text-xs text-amber-100">
|
||||||
{lockState === 'off'
|
{lockState === 'off'
|
||||||
@@ -315,7 +311,7 @@ export default function HomeAssistantControls() {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</CardFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,30 +3,27 @@
|
|||||||
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
|
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
|
||||||
import { useSessionSelector } from '../../context/SessionContext.jsx';
|
import { useSessionSelector } from '../../context/SessionContext.jsx';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
import CardFrame from '../CardFrame/index.jsx';
|
||||||
|
|
||||||
export default function LogPanel() {
|
export default function LogPanel() {
|
||||||
const logs = useSessionSelector((state) => state.logs);
|
const logs = useSessionSelector((state) => state.logs);
|
||||||
const rendered = useMemo(() => logs.slice().reverse(), [logs]);
|
const rendered = useMemo(() => logs.slice().reverse(), [logs]);
|
||||||
return (
|
return (
|
||||||
<div className="panel-section space-y-0.5 text-base">
|
<CardFrame title="Server logs" meta={logs.length} bodyClassName="space-y-0.5 text-base">
|
||||||
<div className="panel-title-row">
|
|
||||||
<span className="panel-title-text">Server logs</span>
|
|
||||||
<span className="panel-title-meta">{logs.length}</span>
|
|
||||||
</div>
|
|
||||||
<div className="surface h-64 overflow-y-auto font-mono text-xs">
|
<div className="surface h-64 overflow-y-auto font-mono text-xs">
|
||||||
{logs.length === 0 ? (
|
{logs.length === 0 ? (
|
||||||
<p>No logs yet.</p>
|
<p>No logs yet.</p>
|
||||||
) : (
|
) : (
|
||||||
rendered.map((entry) => (
|
rendered.map((entry) => (
|
||||||
<div key={entry.id} className="surface">
|
<div key={entry.id} className="surface">
|
||||||
<span className="text-amber-400">{entry.timestamp}</span>{' '}
|
<span className="text-amber-400">{entry.timestamp}</span>{' '}
|
||||||
<span className="text-lime-400">[{entry.level}]</span>{' '}
|
<span className="text-lime-400">[{entry.level}]</span>{' '}
|
||||||
{entry.label && <span className="text-teal-400">[{entry.label}]</span>}{' '}
|
{entry.label && <span className="text-teal-400">[{entry.label}]</span>}{' '}
|
||||||
<span>{entry.message}</span>
|
<span>{entry.message}</span>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CardFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useSessionActions, useSessionSelector } from '../../context/SessionContext.jsx';
|
import { useSessionActions, useSessionSelector } from '../../context/SessionContext.jsx';
|
||||||
import { useSettingsNamespace } from '../../settings/index.js';
|
import { useSettingsNamespace } from '../../settings/index.js';
|
||||||
|
import CardFrame from '../CardFrame/index.jsx';
|
||||||
|
|
||||||
export default function OverseerPreferencePanel() {
|
export default function OverseerPreferencePanel() {
|
||||||
const { identifySession } = useSessionActions();
|
const { identifySession } = useSessionActions();
|
||||||
@@ -21,10 +22,7 @@ export default function OverseerPreferencePanel() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="surface p-1 text-xs text-slate-200 text-center">
|
<CardFrame title="Overseer vote" bodyClassName="space-y-0.5 text-center text-xs text-slate-200">
|
||||||
<div className="panel-title-row">
|
|
||||||
<span className="panel-title-text">Overseer vote</span>
|
|
||||||
</div>
|
|
||||||
<label
|
<label
|
||||||
className={`flex items-center justify-center gap-1.5 rounded px-1 py-0.5 ${
|
className={`flex items-center justify-center gap-1.5 rounded px-1 py-0.5 ${
|
||||||
enabled ? 'bg-emerald-600/80 text-emerald-50' : 'bg-slate-600/70 text-slate-100'
|
enabled ? 'bg-emerald-600/80 text-emerald-50' : 'bg-slate-600/70 text-slate-100'
|
||||||
@@ -47,6 +45,6 @@ export default function OverseerPreferencePanel() {
|
|||||||
{running ? 'Running!' : 'Stopped by vote.'}
|
{running ? 'Running!' : 'Stopped by vote.'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</CardFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useMemo } from 'react';
|
|||||||
import { useSessionSelector } from '../../context/SessionContext.jsx';
|
import { useSessionSelector } from '../../context/SessionContext.jsx';
|
||||||
import NicknameForm from '../NicknameForm/index.jsx';
|
import NicknameForm from '../NicknameForm/index.jsx';
|
||||||
import SocialButtonsGrid from '../SocialButtonsGrid/index.jsx';
|
import SocialButtonsGrid from '../SocialButtonsGrid/index.jsx';
|
||||||
|
import CardFrame from '../CardFrame/index.jsx';
|
||||||
|
|
||||||
function roleColors(role) {
|
function roleColors(role) {
|
||||||
switch (role) {
|
switch (role) {
|
||||||
@@ -57,8 +58,13 @@ export default function RawUserPilePanel({
|
|||||||
: 'h-48 overflow-y-auto';
|
: 'h-48 overflow-y-auto';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<CardFrame
|
||||||
className={`panel-section space-y-0.5 text-base ${fillHeight ? 'flex h-full min-h-0 flex-col overflow-hidden' : ''} ${className}`}
|
title={!hideHeader ? 'Users' : ''}
|
||||||
|
meta={!hideHeader ? sorted.length : null}
|
||||||
|
hideHeader={hideHeader}
|
||||||
|
fillHeight={fillHeight}
|
||||||
|
className={className}
|
||||||
|
bodyClassName="space-y-0.5 text-base"
|
||||||
>
|
>
|
||||||
{!hideNicknameForm && (
|
{!hideNicknameForm && (
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
@@ -75,12 +81,6 @@ export default function RawUserPilePanel({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<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 && (
|
|
||||||
<div className="panel-title-row">
|
|
||||||
<span className="panel-title-text">Users</span>
|
|
||||||
<span className="panel-title-meta">{sorted.length}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div
|
<div
|
||||||
className={`surface flex flex-wrap content-start items-start gap-0.5 px-0 pb-0 ${baseListClass} ${compact ? 'text-[0.8rem]' : ''}`}
|
className={`surface flex flex-wrap content-start items-start gap-0.5 px-0 pb-0 ${baseListClass} ${compact ? 'text-[0.8rem]' : ''}`}
|
||||||
>
|
>
|
||||||
@@ -98,6 +98,6 @@ export default function RawUserPilePanel({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</CardFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useEffect, useMemo, useState } from 'react';
|
|||||||
import { useSessionActions, useSessionSelector } from '../../context/SessionContext.jsx';
|
import { useSessionActions, useSessionSelector } from '../../context/SessionContext.jsx';
|
||||||
import { useSettingsNamespace } from '../../settings/index.js';
|
import { useSettingsNamespace } from '../../settings/index.js';
|
||||||
import { roverNameChromeStyle } from '../../lib/roverColor.js';
|
import { roverNameChromeStyle } from '../../lib/roverColor.js';
|
||||||
|
import CardFrame from '../CardFrame/index.jsx';
|
||||||
|
|
||||||
function normalizeSources(list = []) {
|
function normalizeSources(list = []) {
|
||||||
return list
|
return list
|
||||||
@@ -146,15 +147,10 @@ export default function ReplaySourcesPanel({ panelId = 'replay-sources', fillHei
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const containerClass = fillHeight ? 'h-full flex flex-col' : '';
|
|
||||||
const listWrapClass = fillHeight ? 'flex-1 min-h-0 overflow-y-auto' : '';
|
const listWrapClass = fillHeight ? 'flex-1 min-h-0 overflow-y-auto' : '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={`panel-section p-0.75 text-sm ${containerClass}`}>
|
<CardFrame title="Replay Sources" meta={sources.length} fillHeight={fillHeight} bodyClassName="space-y-0.5 text-sm">
|
||||||
<header className="panel-title-row">
|
|
||||||
<span className="panel-title-text">Replay Sources</span>
|
|
||||||
<span className="panel-title-meta">{sources.length}</span>
|
|
||||||
</header>
|
|
||||||
<div className={`grid gap-0.5 md:grid-cols-2 ${listWrapClass}`}>
|
<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} />
|
||||||
@@ -201,7 +197,7 @@ export default function ReplaySourcesPanel({ panelId = 'replay-sources', fillHei
|
|||||||
{error ? <div className="text-xs text-amber-400">{error}</div> : null}
|
{error ? <div className="text-xs text-amber-400">{error}</div> : null}
|
||||||
{success ? <div className="text-xs text-emerald-300">{success}</div> : null}
|
{success ? <div className="text-xs text-emerald-300">{success}</div> : null}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</CardFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { useSessionSelector } from '../../context/SessionContext.jsx';
|
|||||||
import { useSettingsNamespace } from '../../settings/index.js';
|
import { useSettingsNamespace } from '../../settings/index.js';
|
||||||
import ButtonBoxPanel from '../ButtonBoxPanel/index.jsx';
|
import ButtonBoxPanel from '../ButtonBoxPanel/index.jsx';
|
||||||
import OverseerPreferencePanel from '../OverseerPreferencePanel/index.jsx';
|
import OverseerPreferencePanel from '../OverseerPreferencePanel/index.jsx';
|
||||||
|
import CardFrame from '../CardFrame/index.jsx';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useManualDockAssist } from '../../features/manualDockAssist/useManualDockAssist.js';
|
import { useManualDockAssist } from '../../features/manualDockAssist/useManualDockAssist.js';
|
||||||
|
|
||||||
@@ -35,11 +36,11 @@ function TopDownMapPanel() {
|
|||||||
const sensors = frame?.sensors || {};
|
const sensors = frame?.sensors || {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="panel-section">
|
<CardFrame hideHeader>
|
||||||
<div className="aspect-square w-full">
|
<div className="aspect-square w-full">
|
||||||
<TopDownMap sensors={sensors} />
|
<TopDownMap sensors={sensors} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</CardFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useSessionSelector } from '../../context/SessionContext.jsx';
|
|||||||
import { useSettingsNamespace } from '../../settings/index.js';
|
import { useSettingsNamespace } from '../../settings/index.js';
|
||||||
import { useRoomCameraSnapshots } from '../../hooks/useRoomCameraSnapshots.js';
|
import { useRoomCameraSnapshots } from '../../hooks/useRoomCameraSnapshots.js';
|
||||||
import RoomCameraFeed from '../RoomCameraFeed/index.jsx';
|
import RoomCameraFeed from '../RoomCameraFeed/index.jsx';
|
||||||
|
import CardFrame from '../CardFrame/index.jsx';
|
||||||
|
|
||||||
function EmptyState() {
|
function EmptyState() {
|
||||||
return (
|
return (
|
||||||
@@ -66,35 +67,32 @@ export default function RoomCameraPanel({
|
|||||||
return <EmptyState />;
|
return <EmptyState />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const actions = showLayoutToggle ? (
|
||||||
|
<div className="flex items-center gap-0.5 text-[0.68rem] text-slate-400">
|
||||||
|
<span>Layout</span>
|
||||||
|
<div className="inline-flex overflow-hidden rounded border border-slate-700">
|
||||||
|
{ORIENTATIONS.map((option) => (
|
||||||
|
<button
|
||||||
|
key={option}
|
||||||
|
type="button"
|
||||||
|
className={`px-1 py-0.5 ${effectiveOrientation === option ? 'bg-slate-600 text-white' : 'bg-transparent text-slate-400 hover:text-white'}`}
|
||||||
|
onClick={() => applyOrientation(option)}
|
||||||
|
>
|
||||||
|
{option === 'vertical' ? 'Vertical' : 'Grid'}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="panel-section space-y-0.5 text-base">
|
<CardFrame
|
||||||
{!hideHeader && (
|
title="Room cameras"
|
||||||
<header className="panel-title-row">
|
meta={cameras.length}
|
||||||
<div className="panel-title-left">
|
actions={actions}
|
||||||
<p className="panel-title-text">Room cameras</p>
|
hideHeader={hideHeader}
|
||||||
<span className="panel-title-meta">{cameras.length}</span>
|
bodyClassName="space-y-0.5 text-base"
|
||||||
</div>
|
>
|
||||||
<div className="panel-title-right">
|
|
||||||
{showLayoutToggle && (
|
|
||||||
<div className="panel-title-actions">
|
|
||||||
<span className="panel-title-meta">Layout</span>
|
|
||||||
<div className="inline-flex overflow-hidden rounded border border-slate-700">
|
|
||||||
{ORIENTATIONS.map((option) => (
|
|
||||||
<button
|
|
||||||
key={option}
|
|
||||||
type="button"
|
|
||||||
className={`px-1 py-0.5 ${effectiveOrientation === option ? 'bg-slate-600 text-white' : 'bg-transparent text-slate-400 hover:text-white'}`}
|
|
||||||
onClick={() => applyOrientation(option)}
|
|
||||||
>
|
|
||||||
{option === 'vertical' ? 'Vertical' : 'Grid'}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
)}
|
|
||||||
<div className={containerClass}>
|
<div className={containerClass}>
|
||||||
{cameras.map((camera) => {
|
{cameras.map((camera) => {
|
||||||
const feed = feedMap[camera.id] || null;
|
const feed = feedMap[camera.id] || null;
|
||||||
@@ -109,6 +107,6 @@ export default function RoomCameraPanel({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</CardFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useSessionActions, useSessionSelector } from '../../context/SessionContext.jsx';
|
import { useSessionActions, useSessionSelector } from '../../context/SessionContext.jsx';
|
||||||
import { roverNameChromeStyle } from '../../lib/roverColor.js';
|
import { roverNameChromeStyle } from '../../lib/roverColor.js';
|
||||||
|
import CardFrame from '../CardFrame/index.jsx';
|
||||||
|
|
||||||
function classNames(...values) {
|
function classNames(...values) {
|
||||||
return values.filter(Boolean).join(' ');
|
return values.filter(Boolean).join(' ');
|
||||||
@@ -96,12 +97,7 @@ export default function RoverQueuesPanel({ title = 'Rovers' }) {
|
|||||||
users.find((u) => u.socketId === socketId) || { socketId, nickname: null, role: null };
|
users.find((u) => u.socketId === socketId) || { socketId, nickname: null, role: null };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-0.5 text-sm">
|
<CardFrame title={title} bodyClassName="space-y-0.5 text-sm">
|
||||||
{title && (
|
|
||||||
<div className="panel-title-row">
|
|
||||||
<p className="panel-title-text">{title}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{rosterItems.length === 0 ? (
|
{rosterItems.length === 0 ? (
|
||||||
<p className="text-sm text-slate-500">No rovers registered.</p>
|
<p className="text-sm text-slate-500">No rovers registered.</p>
|
||||||
) : (
|
) : (
|
||||||
@@ -220,6 +216,6 @@ export default function RoverQueuesPanel({ title = 'Rovers' }) {
|
|||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
</div>
|
</CardFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ function SensorDetails({ sensors, dockState }) {
|
|||||||
function DetailCard({ title, children }) {
|
function DetailCard({ title, children }) {
|
||||||
return (
|
return (
|
||||||
<div className="surface space-y-0.5 p-1 text-sm">
|
<div className="surface space-y-0.5 p-1 text-sm">
|
||||||
<div className="panel-title-text">{title}</div>
|
<div className="text-[0.78rem] font-semibold leading-none text-slate-200">{title}</div>
|
||||||
<div className="space-y-0.5">{children}</div>
|
<div className="space-y-0.5">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -68,12 +68,6 @@ function TopDownMapContent({ sensors = {}, variant = 'full', size: overrideSize,
|
|||||||
className={`${overlay ? 'relative' : 'surface relative p-1'}`}
|
className={`${overlay ? 'relative' : 'surface relative p-1'}`}
|
||||||
style={overlay ? { width: `${size}px`, height: `${size}px` } : { height: '100%', width: '100%', aspectRatio: '1 / 1' }}
|
style={overlay ? { width: `${size}px`, height: `${size}px` } : { height: '100%', width: '100%', aspectRatio: '1 / 1' }}
|
||||||
>
|
>
|
||||||
{!overlay ? (
|
|
||||||
<>
|
|
||||||
<div className="absolute left-1 top-1 text-xs text-slate-400">Top-down</div>
|
|
||||||
<div className="absolute right-1 top-1 text-[0.65rem] text-slate-500">0–{maxLight}</div>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
<svg width="100%" height="100%" viewBox={`0 0 ${size} ${size}`} preserveAspectRatio="xMidYMid meet" className="mx-auto block">
|
<svg width="100%" height="100%" viewBox={`0 0 ${size} ${size}`} preserveAspectRatio="xMidYMid meet" className="mx-auto block">
|
||||||
<circle cx={centerX} cy={centerY} r={innerCircle} fill="#0f172a" stroke="#334155" strokeWidth="2" />
|
<circle cx={centerX} cy={centerY} r={innerCircle} fill="#0f172a" stroke="#334155" strokeWidth="2" />
|
||||||
<WheelVisual cx={centerX - wheelLineOffset} cy={centerY} current={wheelCurrentLeft} drop={bumps.wheelDropLeft} overcurrent={wheelOver.leftWheel} label="L" />
|
<WheelVisual cx={centerX - wheelLineOffset} cy={centerY} current={wheelCurrentLeft} drop={bumps.wheelDropLeft} overcurrent={wheelOver.leftWheel} label="L" />
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useSessionSelector } from '../../context/SessionContext.jsx';
|
|||||||
import NicknameForm from '../NicknameForm/index.jsx';
|
import NicknameForm from '../NicknameForm/index.jsx';
|
||||||
import SocialButtonsGrid from '../SocialButtonsGrid/index.jsx';
|
import SocialButtonsGrid from '../SocialButtonsGrid/index.jsx';
|
||||||
import { roverBadgeStyle, roverNameChromeStyle } from '../../lib/roverColor.js';
|
import { roverBadgeStyle, roverNameChromeStyle } from '../../lib/roverColor.js';
|
||||||
|
import CardFrame from '../CardFrame/index.jsx';
|
||||||
|
|
||||||
export function NicknameEntryPanel({ compact = false }) {
|
export function NicknameEntryPanel({ compact = false }) {
|
||||||
return (
|
return (
|
||||||
@@ -19,12 +20,9 @@ export function NicknameEntryPanel({ compact = false }) {
|
|||||||
|
|
||||||
export function LinkButtonsPanel() {
|
export function LinkButtonsPanel() {
|
||||||
return (
|
return (
|
||||||
<section className="panel-section flex h-full min-h-0 flex-col gap-0.5 text-base">
|
<CardFrame title="Socials" fillHeight bodyClassName="flex flex-1 min-h-0 flex-col gap-0.5 text-base">
|
||||||
<div className="panel-title-row">
|
|
||||||
<span className="panel-title-text">Socials</span>
|
|
||||||
</div>
|
|
||||||
<SocialButtonsGrid className="flex-1 min-h-0" />
|
<SocialButtonsGrid className="flex-1 min-h-0" />
|
||||||
</section>
|
</CardFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
import { mergeFloatChunks, encodeWavMono16 } from './audioCodec.js';
|
import { mergeFloatChunks, encodeWavMono16 } from './audioCodec.js';
|
||||||
import StatusIndicator from './StatusIndicator.jsx';
|
import StatusIndicator from './StatusIndicator.jsx';
|
||||||
import KeyPill from './KeyPill.jsx';
|
import KeyPill from './KeyPill.jsx';
|
||||||
|
import CardFrame from '../../CardFrame/index.jsx';
|
||||||
|
|
||||||
export default function VipAudioUploadCard({
|
export default function VipAudioUploadCard({
|
||||||
ownRoverId = '',
|
ownRoverId = '',
|
||||||
@@ -529,9 +530,8 @@ export default function VipAudioUploadCard({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="surface">
|
<CardFrame title="Audio Controls">
|
||||||
<div className="grid gap-1">
|
<div className="grid gap-1">
|
||||||
<p className="text-sm text-slate-100 text-center">Audio Controls</p>
|
|
||||||
<section className="surface">
|
<section className="surface">
|
||||||
<div className="flex items-center justify-center gap-0.5 py-0.25 text-xs text-slate-300">
|
<div className="flex items-center justify-center gap-0.5 py-0.25 text-xs text-slate-300">
|
||||||
<span>Push-to-Talk Key</span>
|
<span>Push-to-Talk Key</span>
|
||||||
@@ -653,6 +653,6 @@ export default function VipAudioUploadCard({
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</CardFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// Purpose: Defines the Vip Identity Card module and the local helpers/components used in this file.
|
// Purpose: Defines the Vip Identity Card module and the local helpers/components used in this file.
|
||||||
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
|
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import CardFrame from '../CardFrame/index.jsx';
|
||||||
import { fieldClass, flowWrapClass, innerFlowClass, maskKey } from './constants.js';
|
import { fieldClass, flowWrapClass, innerFlowClass, maskKey } from './constants.js';
|
||||||
|
|
||||||
export default function VipIdentityCard({ currentStoredKey, applyIdentityKey, onMessage, fullWidth = false }) {
|
export default function VipIdentityCard({ currentStoredKey, applyIdentityKey, onMessage, fullWidth = false }) {
|
||||||
@@ -40,9 +41,8 @@ export default function VipIdentityCard({ currentStoredKey, applyIdentityKey, on
|
|||||||
const wrapClass = fullWidth ? 'w-full' : flowWrapClass;
|
const wrapClass = fullWidth ? 'w-full' : flowWrapClass;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={`surface ${wrapClass}`}>
|
<CardFrame title="Identity key" className={wrapClass}>
|
||||||
<div className={innerFlowClass}>
|
<div className={innerFlowClass}>
|
||||||
<p className="text-sm text-slate-300">Identity key</p>
|
|
||||||
<p className="text-xs text-slate-500">Current: {maskKey(currentStoredKey) || 'not set yet'}</p>
|
<p className="text-xs text-slate-500">Current: {maskKey(currentStoredKey) || 'not set yet'}</p>
|
||||||
<input
|
<input
|
||||||
className={fieldClass}
|
className={fieldClass}
|
||||||
@@ -109,6 +109,6 @@ export default function VipIdentityCard({ currentStoredKey, applyIdentityKey, on
|
|||||||
</form>
|
</form>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</CardFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// Purpose: Renders a shared lift controller panel synced from server session state.
|
// Purpose: Renders a shared lift controller panel synced from server session state.
|
||||||
// Scope: Presents verified-user controls while reflecting global busy/position/cooldown state.
|
// Scope: Presents verified-user controls while reflecting global busy/position/cooldown state.
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
import CardFrame from '../CardFrame/index.jsx';
|
||||||
|
|
||||||
function badgeClass(tone) {
|
function badgeClass(tone) {
|
||||||
if (tone === 'good') return 'bg-emerald-600 text-white';
|
if (tone === 'good') return 'bg-emerald-600 text-white';
|
||||||
@@ -67,7 +68,16 @@ export default function VipLiftCard({ lift, onUp, onDown, fullWidth = false }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={`surface relative text-sm text-slate-200 ${wrapClass}`}>
|
<CardFrame
|
||||||
|
title="Lift Controls"
|
||||||
|
className={`relative ${wrapClass}`}
|
||||||
|
bodyClassName="text-sm text-slate-200"
|
||||||
|
actions={
|
||||||
|
<span className={`inline-flex w-auto rounded px-1 py-0.25 text-xs font-semibold ${badgeClass(statusTone)}`}>
|
||||||
|
{status}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
{blocked ? (
|
{blocked ? (
|
||||||
<div className="absolute inset-0 z-20 flex items-center justify-center rounded-md bg-slate-950/80 px-1.5 text-center">
|
<div className="absolute inset-0 z-20 flex items-center justify-center rounded-md bg-slate-950/80 px-1.5 text-center">
|
||||||
<div className="space-y-0.25">
|
<div className="space-y-0.25">
|
||||||
@@ -82,18 +92,10 @@ export default function VipLiftCard({ lift, onUp, onDown, fullWidth = false }) {
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="grid gap-0.5">
|
<div className="grid gap-0.5">
|
||||||
<div className="relative flex items-center justify-center min-h-[1.5rem]">
|
<div className="text-center">
|
||||||
<div className="text-center">
|
<p className="text-xs text-slate-400">Move the lift up and down. Please don't break anything...</p>
|
||||||
<p className="text-sm text-slate-100">Lift Controls</p>
|
|
||||||
<p className="text-xs text-slate-400">Move the lift up and down. Please don't break anything...</p>
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
className={`absolute right-0 inline-flex w-auto rounded px-1 py-0.25 text-xs font-semibold ${badgeClass(statusTone)}`}
|
|
||||||
>
|
|
||||||
{status}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section className="surface-muted px-0.5 py-0.5">
|
<section className="surface-muted px-0.5 py-0.5">
|
||||||
@@ -133,6 +135,6 @@ export default function VipLiftCard({ lift, onUp, onDown, fullWidth = false }) {
|
|||||||
|
|
||||||
{lift?.lastError ? <p className="text-xs text-rose-300 text-center">Last error: {lift.lastError}</p> : null}
|
{lift?.lastError ? <p className="text-xs text-rose-300 text-center">Last error: {lift.lastError}</p> : null}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</CardFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// Purpose: Defines the Vip Neato Card module and the local helpers/components used in this file.
|
// Purpose: Defines the Vip Neato Card module and the local helpers/components used in this file.
|
||||||
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
|
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import CardFrame from '../CardFrame/index.jsx';
|
||||||
|
|
||||||
function normalizeState(value) {
|
function normalizeState(value) {
|
||||||
return String(value || '').trim();
|
return String(value || '').trim();
|
||||||
@@ -98,18 +99,19 @@ export default function VipNeatoCard({
|
|||||||
const primaryState = docked ? 'Docked' : uiStateLabel !== '--' ? uiStateLabel : 'Away from dock';
|
const primaryState = docked ? 'Docked' : uiStateLabel !== '--' ? uiStateLabel : 'Away from dock';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={`surface text-sm text-slate-200 ${wrapClass}`}>
|
<CardFrame
|
||||||
|
title="Neato Controls"
|
||||||
|
className={wrapClass}
|
||||||
|
bodyClassName="text-sm text-slate-200"
|
||||||
|
actions={
|
||||||
|
<span className={`inline-flex w-auto rounded px-1 py-0.25 text-xs font-semibold ${metricToneClass(headerTone)}`}>
|
||||||
|
{headerStatus}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className="grid gap-0.5">
|
<div className="grid gap-0.5">
|
||||||
<div className="relative flex items-center justify-center min-h-[1.5rem]">
|
<div className="text-center">
|
||||||
<div className="text-center">
|
<p className="text-xs text-slate-400">Control the autonomous Neato robovac. Be nice to him.</p>
|
||||||
<p className="text-sm text-slate-100">Neato Controls</p>
|
|
||||||
<p className="text-xs text-slate-400">Control the autonomous Neato robovac. Be nice to him.</p>
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
className={`absolute right-0 inline-flex w-auto rounded px-1 py-0.25 text-xs font-semibold ${metricToneClass(headerTone)}`}
|
|
||||||
>
|
|
||||||
{headerStatus}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-0.5">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-0.5">
|
||||||
@@ -217,6 +219,6 @@ export default function VipNeatoCard({
|
|||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</CardFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// Purpose: Defines the Vip Private Rover Access Card module and the local helpers/components used in this file.
|
// Purpose: Defines the Vip Private Rover Access Card module and the local helpers/components used in this file.
|
||||||
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
|
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
|
import CardFrame from '../CardFrame/index.jsx';
|
||||||
import { flowWrapClass, innerFlowClass } from './constants.js';
|
import { flowWrapClass, innerFlowClass } from './constants.js';
|
||||||
|
|
||||||
export default function VipPrivateRoverAccessCard({
|
export default function VipPrivateRoverAccessCard({
|
||||||
@@ -42,9 +43,8 @@ export default function VipPrivateRoverAccessCard({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={`surface text-sm text-slate-300 ${wrapClass}`}>
|
<CardFrame title="Private rover access requests" className={wrapClass} bodyClassName="text-sm text-slate-300">
|
||||||
<div className={innerFlowClass}>
|
<div className={innerFlowClass}>
|
||||||
<p className="text-sm text-slate-300">Private rover access requests</p>
|
|
||||||
{requestableRovers.length === 0 ? (
|
{requestableRovers.length === 0 ? (
|
||||||
<p className="text-xs text-slate-500">No closed private rovers are available to request right now.</p>
|
<p className="text-xs text-slate-500">No closed private rovers are available to request right now.</p>
|
||||||
) : (
|
) : (
|
||||||
@@ -79,7 +79,6 @@ export default function VipPrivateRoverAccessCard({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</CardFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
// Scope: Owns local URL validation UX and writes to the existing profile settings namespace.
|
// Scope: Owns local URL validation UX and writes to the existing profile settings namespace.
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useSettingsNamespace } from '../../settings/index.js';
|
import { useSettingsNamespace } from '../../settings/index.js';
|
||||||
|
import CardFrame from '../CardFrame/index.jsx';
|
||||||
import { fieldClass, flowWrapClass, innerFlowClass } from './constants.js';
|
import { fieldClass, flowWrapClass, innerFlowClass } from './constants.js';
|
||||||
|
|
||||||
function normalizeHttpUrl(value) {
|
function normalizeHttpUrl(value) {
|
||||||
@@ -54,9 +55,8 @@ export default function VipProfileImageCard({ isVerified = false, fullWidth = fa
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={`surface text-sm text-slate-300 ${wrapClass}`}>
|
<CardFrame title="Chat profile image URL" className={wrapClass} bodyClassName="text-sm text-slate-300">
|
||||||
<form className={innerFlowClass} onSubmit={handleSave}>
|
<form className={innerFlowClass} onSubmit={handleSave}>
|
||||||
<p className="text-sm text-slate-300">Chat profile image URL</p>
|
|
||||||
<p className="text-xs text-slate-500">
|
<p className="text-xs text-slate-500">
|
||||||
Verified users can set a custom avatar for chat and Discord bridge messages.
|
Verified users can set a custom avatar for chat and Discord bridge messages.
|
||||||
</p>
|
</p>
|
||||||
@@ -86,6 +86,6 @@ export default function VipProfileImageCard({ isVerified = false, fullWidth = fa
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</CardFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
|
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import NicknameForm from '../NicknameForm/index.jsx';
|
import NicknameForm from '../NicknameForm/index.jsx';
|
||||||
|
import CardFrame from '../CardFrame/index.jsx';
|
||||||
import { flowWrapClass, innerFlowClass, fieldClass } from './constants.js';
|
import { flowWrapClass, innerFlowClass, fieldClass } from './constants.js';
|
||||||
|
|
||||||
export default function VipVerificationCard({
|
export default function VipVerificationCard({
|
||||||
@@ -61,29 +62,28 @@ export default function VipVerificationCard({
|
|||||||
|
|
||||||
if (pendingRequestId) {
|
if (pendingRequestId) {
|
||||||
return (
|
return (
|
||||||
<section className={`surface text-sm text-slate-300 ${wrapClass}`}>
|
<CardFrame title="Verification" className={wrapClass} bodyClassName="text-sm text-slate-300">
|
||||||
<div className={innerFlowClass}>Verification request pending: {pendingRequestId}</div>
|
<div className={innerFlowClass}>Verification request pending: {pendingRequestId}</div>
|
||||||
</section>
|
</CardFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requestFlowStep === 0) {
|
if (requestFlowStep === 0) {
|
||||||
return (
|
return (
|
||||||
<section className={`surface ${wrapClass}`}>
|
<CardFrame title="Verification" className={wrapClass}>
|
||||||
<div className={innerFlowClass}>
|
<div className={innerFlowClass}>
|
||||||
<p className="text-sm text-slate-300">Verification</p>
|
|
||||||
<button type="button" className="button-dark text-sm" onClick={beginRequestFlow} disabled={working}>
|
<button type="button" className="button-dark text-sm" onClick={beginRequestFlow} disabled={working}>
|
||||||
Request Verification
|
Request Verification
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</CardFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className={`surface ${wrapClass}`} onSubmit={handleRequestSubmit}>
|
<CardFrame title="Request verification" className={wrapClass}>
|
||||||
|
<form onSubmit={handleRequestSubmit}>
|
||||||
<div className={innerFlowClass}>
|
<div className={innerFlowClass}>
|
||||||
<p className="text-sm text-slate-300">Request verification</p>
|
|
||||||
<p className="text-xs text-slate-500">
|
<p className="text-xs text-slate-500">
|
||||||
Step {requestFlowStep} of 3
|
Step {requestFlowStep} of 3
|
||||||
</p>
|
</p>
|
||||||
@@ -189,6 +189,7 @@ export default function VipVerificationCard({
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
</CardFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,30 +71,6 @@ body {
|
|||||||
@apply text-sm text-slate-400 rounded-md;
|
@apply text-sm text-slate-400 rounded-md;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-title-row {
|
|
||||||
@apply flex items-center justify-between gap-0.5 rounded-md bg-slate-900 px-0.5 py-0.5 text-left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-title-text {
|
|
||||||
@apply text-[0.78rem] font-semibold tracking-normal text-slate-200 leading-none text-left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-title-meta {
|
|
||||||
@apply text-[0.68rem] font-medium tracking-normal text-slate-400 leading-none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-title-left {
|
|
||||||
@apply flex min-w-0 items-center gap-0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-title-right {
|
|
||||||
@apply flex flex-wrap items-center justify-end gap-0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-title-actions {
|
|
||||||
@apply flex items-center gap-0.5 text-[0.68rem] text-slate-400 leading-none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.surface {
|
.surface {
|
||||||
@apply bg-neutral-800 text-white px-0.5 py-0.5 rounded-md;
|
@apply bg-neutral-800 text-white px-0.5 py-0.5 rounded-md;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user