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
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-title" content="Roomba Rover" />
<title>Roomba Rover</title>
<script type="module" crossorigin src="/assets/index-4eskKUF4.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-B8wCAHw2.css">
<script type="module" crossorigin src="/assets/index-BBFCmHB8.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Dva9v7lV.css">
</head>
<body>
<div id="root"></div>
@@ -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);
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">
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 className="panel-title-meta">On</span>
<span>On</span>
{onKeyLabel ? <KeyPill label={onKeyLabel} /> : null}
</span>
<span className="flex items-center gap-0.5">
<span className="panel-title-meta">Off</span>
<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'} />
</div>
</header>
</>
);
return (
<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>
);
}
+3 -6
View File
@@ -3,16 +3,13 @@
// 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>
@@ -27,6 +24,6 @@ export default function LogPanel() {
))
)}
</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>
);
}
+15 -17
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,18 +67,9 @@ export default function RoomCameraPanel({
return <EmptyState />;
}
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>
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
@@ -91,10 +83,16 @@ export default function RoomCameraPanel({
))}
</div>
</div>
)}
</div>
</header>
)}
) : null;
return (
<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>
);
}
+12 -10
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">
@@ -84,17 +94,9 @@ export default function VipLiftCard({ lift, onUp, onDown, fullWidth = false }) {
</div>
) : 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>
<section className="surface-muted px-0.5 py-0.5">
<div className="grid grid-cols-1 md:grid-cols-3 gap-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>
);
}
+13 -11
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}`}>
<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)}`}
>
<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="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>
@@ -190,5 +190,6 @@ export default function VipVerificationCard({
) : null}
</div>
</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;
}