rover based colors!

This commit is contained in:
legop3
2026-05-21 20:49:01 -04:00
parent d72bc0db03
commit 7e7b07083a
27 changed files with 268 additions and 169 deletions
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="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-D13hsMAR.js"></script> <script type="module" crossorigin src="/assets/index-tQBHQF7J.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Dva9v7lV.css"> <link rel="stylesheet" crossorigin href="/assets/index-C-j0HVkU.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
@@ -127,7 +127,7 @@ export default function ButtonBoxPanel() {
}, [effectiveAlertVolume, socket]); }, [effectiveAlertVolume, socket]);
return ( return (
<CardFrame title="Button Box" accent="#eab308" bodyClassName="space-y-0.5 text-base"> <CardFrame title="Button Box" bodyClassName="space-y-0.5 text-base">
<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);
+22 -8
View File
@@ -1,7 +1,11 @@
import { useSessionSelector } from '../../context/SessionContext.jsx';
// Utilities
function cx(...values) { function cx(...values) {
return values.filter(Boolean).join(' '); return values.filter(Boolean).join(' ');
} }
// Color helpers
function hexToRgb(hex) { function hexToRgb(hex) {
const raw = String(hex || '').trim(); const raw = String(hex || '').trim();
const normalized = raw.startsWith('#') ? raw.slice(1) : raw; const normalized = raw.startsWith('#') ? raw.slice(1) : raw;
@@ -24,21 +28,29 @@ function rgba(rgb, alpha) {
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alpha})`; return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alpha})`;
} }
// Component
export default function CardFrame({ export default function CardFrame({
title = '', title = '',
meta = null, meta = null,
actions = null, actions = null,
accent = null,
hideHeader = false, hideHeader = false,
className = '', className = '',
headerClassName = '', headerClassName = '',
bodyClassName = '', bodyClassName = '',
fillHeight = false, fillHeight = false,
clipOverflow = true,
children, children,
}) { }) {
const showHeader = !hideHeader && (title || meta != null || actions); const showHeader = !hideHeader && (title || meta != null || actions);
const accentRgb = hexToRgb(accent); const ownRoverColor = useSessionSelector((state) => {
const cardStyle = accentRgb ? { borderColor: rgba(accentRgb, 0.65) } : undefined; const roverId = String(state.session?.assignment?.roverId || '').trim();
if (!roverId) return null;
const roster = Array.isArray(state.session?.roster) ? state.session.roster : [];
const rover = roster.find((entry) => String(entry?.id) === roverId);
return rover?.color || null;
});
const accentRgb = hexToRgb(ownRoverColor);
const cardStyle = accentRgb ? { borderColor: rgba(accentRgb, 0.35) } : undefined;
const headerStyle = accentRgb const headerStyle = accentRgb
? { ? {
backgroundImage: `linear-gradient(90deg, rgba(23,23,23,0.96) 0%, rgba(38,38,38,0.94) 58%, ${rgba(accentRgb, 0.18)} 100%)`, backgroundImage: `linear-gradient(90deg, rgba(23,23,23,0.96) 0%, rgba(38,38,38,0.94) 58%, ${rgba(accentRgb, 0.18)} 100%)`,
@@ -48,28 +60,30 @@ export default function CardFrame({
return ( return (
<section <section
className={cx( 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)]', 'panel-section border border-neutral-500/60 bg-neutral-900/95 shadow-[0_1px_0_rgba(255,255,255,0.05)_inset,0_10px_24px_rgba(0,0,0,0.28)]',
clipOverflow ? 'overflow-hidden' : 'overflow-visible',
fillHeight && 'flex h-full min-h-0 flex-col', fillHeight && 'flex h-full min-h-0 flex-col',
className, className,
)} )}
style={cardStyle} style={cardStyle}
> >
{showHeader ? ( {showHeader ? (
// Header row
<header <header
className={cx( 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', 'flex items-center justify-between gap-0.5 border-b border-neutral-500/50 bg-gradient-to-r from-neutral-800 via-neutral-700 to-neutral-600 px-0.5 py-0.5',
headerClassName, headerClassName,
)} )}
style={headerStyle} style={headerStyle}
> >
<div className="flex min-w-0 items-center gap-0.5"> <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} {title ? <p className="m-0 text-[0.78rem] font-semibold leading-none text-neutral-50">{title}</p> : null}
{meta != null ? <span className="text-[0.68rem] font-medium leading-none text-neutral-300">{meta}</span> : null} {meta != null ? <span className="text-[0.68rem] font-medium leading-none text-neutral-200">{meta}</span> : null}
</div> </div>
{actions ? <div className="flex flex-wrap items-center justify-end gap-0.5">{actions}</div> : null} {actions ? <div className="flex flex-wrap items-center justify-end gap-0.5">{actions}</div> : null}
</header> </header>
) : null} ) : null}
<div className={cx('', fillHeight && 'flex flex-1 min-h-0 flex-col', bodyClassName)}>{children}</div> <div className={cx(fillHeight && 'flex flex-1 min-h-0 flex-col', bodyClassName)}>{children}</div>
</section> </section>
); );
} }
+1 -1
View File
@@ -109,7 +109,7 @@ export default function ChatPanel({
return ( return (
<CardFrame <CardFrame
title={title} title={title}
accent="#22c55e"
hideHeader={!title} hideHeader={!title}
fillHeight={fillHeight} fillHeight={fillHeight}
bodyClassName="space-y-0.5 text-base" bodyClassName="space-y-0.5 text-base"
@@ -8,11 +8,40 @@ const MOBILE_DISMISS_MS = 10000;
const MAX_FONT_PX = 28; const MAX_FONT_PX = 28;
const MIN_FONT_PX = 14; const MIN_FONT_PX = 14;
function hexToRgb(hex) {
const raw = String(hex || '').trim();
const normalized = raw.startsWith('#') ? raw.slice(1) : raw;
if (!/^[0-9a-fA-F]{3}$|^[0-9a-fA-F]{6}$/.test(normalized)) return null;
const expanded =
normalized.length === 3
? normalized
.split('')
.map((ch) => ch + ch)
.join('')
: normalized;
return {
r: Number.parseInt(expanded.slice(0, 2), 16),
g: Number.parseInt(expanded.slice(2, 4), 16),
b: Number.parseInt(expanded.slice(4, 6), 16),
};
}
function rgba(rgb, alpha) {
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alpha})`;
}
export default function GlobalObjectiveBanner({ layout = 'desktop', className = '', dismissable = true }) { export default function GlobalObjectiveBanner({ layout = 'desktop', className = '', dismissable = true }) {
const goalText = useSessionSelector((state) => { const goalText = useSessionSelector((state) => {
const text = state.session?.globalObjective?.text; const text = state.session?.globalObjective?.text;
return text ? String(text).trim() : ''; return text ? String(text).trim() : '';
}); });
const ownRoverColor = useSessionSelector((state) => {
const roverId = String(state.session?.assignment?.roverId || '').trim();
if (!roverId) return null;
const roster = Array.isArray(state.session?.roster) ? state.session.roster : [];
const rover = roster.find((entry) => String(entry?.id) === roverId);
return rover?.color || null;
});
const isMobile = layout === 'mobile-portrait' || layout === 'mobile-landscape' || layout === 'mobile'; const isMobile = layout === 'mobile-portrait' || layout === 'mobile-landscape' || layout === 'mobile';
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [fontSize, setFontSize] = useState(MAX_FONT_PX); const [fontSize, setFontSize] = useState(MAX_FONT_PX);
@@ -72,7 +101,7 @@ export default function GlobalObjectiveBanner({ layout = 'desktop', className =
const containerClass = useMemo( const containerClass = useMemo(
() => () =>
[ [
'panel-section flex w-full items-center justify-center', 'panel-section flex w-full items-center justify-center border border-neutral-500/60 shadow-[0_1px_0_rgba(255,255,255,0.05)_inset,0_10px_24px_rgba(0,0,0,0.28)]',
isMobile ? 'rounded-none' : 'rounded', isMobile ? 'rounded-none' : 'rounded',
'px-1 py-1 text-center font-semibold tracking-tight', 'px-1 py-1 text-center font-semibold tracking-tight',
className, className,
@@ -81,6 +110,13 @@ export default function GlobalObjectiveBanner({ layout = 'desktop', className =
.join(' '), .join(' '),
[className, isMobile], [className, isMobile],
); );
const accentRgb = hexToRgb(ownRoverColor);
const frameStyle = accentRgb
? {
borderColor: rgba(accentRgb, 0.35),
backgroundImage: `linear-gradient(90deg, rgba(23,23,23,0.96) 0%, rgba(38,38,38,0.94) 58%, ${rgba(accentRgb, 0.18)} 100%)`,
}
: undefined;
if (!goalText || !visible) return null; if (!goalText || !visible) return null;
@@ -94,7 +130,7 @@ export default function GlobalObjectiveBanner({ layout = 'desktop', className =
<div <div
ref={containerRef} ref={containerRef}
className={containerClass} className={containerClass}
style={{ fontSize: `${fontSize}px`, lineHeight: 1.1 }} style={{ ...(frameStyle || {}), fontSize: `${fontSize}px`, lineHeight: 1.1 }}
{...dismissProps} {...dismissProps}
> >
<span className="flex w-full items-stretch gap-0.5 whitespace-nowrap rounded-md"> <span className="flex w-full items-stretch gap-0.5 whitespace-nowrap rounded-md">
@@ -256,7 +256,7 @@ export default function HomeAssistantControls() {
if (!ha?.enabled) { if (!ha?.enabled) {
return ( return (
<CardFrame title="Room Controls" accent="#14b8a6" bodyClassName="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="text-slate-500">Not configured on the server.</p> <p className="text-slate-500">Not configured on the server.</p>
</CardFrame> </CardFrame>
); );
@@ -264,7 +264,7 @@ export default function HomeAssistantControls() {
if (entities.length === 0) { if (entities.length === 0) {
return ( return (
<CardFrame title="Room Controls" accent="#14b8a6" bodyClassName="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="text-slate-500">No lights or switches configured.</p> <p className="text-slate-500">No lights or switches configured.</p>
</CardFrame> </CardFrame>
); );
@@ -290,7 +290,7 @@ export default function HomeAssistantControls() {
); );
return ( return (
<CardFrame title="Room Controls" accent="#14b8a6" actions={actions} bodyClassName="space-y-0.5 text-base"> <CardFrame title="Room Controls" actions={actions} bodyClassName="space-y-0.5 text-base">
{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'
+1 -1
View File
@@ -9,7 +9,7 @@ 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 (
<CardFrame title="Server logs" accent="#f97316" bodyClassName="space-y-0.5 text-base"> <CardFrame title="Server logs" bodyClassName="space-y-0.5 text-base">
<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>
@@ -22,7 +22,7 @@ export default function OverseerPreferencePanel() {
}; };
return ( return (
<CardFrame title="Overseer vote" accent="#10b981" bodyClassName="space-y-0.5 text-center text-xs text-slate-200"> <CardFrame title="Overseer vote" bodyClassName="space-y-0.5 text-center text-xs text-slate-200">
<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'
@@ -60,7 +60,7 @@ export default function RawUserPilePanel({
return ( return (
<CardFrame <CardFrame
title={!hideHeader ? 'Users' : ''} title={!hideHeader ? 'Users' : ''}
accent="#94a3b8"
hideHeader={hideHeader} hideHeader={hideHeader}
fillHeight={fillHeight} fillHeight={fillHeight}
className={className} className={className}
@@ -150,7 +150,7 @@ export default function ReplaySourcesPanel({ panelId = 'replay-sources', fillHei
const listWrapClass = fillHeight ? 'flex-1 min-h-0 overflow-y-auto' : ''; const listWrapClass = fillHeight ? 'flex-1 min-h-0 overflow-y-auto' : '';
return ( return (
<CardFrame title="Replay Sources" accent="#06b6d4" fillHeight={fillHeight} bodyClassName="space-y-0.5 text-sm"> <CardFrame title="Replay Sources" fillHeight={fillHeight} bodyClassName="space-y-0.5 text-sm">
<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} />
+1 -1
View File
@@ -36,7 +36,7 @@ function TopDownMapPanel() {
const sensors = frame?.sensors || {}; const sensors = frame?.sensors || {};
return ( return (
<CardFrame hideHeader accent="#0067f7"> <CardFrame hideHeader>
<div className="aspect-square w-full"> <div className="aspect-square w-full">
<TopDownMap sensors={sensors} /> <TopDownMap sensors={sensors} />
</div> </div>
@@ -88,7 +88,7 @@ export default function RoomCameraPanel({
return ( return (
<CardFrame <CardFrame
title="Room cameras" title="Room cameras"
accent="#38bdf8"
actions={actions} actions={actions}
hideHeader={hideHeader} hideHeader={hideHeader}
bodyClassName="space-y-0.5 text-base" bodyClassName="space-y-0.5 text-base"
@@ -97,7 +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 (
<CardFrame title={title} accent="#f59e0b" bodyClassName="space-y-0.5 text-sm"> <CardFrame title={title} bodyClassName="space-y-0.5 text-sm">
{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>
) : ( ) : (
+49 -1
View File
@@ -2,6 +2,7 @@
// Purpose: Defines the Tabs module and the local helpers/components used in this file. // Purpose: Defines the Tabs 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 { createContext, useCallback, useContext, useMemo, useState, useEffect } from 'react'; import { createContext, useCallback, useContext, useMemo, useState, useEffect } from 'react';
import { useSessionSelector } from '../../context/SessionContext.jsx';
const TabsContext = createContext(null); const TabsContext = createContext(null);
@@ -33,6 +34,28 @@ function classNames(...parts) {
return parts.filter(Boolean).join(' '); return parts.filter(Boolean).join(' ');
} }
function hexToRgb(hex) {
const raw = String(hex || '').trim();
const normalized = raw.startsWith('#') ? raw.slice(1) : raw;
if (!/^[0-9a-fA-F]{3}$|^[0-9a-fA-F]{6}$/.test(normalized)) return null;
const expanded =
normalized.length === 3
? normalized
.split('')
.map((ch) => ch + ch)
.join('')
: normalized;
return {
r: Number.parseInt(expanded.slice(0, 2), 16),
g: Number.parseInt(expanded.slice(2, 4), 16),
b: Number.parseInt(expanded.slice(4, 6), 16),
};
}
function rgba(rgb, alpha) {
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alpha})`;
}
export default function Tabs({ children, defaultTab, currentTab, onTabChange, variant = DEFAULT_VARIANT }) { export default function Tabs({ children, defaultTab, currentTab, onTabChange, variant = DEFAULT_VARIANT }) {
const [internalTab, setInternalTab] = useState(defaultTab ?? null); const [internalTab, setInternalTab] = useState(defaultTab ?? null);
const [tabOrder, setTabOrder] = useState([]); const [tabOrder, setTabOrder] = useState([]);
@@ -95,7 +118,32 @@ export default function Tabs({ children, defaultTab, currentTab, onTabChange, va
} }
export function TabList({ children, className = '' }) { export function TabList({ children, className = '' }) {
return <div className={classNames('flex gap-0.5', className)}>{children}</div>; const ownRoverColor = useSessionSelector((state) => {
const roverId = String(state.session?.assignment?.roverId || '').trim();
if (!roverId) return null;
const roster = Array.isArray(state.session?.roster) ? state.session.roster : [];
const rover = roster.find((entry) => String(entry?.id) === roverId);
return rover?.color || null;
});
const accentRgb = hexToRgb(ownRoverColor);
const frameStyle = accentRgb ? { borderColor: rgba(accentRgb, 0.35) } : undefined;
const headerStyle = accentRgb
? {
backgroundImage: `linear-gradient(90deg, rgba(23,23,23,0.96) 0%, rgba(38,38,38,0.94) 58%, ${rgba(accentRgb, 0.18)} 100%)`,
}
: undefined;
return (
<div
className={classNames(
'panel-section overflow-hidden border border-neutral-500/60 bg-neutral-900/95 p-0.5 shadow-[0_1px_0_rgba(255,255,255,0.05)_inset,0_10px_24px_rgba(0,0,0,0.28)]',
className
)}
style={frameStyle}
>
<div className="flex gap-0.5" style={headerStyle}>{children}</div>
</div>
);
} }
export function Tab({ id, children, className = '', disabled = false, highlight = 'none' }) { export function Tab({ id, children, className = '', disabled = false, highlight = 'none' }) {
@@ -5,6 +5,7 @@ import { useMemo } from 'react';
import { useSessionSelector } from '../../context/SessionContext.jsx'; import { useSessionSelector } from '../../context/SessionContext.jsx';
import { useTelemetryFrame } from '../../context/TelemetryContext.jsx'; import { useTelemetryFrame } from '../../context/TelemetryContext.jsx';
import { useDockIr } from '../../hooks/useDockIr.js'; import { useDockIr } from '../../hooks/useDockIr.js';
import CardFrame from '../CardFrame/index.jsx';
function formatMetric(value, fallback = '--') { function formatMetric(value, fallback = '--') {
if (value == null || value === '') return fallback; if (value == null || value === '') return fallback;
@@ -39,7 +40,7 @@ export default function TelemetryPanel() {
}, [activeDriverId, roverId, selfSocketId, users]); }, [activeDriverId, roverId, selfSocketId, users]);
return ( return (
<section className="panel-section space-y-0.5 text-base text-slate-100"> <CardFrame hideHeader clipOverflow={false} bodyClassName="space-y-0.5 text-base text-slate-100">
{/* <div className="text-sm text-slate-400"> {/* <div className="text-sm text-slate-400">
<span>{connected ? 'online' : 'offline'}</span> <span>{connected ? 'online' : 'offline'}</span>
<span> · role {session?.role || 'unknown'}</span> <span> · role {session?.role || 'unknown'}</span>
@@ -60,7 +61,7 @@ export default function TelemetryPanel() {
{rawSnippet && ( {rawSnippet && (
<pre className="surface whitespace-pre-wrap break-words text-xs text-lime-300">{rawSnippet}</pre> <pre className="surface whitespace-pre-wrap break-words text-xs text-lime-300">{rawSnippet}</pre>
)} )}
</section> </CardFrame>
); );
} }
+1 -1
View File
@@ -20,7 +20,7 @@ export function NicknameEntryPanel({ compact = false }) {
export function LinkButtonsPanel() { export function LinkButtonsPanel() {
return ( return (
<CardFrame title="Socials" accent="#3030f5" fillHeight bodyClassName="flex flex-1 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">
<SocialButtonsGrid className="flex-1 min-h-0" /> <SocialButtonsGrid className="flex-1 min-h-0" />
</CardFrame> </CardFrame>
); );
@@ -530,7 +530,7 @@ export default function VipAudioUploadCard({
); );
return ( return (
<CardFrame title="Audio Controls" accent="#a78bfa"> <CardFrame title="Audio Controls">
<div className="grid gap-1"> <div className="grid gap-1">
<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">
+1 -1
View File
@@ -41,7 +41,7 @@ export default function VipIdentityCard({ currentStoredKey, applyIdentityKey, on
const wrapClass = fullWidth ? 'w-full' : flowWrapClass; const wrapClass = fullWidth ? 'w-full' : flowWrapClass;
return ( return (
<CardFrame title="Identity key" accent="#60a5fa" className={wrapClass}> <CardFrame title="Identity key" className={wrapClass}>
<div className={innerFlowClass}> <div className={innerFlowClass}>
<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
+1 -1
View File
@@ -70,7 +70,7 @@ export default function VipLiftCard({ lift, onUp, onDown, fullWidth = false }) {
return ( return (
<CardFrame <CardFrame
title="Lift Controls" title="Lift Controls"
accent="#06b6d4"
className={`relative ${wrapClass}`} className={`relative ${wrapClass}`}
bodyClassName="text-sm text-slate-200" bodyClassName="text-sm text-slate-200"
actions={ actions={
+1 -1
View File
@@ -101,7 +101,7 @@ export default function VipNeatoCard({
return ( return (
<CardFrame <CardFrame
title="Neato Controls" title="Neato Controls"
accent="#22c55e"
className={wrapClass} className={wrapClass}
bodyClassName="text-sm text-slate-200" bodyClassName="text-sm text-slate-200"
actions={ actions={
@@ -43,7 +43,7 @@ export default function VipPrivateRoverAccessCard({
}; };
return ( return (
<CardFrame title="Private rover access requests" accent="#f43f5e" className={wrapClass} bodyClassName="text-sm text-slate-300"> <CardFrame title="Private rover access requests" className={wrapClass} bodyClassName="text-sm text-slate-300">
<div className={innerFlowClass}> <div className={innerFlowClass}>
{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>
@@ -55,7 +55,7 @@ export default function VipProfileImageCard({ isVerified = false, fullWidth = fa
}; };
return ( return (
<CardFrame title="Chat profile image URL" accent="#f59e0b" className={wrapClass} bodyClassName="text-sm text-slate-300"> <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-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.
@@ -62,7 +62,7 @@ export default function VipVerificationCard({
if (pendingRequestId) { if (pendingRequestId) {
return ( return (
<CardFrame title="Verification" accent="#eab308" className={wrapClass} bodyClassName="text-sm text-slate-300"> <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>
</CardFrame> </CardFrame>
); );
@@ -70,7 +70,7 @@ export default function VipVerificationCard({
if (requestFlowStep === 0) { if (requestFlowStep === 0) {
return ( return (
<CardFrame title="Verification" accent="#eab308" className={wrapClass}> <CardFrame title="Verification" className={wrapClass}>
<div className={innerFlowClass}> <div className={innerFlowClass}>
<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
@@ -81,7 +81,7 @@ export default function VipVerificationCard({
} }
return ( return (
<CardFrame title="Request verification" accent="#eab308" className={wrapClass}> <CardFrame title="Request verification" className={wrapClass}>
<form onSubmit={handleRequestSubmit}> <form onSubmit={handleRequestSubmit}>
<div className={innerFlowClass}> <div className={innerFlowClass}>
<p className="text-xs text-slate-500"> <p className="text-xs text-slate-500">