pre cardcolor

This commit is contained in:
legop3
2026-05-21 02:42:44 -04:00
parent bfd1a94030
commit 331302da01
27 changed files with 324 additions and 328 deletions
@@ -7,6 +7,7 @@ import { useSocket } from '../../context/SocketContext.jsx';
import { useSettingsNamespace } from '../../settings/index.js';
import { AUDIO_SETTINGS_DEFAULTS } from '../../settings/namespaces.js';
import ButtonBoxTile from '../ButtonBoxTile/index.jsx';
import CardFrame from '../CardFrame/index.jsx';
const FLASH_MS = 420;
const REWARD_FLASH_MS = 1200;
@@ -126,10 +127,7 @@ export default function ButtonBoxPanel() {
}, [effectiveAlertVolume, socket]);
return (
<section className="panel-section space-y-0.5 text-base">
<header className="panel-title-row">
<p className="panel-title-text">Button Box</p>
</header>
<CardFrame title="Button Box" bodyClassName="space-y-0.5 text-base">
<div className="grid grid-cols-4 gap-0.5">
{buttons.map((button) => {
const id = Number(button.id);
@@ -155,6 +153,6 @@ export default function ButtonBoxPanel() {
);
})}
</div>
</section>
</CardFrame>
);
}
+43
View File
@@ -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>
);
}
+8 -7
View File
@@ -7,6 +7,7 @@ import { useSessionSelector } from '../../context/SessionContext.jsx';
import { useSettingsNamespace } from '../../settings/index.js';
import ChatMessageRow from '../ChatMessageRow/index.jsx';
import ChatTypingRow from '../ChatTypingRow/index.jsx';
import CardFrame from '../CardFrame/index.jsx';
const FLITE_VOICES = ['kal', 'rms', 'slt', 'ksp', 'bdl'];
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';
return (
<section className={`panel-section space-y-0.5 text-base ${fillHeight ? 'flex h-full flex-col overflow-hidden' : ''}`}>
{title ? (
<div className="panel-title-row">
<span className="panel-title-text">{title}</span>
</div>
) : null}
<CardFrame
title={title}
hideHeader={!title}
fillHeight={fillHeight}
bodyClassName="space-y-0.5 text-base"
>
<div className={`surface overflow-y-auto space-y-0.5 px-0 ${listClass}`} ref={listRef}>
{sorted.length === 0 && typingRows.length === 0 ? (
<p className="text-sm text-slate-500">No messages yet.</p>
@@ -218,6 +219,6 @@ export default function ChatPanel({
</button>
</form>
)}
</section>
</CardFrame>
);
}
@@ -5,6 +5,7 @@ import { useEffect, useMemo, useRef, useState } from 'react';
import { useSessionActions, useSessionSelector } from '../../context/SessionContext.jsx';
import { useControlSystem } from '../../controls/index.js';
import { formatKeyLabel } from '../../controls/keymapUtils.js';
import CardFrame from '../CardFrame/index.jsx';
function StatusBadge({ label, tone = 'muted' }) {
const styles =
@@ -255,46 +256,41 @@ export default function HomeAssistantControls() {
if (!ha?.enabled) {
return (
<section className="panel-section space-y-0.5 text-sm text-slate-400">
<p className="panel-title-text">Room Controls</p>
<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>
</section>
</CardFrame>
);
}
if (entities.length === 0) {
return (
<section className="panel-section space-y-0.5 text-sm text-slate-400">
<p className="panel-title-text">Room Controls</p>
<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>
</section>
</CardFrame>
);
}
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 (
<section className="panel-section 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>
<CardFrame title="Room Controls" meta={entities.length} actions={actions} bodyClassName="space-y-0.5 text-base">
{controlsLocked ? (
<p className="rounded border border-amber-600/60 bg-amber-900/40 px-1 py-0.5 text-xs text-amber-100">
{lockState === 'off'
@@ -315,7 +311,7 @@ export default function HomeAssistantControls() {
/>
))}
</div>
</section>
</CardFrame>
);
}
+10 -13
View File
@@ -3,30 +3,27 @@
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
import { useSessionSelector } from '../../context/SessionContext.jsx';
import { useMemo } from 'react';
import CardFrame from '../CardFrame/index.jsx';
export default function LogPanel() {
const logs = useSessionSelector((state) => state.logs);
const rendered = useMemo(() => logs.slice().reverse(), [logs]);
return (
<div className="panel-section 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>
<CardFrame title="Server logs" meta={logs.length} bodyClassName="space-y-0.5 text-base">
<div className="surface h-64 overflow-y-auto font-mono text-xs">
{logs.length === 0 ? (
<p>No logs yet.</p>
) : (
rendered.map((entry) => (
<div key={entry.id} className="surface">
<span className="text-amber-400">{entry.timestamp}</span>{' '}
<span className="text-lime-400">[{entry.level}]</span>{' '}
{entry.label && <span className="text-teal-400">[{entry.label}]</span>}{' '}
<span>{entry.message}</span>
</div>
))
<div key={entry.id} className="surface">
<span className="text-amber-400">{entry.timestamp}</span>{' '}
<span className="text-lime-400">[{entry.level}]</span>{' '}
{entry.label && <span className="text-teal-400">[{entry.label}]</span>}{' '}
<span>{entry.message}</span>
</div>
))
)}
</div>
</div>
</CardFrame>
);
}
@@ -1,5 +1,6 @@
import { useSessionActions, useSessionSelector } from '../../context/SessionContext.jsx';
import { useSettingsNamespace } from '../../settings/index.js';
import CardFrame from '../CardFrame/index.jsx';
export default function OverseerPreferencePanel() {
const { identifySession } = useSessionActions();
@@ -21,10 +22,7 @@ export default function OverseerPreferencePanel() {
};
return (
<section className="surface p-1 text-xs text-slate-200 text-center">
<div className="panel-title-row">
<span className="panel-title-text">Overseer vote</span>
</div>
<CardFrame title="Overseer vote" bodyClassName="space-y-0.5 text-center text-xs text-slate-200">
<label
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'
@@ -47,6 +45,6 @@ export default function OverseerPreferencePanel() {
{running ? 'Running!' : 'Stopped by vote.'}
</span>
</div>
</section>
</CardFrame>
);
}
@@ -5,6 +5,7 @@ import { useMemo } from 'react';
import { useSessionSelector } from '../../context/SessionContext.jsx';
import NicknameForm from '../NicknameForm/index.jsx';
import SocialButtonsGrid from '../SocialButtonsGrid/index.jsx';
import CardFrame from '../CardFrame/index.jsx';
function roleColors(role) {
switch (role) {
@@ -57,8 +58,13 @@ export default function RawUserPilePanel({
: 'h-48 overflow-y-auto';
return (
<section
className={`panel-section space-y-0.5 text-base ${fillHeight ? 'flex h-full min-h-0 flex-col overflow-hidden' : ''} ${className}`}
<CardFrame
title={!hideHeader ? 'Users' : ''}
meta={!hideHeader ? sorted.length : null}
hideHeader={hideHeader}
fillHeight={fillHeight}
className={className}
bodyClassName="space-y-0.5 text-base"
>
{!hideNicknameForm && (
<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' : ''}`}>
{!hideHeader && (
<div className="panel-title-row">
<span className="panel-title-text">Users</span>
<span className="panel-title-meta">{sorted.length}</span>
</div>
)}
<div
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>
</section>
</CardFrame>
);
}
@@ -5,6 +5,7 @@ import { useEffect, useMemo, useState } from 'react';
import { useSessionActions, useSessionSelector } from '../../context/SessionContext.jsx';
import { useSettingsNamespace } from '../../settings/index.js';
import { roverNameChromeStyle } from '../../lib/roverColor.js';
import CardFrame from '../CardFrame/index.jsx';
function normalizeSources(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' : '';
return (
<section className={`panel-section p-0.75 text-sm ${containerClass}`}>
<header className="panel-title-row">
<span className="panel-title-text">Replay Sources</span>
<span className="panel-title-meta">{sources.length}</span>
</header>
<CardFrame title="Replay Sources" meta={sources.length} fillHeight={fillHeight} bodyClassName="space-y-0.5 text-sm">
<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} />
@@ -201,7 +197,7 @@ export default function ReplaySourcesPanel({ panelId = 'replay-sources', fillHei
{error ? <div className="text-xs text-amber-400">{error}</div> : null}
{success ? <div className="text-xs text-emerald-300">{success}</div> : null}
</div>
</section>
</CardFrame>
);
}
+3 -2
View File
@@ -24,6 +24,7 @@ import { useSessionSelector } from '../../context/SessionContext.jsx';
import { useSettingsNamespace } from '../../settings/index.js';
import ButtonBoxPanel from '../ButtonBoxPanel/index.jsx';
import OverseerPreferencePanel from '../OverseerPreferencePanel/index.jsx';
import CardFrame from '../CardFrame/index.jsx';
import { useState } from 'react';
import { useManualDockAssist } from '../../features/manualDockAssist/useManualDockAssist.js';
@@ -35,11 +36,11 @@ function TopDownMapPanel() {
const sensors = frame?.sensors || {};
return (
<section className="panel-section">
<CardFrame hideHeader>
<div className="aspect-square w-full">
<TopDownMap sensors={sensors} />
</div>
</section>
</CardFrame>
);
}
+27 -29
View File
@@ -6,6 +6,7 @@ import { useSessionSelector } from '../../context/SessionContext.jsx';
import { useSettingsNamespace } from '../../settings/index.js';
import { useRoomCameraSnapshots } from '../../hooks/useRoomCameraSnapshots.js';
import RoomCameraFeed from '../RoomCameraFeed/index.jsx';
import CardFrame from '../CardFrame/index.jsx';
function EmptyState() {
return (
@@ -66,35 +67,32 @@ export default function RoomCameraPanel({
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 (
<section className="panel-section space-y-0.5 text-base">
{!hideHeader && (
<header className="panel-title-row">
<div className="panel-title-left">
<p className="panel-title-text">Room cameras</p>
<span className="panel-title-meta">{cameras.length}</span>
</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>
)}
<CardFrame
title="Room cameras"
meta={cameras.length}
actions={actions}
hideHeader={hideHeader}
bodyClassName="space-y-0.5 text-base"
>
<div className={containerClass}>
{cameras.map((camera) => {
const feed = feedMap[camera.id] || null;
@@ -109,6 +107,6 @@ export default function RoomCameraPanel({
);
})}
</div>
</section>
</CardFrame>
);
}
@@ -4,6 +4,7 @@
import { useEffect, useMemo, useState } from 'react';
import { useSessionActions, useSessionSelector } from '../../context/SessionContext.jsx';
import { roverNameChromeStyle } from '../../lib/roverColor.js';
import CardFrame from '../CardFrame/index.jsx';
function classNames(...values) {
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 };
return (
<div className="space-y-0.5 text-sm">
{title && (
<div className="panel-title-row">
<p className="panel-title-text">{title}</p>
</div>
)}
<CardFrame title={title} bodyClassName="space-y-0.5 text-sm">
{rosterItems.length === 0 ? (
<p className="text-sm text-slate-500">No rovers registered.</p>
) : (
@@ -220,6 +216,6 @@ export default function RoverQueuesPanel({ title = 'Rovers' }) {
})}
</ul>
)}
</div>
</CardFrame>
);
}
@@ -173,7 +173,7 @@ function SensorDetails({ sensors, dockState }) {
function DetailCard({ title, children }) {
return (
<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>
);
@@ -68,12 +68,6 @@ function TopDownMapContent({ sensors = {}, variant = 'full', size: overrideSize,
className={`${overlay ? 'relative' : 'surface relative p-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">
<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" />
+3 -5
View File
@@ -6,6 +6,7 @@ import { useSessionSelector } from '../../context/SessionContext.jsx';
import NicknameForm from '../NicknameForm/index.jsx';
import SocialButtonsGrid from '../SocialButtonsGrid/index.jsx';
import { roverBadgeStyle, roverNameChromeStyle } from '../../lib/roverColor.js';
import CardFrame from '../CardFrame/index.jsx';
export function NicknameEntryPanel({ compact = false }) {
return (
@@ -19,12 +20,9 @@ export function NicknameEntryPanel({ compact = false }) {
export function LinkButtonsPanel() {
return (
<section className="panel-section flex h-full min-h-0 flex-col gap-0.5 text-base">
<div className="panel-title-row">
<span className="panel-title-text">Socials</span>
</div>
<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" />
</section>
</CardFrame>
);
}
@@ -17,6 +17,7 @@ import {
import { mergeFloatChunks, encodeWavMono16 } from './audioCodec.js';
import StatusIndicator from './StatusIndicator.jsx';
import KeyPill from './KeyPill.jsx';
import CardFrame from '../../CardFrame/index.jsx';
export default function VipAudioUploadCard({
ownRoverId = '',
@@ -529,9 +530,8 @@ export default function VipAudioUploadCard({
);
return (
<section className="surface">
<CardFrame title="Audio Controls">
<div className="grid gap-1">
<p className="text-sm text-slate-100 text-center">Audio Controls</p>
<section className="surface">
<div className="flex items-center justify-center gap-0.5 py-0.25 text-xs text-slate-300">
<span>Push-to-Talk Key</span>
@@ -653,6 +653,6 @@ export default function VipAudioUploadCard({
</div>
</section>
</div>
</section>
</CardFrame>
);
}
+3 -3
View File
@@ -2,6 +2,7 @@
// 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.
import { useState } from 'react';
import CardFrame from '../CardFrame/index.jsx';
import { fieldClass, flowWrapClass, innerFlowClass, maskKey } from './constants.js';
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;
return (
<section className={`surface ${wrapClass}`}>
<CardFrame title="Identity key" className={wrapClass}>
<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>
<input
className={fieldClass}
@@ -109,6 +109,6 @@ export default function VipIdentityCard({ currentStoredKey, applyIdentityKey, on
</form>
) : null}
</div>
</section>
</CardFrame>
);
}
+15 -13
View File
@@ -2,6 +2,7 @@
// Purpose: Renders a shared lift controller panel synced from server session state.
// Scope: Presents verified-user controls while reflecting global busy/position/cooldown state.
import { useEffect, useMemo, useState } from 'react';
import CardFrame from '../CardFrame/index.jsx';
function badgeClass(tone) {
if (tone === 'good') return 'bg-emerald-600 text-white';
@@ -67,7 +68,16 @@ export default function VipLiftCard({ lift, onUp, onDown, fullWidth = false }) {
};
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 ? (
<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">
@@ -82,18 +92,10 @@ export default function VipLiftCard({ lift, onUp, onDown, fullWidth = false }) {
) : null}
</div>
</div>
) : null}
) : null}
<div className="grid gap-0.5">
<div className="relative flex items-center justify-center min-h-[1.5rem]">
<div className="text-center">
<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 className="text-center">
<p className="text-xs text-slate-400">Move the lift up and down. Please don't break anything...</p>
</div>
<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}
</div>
</section>
</CardFrame>
);
}
+14 -12
View File
@@ -2,6 +2,7 @@
// 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.
import { useState } from 'react';
import CardFrame from '../CardFrame/index.jsx';
function normalizeState(value) {
return String(value || '').trim();
@@ -98,18 +99,19 @@ export default function VipNeatoCard({
const primaryState = docked ? 'Docked' : uiStateLabel !== '--' ? uiStateLabel : 'Away from dock';
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="relative flex items-center justify-center min-h-[1.5rem]">
<div className="text-center">
<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 className="text-center">
<p className="text-xs text-slate-400">Control the autonomous Neato robovac. Be nice to him.</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-0.5">
@@ -217,6 +219,6 @@ export default function VipNeatoCard({
</p>
) : null}
</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.
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
import { useMemo, useState } from 'react';
import CardFrame from '../CardFrame/index.jsx';
import { flowWrapClass, innerFlowClass } from './constants.js';
export default function VipPrivateRoverAccessCard({
@@ -42,9 +43,8 @@ export default function VipPrivateRoverAccessCard({
};
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}>
<p className="text-sm text-slate-300">Private rover access requests</p>
{requestableRovers.length === 0 ? (
<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>
</section>
</CardFrame>
);
}
@@ -3,6 +3,7 @@
// Scope: Owns local URL validation UX and writes to the existing profile settings namespace.
import { useEffect, useMemo, useState } from 'react';
import { useSettingsNamespace } from '../../settings/index.js';
import CardFrame from '../CardFrame/index.jsx';
import { fieldClass, flowWrapClass, innerFlowClass } from './constants.js';
function normalizeHttpUrl(value) {
@@ -54,9 +55,8 @@ export default function VipProfileImageCard({ isVerified = false, fullWidth = fa
};
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}>
<p className="text-sm text-slate-300">Chat profile image URL</p>
<p className="text-xs text-slate-500">
Verified users can set a custom avatar for chat and Discord bridge messages.
</p>
@@ -86,6 +86,6 @@ export default function VipProfileImageCard({ isVerified = false, fullWidth = fa
</button>
</div>
</form>
</section>
</CardFrame>
);
}
@@ -3,6 +3,7 @@
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
import { useState } from 'react';
import NicknameForm from '../NicknameForm/index.jsx';
import CardFrame from '../CardFrame/index.jsx';
import { flowWrapClass, innerFlowClass, fieldClass } from './constants.js';
export default function VipVerificationCard({
@@ -61,29 +62,28 @@ export default function VipVerificationCard({
if (pendingRequestId) {
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>
</section>
</CardFrame>
);
}
if (requestFlowStep === 0) {
return (
<section className={`surface ${wrapClass}`}>
<CardFrame title="Verification" className={wrapClass}>
<div className={innerFlowClass}>
<p className="text-sm text-slate-300">Verification</p>
<button type="button" className="button-dark text-sm" onClick={beginRequestFlow} disabled={working}>
Request Verification
</button>
</div>
</section>
</CardFrame>
);
}
return (
<form className={`surface ${wrapClass}`} onSubmit={handleRequestSubmit}>
<CardFrame title="Request verification" className={wrapClass}>
<form onSubmit={handleRequestSubmit}>
<div className={innerFlowClass}>
<p className="text-sm text-slate-300">Request verification</p>
<p className="text-xs text-slate-500">
Step {requestFlowStep} of 3
</p>
@@ -189,6 +189,7 @@ export default function VipVerificationCard({
</div>
) : null}
</div>
</form>
</form>
</CardFrame>
);
}
-24
View File
@@ -71,30 +71,6 @@ body {
@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 {
@apply bg-neutral-800 text-white px-0.5 py-0.5 rounded-md;
}