colors!!!

This commit is contained in:
legop3
2026-05-21 03:02:29 -04:00
parent 331302da01
commit e9989da71a
21 changed files with 63 additions and 26 deletions
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -11,7 +11,7 @@
<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-BBFCmHB8.js"></script> <script type="module" crossorigin src="/assets/index-DqC5SblS.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Dva9v7lV.css"> <link rel="stylesheet" crossorigin href="/assets/index-Dva9v7lV.css">
</head> </head>
<body> <body>
@@ -127,7 +127,7 @@ export default function ButtonBoxPanel() {
}, [effectiveAlertVolume, socket]); }, [effectiveAlertVolume, socket]);
return ( return (
<CardFrame title="Button Box" bodyClassName="space-y-0.5 text-base"> <CardFrame title="Button Box" accent="#eab308" 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);
+32
View File
@@ -2,10 +2,33 @@ function cx(...values) {
return values.filter(Boolean).join(' '); return values.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 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 = '',
@@ -14,6 +37,13 @@ export default function CardFrame({
children, children,
}) { }) {
const showHeader = !hideHeader && (title || meta != null || actions); const showHeader = !hideHeader && (title || meta != null || actions);
const accentRgb = hexToRgb(accent);
const cardStyle = accentRgb ? { borderColor: rgba(accentRgb, 0.65) } : 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 ( return (
<section <section
@@ -22,6 +52,7 @@ export default function CardFrame({
fillHeight && 'flex h-full min-h-0 flex-col', fillHeight && 'flex h-full min-h-0 flex-col',
className, className,
)} )}
style={cardStyle}
> >
{showHeader ? ( {showHeader ? (
<header <header
@@ -29,6 +60,7 @@ export default function CardFrame({
'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-600/45 bg-gradient-to-r from-neutral-900 via-neutral-800 to-neutral-700 px-0.5 py-0.5',
headerClassName, headerClassName,
)} )}
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-100">{title}</p> : null}
+1
View File
@@ -109,6 +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"
@@ -256,7 +256,7 @@ export default function HomeAssistantControls() {
if (!ha?.enabled) { if (!ha?.enabled) {
return ( return (
<CardFrame title="Room Controls" bodyClassName="space-y-0.5 text-sm text-slate-400"> <CardFrame title="Room Controls" accent="#14b8a6" 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" bodyClassName="space-y-0.5 text-sm text-slate-400"> <CardFrame title="Room Controls" accent="#14b8a6" 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" meta={entities.length} actions={actions} bodyClassName="space-y-0.5 text-base"> <CardFrame title="Room Controls" meta={entities.length} accent="#14b8a6" 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" meta={logs.length} bodyClassName="space-y-0.5 text-base"> <CardFrame title="Server logs" meta={logs.length} accent="#f97316" 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" bodyClassName="space-y-0.5 text-center text-xs text-slate-200"> <CardFrame title="Overseer vote" accent="#10b981" 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'
@@ -61,6 +61,7 @@ export default function RawUserPilePanel({
<CardFrame <CardFrame
title={!hideHeader ? 'Users' : ''} title={!hideHeader ? 'Users' : ''}
meta={!hideHeader ? sorted.length : null} meta={!hideHeader ? sorted.length : null}
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" meta={sources.length} fillHeight={fillHeight} bodyClassName="space-y-0.5 text-sm"> <CardFrame title="Replay Sources" meta={sources.length} accent="#06b6d4" 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> <CardFrame hideHeader accent="#0067f7">
<div className="aspect-square w-full"> <div className="aspect-square w-full">
<TopDownMap sensors={sensors} /> <TopDownMap sensors={sensors} />
</div> </div>
@@ -89,6 +89,7 @@ export default function RoomCameraPanel({
<CardFrame <CardFrame
title="Room cameras" title="Room cameras"
meta={cameras.length} meta={cameras.length}
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} bodyClassName="space-y-0.5 text-sm"> <CardFrame title={title} accent="#f59e0b" 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>
) : ( ) : (
+1 -1
View File
@@ -20,7 +20,7 @@ export function NicknameEntryPanel({ compact = false }) {
export function LinkButtonsPanel() { export function LinkButtonsPanel() {
return ( return (
<CardFrame title="Socials" fillHeight bodyClassName="flex flex-1 min-h-0 flex-col gap-0.5 text-base"> <CardFrame title="Socials" accent="#3030f5" 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"> <CardFrame title="Audio Controls" accent="#a78bfa">
<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" className={wrapClass}> <CardFrame title="Identity key" accent="#60a5fa" 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
View File
@@ -70,6 +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={
@@ -101,6 +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" className={wrapClass} bodyClassName="text-sm text-slate-300"> <CardFrame title="Private rover access requests" accent="#f43f5e" 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" className={wrapClass} bodyClassName="text-sm text-slate-300"> <CardFrame title="Chat profile image URL" accent="#f59e0b" 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" className={wrapClass} bodyClassName="text-sm text-slate-300"> <CardFrame title="Verification" accent="#eab308" 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" className={wrapClass}> <CardFrame title="Verification" accent="#eab308" 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" className={wrapClass}> <CardFrame title="Request verification" accent="#eab308" 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">