mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
new themeses!!
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -78,8 +78,8 @@
|
||||
<script defer src="https://analytics.otter.land/script.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land"></script>
|
||||
<script defer src="https://analytics.otter.land/recorder.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land" data-sample-rate="0.15" data-mask-level="moderate" data-max-duration="300000"></script>
|
||||
<title>Roomba Rover</title>
|
||||
<script type="module" crossorigin src="/assets/index-IknCIkGO.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-9ecW8jwy.css">
|
||||
<script type="module" crossorigin src="/assets/index-DC2T_UQE.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BHMHSpML.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
1. interinstance UI updates
|
||||
1. on desktop, scale up the overlay UI, like actually scale like 1.5x
|
||||
2. update the rover queues panel so that instances are always expanded
|
||||
1. have a "scroll for more" thing
|
||||
2. make it so that the list takes up all the vertical space
|
||||
2. assign rovers based on battery percentage, give people highest one
|
||||
3. add admin ui for VIP and private requests instead of only through discord
|
||||
4. add more background gap themes
|
||||
5. fix this:
|
||||
1. assign rovers based on battery percentage, give people highest one
|
||||
2. add admin ui for VIP and private requests instead of only through discord
|
||||
3. add more background gap themes
|
||||
4. fix this:
|
||||
`Jun 18 15:14:18 roombaserver.local node[216731]: /home/daniel/MultiRoombaRover/server/src/services/roverManager/socketHandlers.js:92
|
||||
Jun 18 15:14:18 roombaserver.local node[216731]: cb({ error: err.message });
|
||||
Jun 18 15:14:18 roombaserver.local node[216731]: ^
|
||||
|
||||
+12
-1
@@ -51,7 +51,12 @@ import NeatoCard from './components/NeatoCard/index.jsx';
|
||||
import RewardRunOverlay from './components/RewardRunOverlay/index.jsx';
|
||||
import SocketConnectionPill from './components/SocketConnectionPill/index.jsx';
|
||||
import DuplicateIdentityOverlay from './components/DuplicateIdentityOverlay/index.jsx';
|
||||
import { pageBackgroundClass, themeGapClass, themeStackClass } from './themeFlags.js';
|
||||
import {
|
||||
DEFAULT_PAGE_THEME_KEY,
|
||||
getPageThemeClass,
|
||||
themeGapClass,
|
||||
themeStackClass,
|
||||
} from './themes/index.js';
|
||||
import { trackAnalyticsEvent } from './analytics/index.js';
|
||||
import useLayoutMode from './hooks/useLayoutMode.js';
|
||||
|
||||
@@ -280,6 +285,12 @@ function App() {
|
||||
const layout = useLayoutMode();
|
||||
const isDesktop = layout === 'desktop';
|
||||
const fullscreen = useFullscreenPrompt(layout);
|
||||
const { value: pageSettings } = useSettingsNamespace('page', {
|
||||
backgroundTheme: DEFAULT_PAGE_THEME_KEY,
|
||||
});
|
||||
// Resolve the cookie value through the shared catalog before painting the page. This prevents
|
||||
// an obsolete or hand-edited key from stripping the background class from every exposed seam.
|
||||
const pageBackgroundClass = getPageThemeClass(pageSettings?.backgroundTheme);
|
||||
|
||||
return (
|
||||
<div className={`${pageBackgroundClass} text-slate-100 ${isDesktop ? 'h-screen overflow-hidden' : 'ios-safe-screen min-h-screen'}`}>
|
||||
|
||||
@@ -23,6 +23,8 @@ import { usePtzCameraSnapshots } from '../../hooks/usePtzCameraSnapshot.js';
|
||||
import { useSharedClock } from '../../hooks/useSharedClock.js';
|
||||
import { isFeatureEnabled } from '../../lib/features.js';
|
||||
import { trackAnalyticsEvent } from '../../analytics/index.js';
|
||||
import { useSettingsNamespace } from '../../settings/index.js';
|
||||
import { DEFAULT_PAGE_THEME_KEY, getPageThemeClass } from '../../themes/index.js';
|
||||
|
||||
const PTZ_DEFAULT_COLOR = '#38bdf8';
|
||||
|
||||
@@ -562,7 +564,10 @@ function PtzDesktopFullscreen({ ptz, releasePending }) {
|
||||
<main className="min-h-0 shrink-0 overflow-hidden bg-black" style={{ aspectRatio: '16 / 9' }}>
|
||||
<PtzMediaPane ptz={ptz} open framed />
|
||||
</main>
|
||||
<aside className="flex min-h-0 min-w-56 flex-1 flex-col gap-0.5 overflow-y-auto bg-neutral-950 text-sm">
|
||||
{/* Keep the sidebar itself transparent. Its child cards still own their dark surfaces,
|
||||
while the shared PTZ page theme can show through the same compact gaps as the driver
|
||||
layout instead of being covered by one solid sidebar rectangle. */}
|
||||
<aside className="flex min-h-0 min-w-56 flex-1 flex-col gap-0.5 overflow-y-auto text-sm">
|
||||
<PtzQueueSummary ptz={ptz} />
|
||||
{ptz?.isOperator ? (
|
||||
<PtzLightingControls ptz={ptz} />
|
||||
@@ -717,9 +722,16 @@ export function PtzControllerPage({ layout = 'desktop' }) {
|
||||
const routeExitReleaseTimerRef = useRef(null);
|
||||
const participantRef = useRef(false);
|
||||
const closingThroughButtonRef = useRef(false);
|
||||
const { value: pageSettings } = useSettingsNamespace('page', {
|
||||
backgroundTheme: DEFAULT_PAGE_THEME_KEY,
|
||||
});
|
||||
const isMobile = layout !== 'desktop';
|
||||
const canUse = Boolean(ptz?.canUse || isVerified || role === 'admin' || role === 'lockdown');
|
||||
const isParticipant = Boolean(ptz?.isOperator || ptz?.queuedPosition);
|
||||
// PTZ is a separate route but shares the browser's page settings. Applying the catalog class to
|
||||
// its body surface exposes the theme only through layout padding and card gaps; camera pixels,
|
||||
// controls, and card interiors retain their purpose-built dark backgrounds.
|
||||
const pageBackgroundClass = getPageThemeClass(pageSettings?.backgroundTheme);
|
||||
|
||||
useEffect(() => {
|
||||
// Route-exit cleanup runs after the last render, so retain the latest
|
||||
@@ -825,7 +837,7 @@ export function PtzControllerPage({ layout = 'desktop' }) {
|
||||
|
||||
if (!featureEnabled) {
|
||||
return (
|
||||
<main className="flex min-h-[100dvh] items-center justify-center bg-black p-2 text-slate-100">
|
||||
<main className={`flex min-h-[100dvh] items-center justify-center p-2 text-slate-100 ${pageBackgroundClass}`}>
|
||||
<CardFrame title="PTZ camera" bodyClassName="space-y-1 p-2 text-sm">
|
||||
<p>The PTZ camera is not available.</p>
|
||||
<button type="button" className="button-dark w-full" onClick={() => navigate('/')}>Return to driver page</button>
|
||||
@@ -835,7 +847,10 @@ export function PtzControllerPage({ layout = 'desktop' }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="h-[100dvh] w-full overflow-hidden bg-black text-slate-100">
|
||||
<main className={`h-[100dvh] w-full overflow-hidden text-slate-100 ${pageBackgroundClass}`}>
|
||||
{/* The fullscreen CardFrame remains the structural shell. Painting its otherwise
|
||||
transparent body is what lets every desktop and mobile PTZ composition share one
|
||||
continuous pattern without threading theme props into each individual child panel. */}
|
||||
<CardFrame
|
||||
title={isMobile ? '' : ptz?.name || 'PTZ Camera'}
|
||||
actions={isMobile ? null : (
|
||||
@@ -847,7 +862,7 @@ export function PtzControllerPage({ layout = 'desktop' }) {
|
||||
fillHeight
|
||||
clipOverflow={false}
|
||||
className="h-[100dvh] w-[100vw] rounded-none border-0 !bg-black"
|
||||
bodyClassName="relative min-h-0 flex-1"
|
||||
bodyClassName={`relative min-h-0 flex-1 ${pageBackgroundClass}`}
|
||||
>
|
||||
{isMobile ? (
|
||||
layout === 'mobile-landscape' ? (
|
||||
|
||||
@@ -34,7 +34,7 @@ import OverseerPreferencePanel from '../OverseerPreferencePanel/index.jsx';
|
||||
import CardFrame from '../CardFrame/index.jsx';
|
||||
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useManualDockAssist } from '../../features/manualDockAssist/useManualDockAssist.js';
|
||||
import { themeGapClass, themeStackClass } from '../../themeFlags.js';
|
||||
import { themeGapClass, themeStackClass } from '../../themes/index.js';
|
||||
import { trackAnalyticsEvent } from '../../analytics/index.js';
|
||||
|
||||
const CHAT_DOCK_INITIAL_HEIGHT = 224;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Settings Panel
|
||||
// Purpose: Defines the Settings Panel 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 } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useControlActions, useControlSelector } from '../../controls/index.js';
|
||||
import AuthPanel from '../AuthPanel/index.jsx';
|
||||
import AdminPanel from '../AdminPanel/index.jsx';
|
||||
@@ -19,6 +19,13 @@ import { useSocket } from '../../context/SocketContext.jsx';
|
||||
import { AUDIO_SETTINGS_DEFAULTS, VIDEO_SETTINGS_DEFAULTS } from '../../settings/namespaces.js';
|
||||
import { formatKeyLabel } from '../../controls/keymapUtils.js';
|
||||
import { trackAnalyticsEvent, trackAnalyticsEventThrottled } from '../../analytics/index.js';
|
||||
import {
|
||||
DEFAULT_PAGE_THEME_KEY,
|
||||
PAGE_THEME_OPTIONS,
|
||||
getPageTheme,
|
||||
getPageThemeClass,
|
||||
normalizePageThemeKey,
|
||||
} from '../../themes/index.js';
|
||||
|
||||
const manualTabs = [
|
||||
{ key: 'start', label: 'Start OI' },
|
||||
@@ -66,6 +73,24 @@ function SettingHelp({ children }) {
|
||||
return <p className="mx-auto w-full max-w-lg text-xs leading-snug text-white">{children}</p>;
|
||||
}
|
||||
|
||||
function ThemePreviewCard({ title, className = '' }) {
|
||||
// Use the production CardFrame rather than a lookalike rectangle. This makes the demonstration
|
||||
// honest about borders, opaque card surfaces, and the exact amount of theme visible in a gap.
|
||||
return (
|
||||
<CardFrame
|
||||
title={title}
|
||||
className={className}
|
||||
bodyClassName="flex min-h-0 flex-1 flex-col justify-center gap-1 p-1"
|
||||
fillHeight
|
||||
>
|
||||
{/* Neutral placeholder lines suggest real panel content without making the preview look like
|
||||
an interactive control surface or tying it to any one driver/PTZ layout. */}
|
||||
<div className="h-1.5 w-4/5 rounded-full bg-neutral-600/80" />
|
||||
<div className="h-1.5 w-3/5 rounded-full bg-neutral-700/90" />
|
||||
</CardFrame>
|
||||
);
|
||||
}
|
||||
|
||||
function RangeSetting({ label, value, disabled = false, onChange }) {
|
||||
// Range settings need enough horizontal room for accurate pointer input, so the slider spans
|
||||
// the row while the percentage value stays beside the label for quick feedback.
|
||||
@@ -116,6 +141,7 @@ export default function SettingsPanel() {
|
||||
swapMobileControlColumns: false,
|
||||
driveMacroBackoffEnabled: true,
|
||||
interInstanceTransferSettings: true,
|
||||
backgroundTheme: DEFAULT_PAGE_THEME_KEY,
|
||||
});
|
||||
const { value: audioSettings, save: saveAudioSettings } = useSettingsNamespace('audio', AUDIO_SETTINGS_DEFAULTS);
|
||||
const { value: videoSettings, save: saveVideoSettings } = useSettingsNamespace('video', VIDEO_SETTINGS_DEFAULTS);
|
||||
@@ -126,6 +152,11 @@ export default function SettingsPanel() {
|
||||
? pageSettings.driveMacroBackoffEnabled
|
||||
: true;
|
||||
const interInstanceTransferSettings = pageSettings?.interInstanceTransferSettings !== false;
|
||||
const savedPageThemeKey = normalizePageThemeKey(pageSettings?.backgroundTheme);
|
||||
const [previewPageThemeKey, setPreviewPageThemeKey] = useState(savedPageThemeKey);
|
||||
const previewPageTheme = getPageTheme(previewPageThemeKey);
|
||||
const savedPageTheme = getPageTheme(savedPageThemeKey);
|
||||
const hasUnsavedPageTheme = previewPageThemeKey !== savedPageThemeKey;
|
||||
const masterVolume = Number.isFinite(audioSettings?.masterVolume) ? audioSettings.masterVolume : AUDIO_SETTINGS_DEFAULTS.masterVolume;
|
||||
const alertVolume = Number.isFinite(audioSettings?.alertVolume) ? audioSettings.alertVolume : AUDIO_SETTINGS_DEFAULTS.alertVolume;
|
||||
const roverVolume = Number.isFinite(audioSettings?.roverVolume) ? audioSettings.roverVolume : AUDIO_SETTINGS_DEFAULTS.roverVolume;
|
||||
@@ -141,6 +172,13 @@ export default function SettingsPanel() {
|
||||
const videoColorFilter = normalizeVideoFilter(videoSettings?.colorFilter);
|
||||
const videoFilterCycleKeyLabel = formatKeyLabel(keymap?.videoFilterCycle?.[0]);
|
||||
|
||||
useEffect(() => {
|
||||
// Settings load after the provider mounts and can also be replaced by an incoming inter-instance
|
||||
// transfer. Resynchronize only when the persisted key changes; browsing the local preview does
|
||||
// not touch pageSettings, so Previous/Next choices are not accidentally reset.
|
||||
setPreviewPageThemeKey(savedPageThemeKey);
|
||||
}, [savedPageThemeKey]);
|
||||
|
||||
const sensorButtons = useMemo(
|
||||
() => [
|
||||
{ key: 'start', label: 'Enable stream', enable: true },
|
||||
@@ -202,6 +240,32 @@ export default function SettingsPanel() {
|
||||
trackAnalyticsEvent('settings_change', { setting: 'interInstanceTransferSettings', value: checked });
|
||||
};
|
||||
|
||||
const movePageThemePreview = (direction) => {
|
||||
const currentIndex = PAGE_THEME_OPTIONS.findIndex((theme) => theme.key === previewPageThemeKey);
|
||||
const safeIndex = currentIndex >= 0 ? currentIndex : 0;
|
||||
// Theme browsing wraps in both directions so the Back and Next buttons remain useful at the
|
||||
// ends of the catalog instead of forcing the user to reverse through every previous option.
|
||||
const nextIndex = (safeIndex + direction + PAGE_THEME_OPTIONS.length) % PAGE_THEME_OPTIONS.length;
|
||||
setPreviewPageThemeKey(PAGE_THEME_OPTIONS[nextIndex].key);
|
||||
};
|
||||
|
||||
const handlePageThemeSelect = (event) => {
|
||||
setPreviewPageThemeKey(normalizePageThemeKey(event.target.value));
|
||||
};
|
||||
|
||||
const handlePageThemeSave = () => {
|
||||
// Browsing is intentionally local. Persist only this explicit choice so opening Page settings
|
||||
// and experimenting with patterns cannot unexpectedly alter the driver or PTZ page background.
|
||||
savePageSettings((current) => ({
|
||||
...(current ?? {}),
|
||||
backgroundTheme: previewPageThemeKey,
|
||||
}));
|
||||
trackAnalyticsEvent('settings_change', {
|
||||
setting: 'backgroundTheme',
|
||||
value: previewPageThemeKey,
|
||||
});
|
||||
};
|
||||
|
||||
const handleVideoFilterChange = (event) => {
|
||||
const nextFilter = normalizeVideoFilter(event.target.value);
|
||||
|
||||
@@ -238,6 +302,73 @@ export default function SettingsPanel() {
|
||||
stretched column. Audio spans both columns because volume sliders need extra width
|
||||
for comfortable pointer control. */}
|
||||
<div className="grid gap-1.5 lg:grid-cols-2">
|
||||
<CardFrame
|
||||
title="Background theme"
|
||||
className="lg:col-span-2"
|
||||
bodyClassName="space-y-1.5 p-1 text-sm"
|
||||
>
|
||||
{/* Back/Next provide fast visual browsing, while the dropdown remains the direct
|
||||
route to a known theme in a growing catalog. Neither control saves implicitly. */}
|
||||
<div className="grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-1.5">
|
||||
<button
|
||||
type="button"
|
||||
className="button-dark min-w-16 text-sm"
|
||||
onClick={() => movePageThemePreview(-1)}
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
<select
|
||||
aria-label="Preview background theme"
|
||||
value={previewPageThemeKey}
|
||||
onChange={handlePageThemeSelect}
|
||||
className="field-input min-w-0 px-1 py-0.5 text-sm"
|
||||
>
|
||||
{PAGE_THEME_OPTIONS.map((theme) => (
|
||||
<option key={theme.key} value={theme.key}>
|
||||
{theme.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
className="button-dark min-w-16 text-sm"
|
||||
onClick={() => movePageThemePreview(1)}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* This miniature layout contains both a full-height vertical seam and horizontal
|
||||
seams between stacked cards. It exercises the exact gap directions the artwork
|
||||
must serve on the driver and PTZ pages. */}
|
||||
<div
|
||||
className={`page-theme-preview grid h-32 grid-cols-[minmax(0,1.15fr)_minmax(0,0.85fr)_minmax(0,0.85fr)] grid-rows-2 gap-0.5 overflow-hidden rounded border border-neutral-500/70 p-0.5 ${getPageThemeClass(previewPageThemeKey)}`}
|
||||
>
|
||||
<ThemePreviewCard title="Video" className="row-span-2" />
|
||||
<ThemePreviewCard title="Controls" />
|
||||
<ThemePreviewCard title="Queue" />
|
||||
<ThemePreviewCard title="Chat" className="col-span-2" />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center justify-between gap-1.5">
|
||||
<p className="min-w-0 text-xs text-white">
|
||||
Previewing <span className="font-semibold">{previewPageTheme.label}</span>
|
||||
{hasUnsavedPageTheme ? (
|
||||
<span className="text-slate-300">; saved theme is {savedPageTheme.label}.</span>
|
||||
) : (
|
||||
<span className="text-slate-300">; this is your saved theme.</span>
|
||||
)}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
className="button-dark min-w-24 text-sm disabled:cursor-default disabled:opacity-45"
|
||||
disabled={!hasUnsavedPageTheme}
|
||||
onClick={handlePageThemeSave}
|
||||
>
|
||||
Save theme
|
||||
</button>
|
||||
</div>
|
||||
</CardFrame>
|
||||
<CardFrame title="HUD" bodyClassName="space-y-1 p-1 text-sm">
|
||||
<SettingRow className="grid-cols-[auto_minmax(0,1fr)] max-[420px]:grid-cols-[auto_minmax(0,1fr)]">
|
||||
<input
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// 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.
|
||||
import { createContext, useCallback, useContext, useMemo, useState, useEffect } from 'react';
|
||||
import { themeGapClass, themeStackClass } from '../../themeFlags.js';
|
||||
import { themeGapClass, themeStackClass } from '../../themes/index.js';
|
||||
|
||||
const TabsContext = createContext(null);
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@ import CardFrame from '../components/CardFrame/index.jsx';
|
||||
import SocketConnectionPill from '../components/SocketConnectionPill/index.jsx';
|
||||
import { useSessionSelector } from '../context/SessionContext.jsx';
|
||||
import useUserIdentitySync from '../hooks/useUserIdentitySync.js';
|
||||
import { pageBackgroundClass } from '../themeFlags.js';
|
||||
import { useSettingsNamespace } from '../settings/index.js';
|
||||
import { DEFAULT_PAGE_THEME_KEY, getPageThemeClass } from '../themes/index.js';
|
||||
import IdentityDatabasePanel from './IdentityDatabasePanel.jsx';
|
||||
|
||||
function isLockdownAdminRole(role) {
|
||||
@@ -17,6 +18,12 @@ export default function DatabaseAdminApp() {
|
||||
useUserIdentitySync({ identitySurface: 'passive' });
|
||||
const role = useSessionSelector((state) => state.session?.role || null);
|
||||
const connected = useSessionSelector((state) => state.connected);
|
||||
const { value: pageSettings } = useSettingsNamespace('page', {
|
||||
backgroundTheme: DEFAULT_PAGE_THEME_KEY,
|
||||
});
|
||||
// Database cards use the same narrow seams as the driver page, so honoring the shared browser
|
||||
// preference here keeps the existing route-level background behavior while making it dynamic.
|
||||
const pageBackgroundClass = getPageThemeClass(pageSettings?.backgroundTheme);
|
||||
|
||||
const isLockdownAdmin = isLockdownAdminRole(role);
|
||||
const isLoggedInAdmin = role === 'admin' || isLockdownAdmin;
|
||||
|
||||
@@ -92,26 +92,6 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
.pride-page-bg {
|
||||
background-color: #050505;
|
||||
background-image:
|
||||
linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0)),
|
||||
repeating-linear-gradient(
|
||||
45deg,
|
||||
#5bcefa 0 1.818%,
|
||||
#f5a9b8 1.818% 3.636%,
|
||||
#ffffff 3.636% 5.455%,
|
||||
#f5a9b8 5.455% 7.273%,
|
||||
#5bcefa 7.273% 9.091%,
|
||||
#e40303 9.091% 10.909%,
|
||||
#ff8c00 10.909% 12.727%,
|
||||
#ffed00 12.727% 14.545%,
|
||||
#00a651 14.545% 16.364%,
|
||||
#0066ff 16.364% 18.182%,
|
||||
#9b2fae 18.182% 20%
|
||||
);
|
||||
background-attachment: fixed;
|
||||
}
|
||||
|
||||
.panel {
|
||||
@apply bg-black text-white p-0 rounded-md;
|
||||
|
||||
@@ -4,6 +4,9 @@ import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { BrowserRouter, Route, Routes } from 'react-router-dom'
|
||||
import './index.css'
|
||||
// Theme artwork is a separate style concern from global component utilities. Loading its dedicated
|
||||
// entrypoint here keeps every route consistent without returning theme definitions to index.css.
|
||||
import './themes/styles/index.css'
|
||||
import App from './App.jsx'
|
||||
import { SocketProvider } from './context/SocketContext.jsx'
|
||||
import { SessionProvider } from './context/SessionContext.jsx'
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
export const PRIDE_THEME_ENABLED = true;
|
||||
|
||||
export const pageBackgroundClass = PRIDE_THEME_ENABLED ? 'pride-page-bg' : 'bg-black';
|
||||
// export const themeGapClass = PRIDE_THEME_ENABLED ? 'gap-1' : 'gap-0.5';
|
||||
// export const themeStackClass = PRIDE_THEME_ENABLED ? 'space-y-1' : 'space-y-0.5';
|
||||
|
||||
// permanent smaller gaps i think is better probably
|
||||
export const themeGapClass = 'gap-0.5';
|
||||
export const themeStackClass = 'space-y-0.5';
|
||||
@@ -0,0 +1,55 @@
|
||||
// Page Theme Catalog
|
||||
// Purpose: Defines persisted theme identities and their presentation order.
|
||||
// Scope: Shared by the settings picker and every route that resolves a saved page theme.
|
||||
|
||||
export const DEFAULT_PAGE_THEME_KEY = 'progress-pride';
|
||||
|
||||
export const PAGE_THEME_OPTIONS = [
|
||||
// Stable keys are stored in the browser settings cookie. Keeping presentation labels separate
|
||||
// lets the UI wording improve later without invalidating anybody's saved preference.
|
||||
{ key: 'progress-pride', label: 'Progress pride', className: 'page-theme-progress-pride' },
|
||||
// Keep the high-contrast caution-stripe option near the default so it is reachable with one
|
||||
// Next click as well as directly through the dropdown.
|
||||
{ key: 'hazard-stripes', label: 'Hazard stripes', className: 'page-theme-hazard-stripes' },
|
||||
{ key: 'pride-mix', label: 'Pride mix', className: 'page-theme-pride-mix' },
|
||||
{ key: 'rainbow', label: 'Rainbow', className: 'page-theme-rainbow' },
|
||||
{ key: 'transgender', label: 'Transgender', className: 'page-theme-transgender' },
|
||||
{ key: 'bisexual', label: 'Bisexual', className: 'page-theme-bisexual' },
|
||||
{ key: 'lesbian', label: 'Lesbian', className: 'page-theme-lesbian' },
|
||||
{ key: 'nonbinary', label: 'Nonbinary', className: 'page-theme-nonbinary' },
|
||||
{ key: 'pansexual', label: 'Pansexual', className: 'page-theme-pansexual' },
|
||||
{ key: 'asexual', label: 'Asexual', className: 'page-theme-asexual' },
|
||||
{ key: 'aurora', label: 'Aurora', className: 'page-theme-aurora' },
|
||||
{ key: 'synthwave-grid', label: 'Synthwave grid', className: 'page-theme-synthwave-grid' },
|
||||
{ key: 'neon-checker', label: 'Neon checker', className: 'page-theme-neon-checker' },
|
||||
{ key: 'ocean-current', label: 'Ocean current', className: 'page-theme-ocean-current' },
|
||||
{ key: 'ember-lattice', label: 'Ember lattice', className: 'page-theme-ember-lattice' },
|
||||
{ key: 'starfield', label: 'Starfield', className: 'page-theme-starfield' },
|
||||
{ key: 'vaporwave-sunset', label: 'Vaporwave sunset', className: 'page-theme-vaporwave-sunset' },
|
||||
{ key: 'electric-circuit', label: 'Electric circuit', className: 'page-theme-electric-circuit' },
|
||||
{ key: 'lava-flow', label: 'Lava flow', className: 'page-theme-lava-flow' },
|
||||
{ key: 'deep-space-nebula', label: 'Deep-space nebula', className: 'page-theme-deep-space-nebula' },
|
||||
{ key: 'holographic-waves', label: 'Holographic waves', className: 'page-theme-holographic-waves' },
|
||||
{ key: 'mint-mosaic', label: 'Mint mosaic', className: 'page-theme-mint-mosaic' },
|
||||
{ key: 'candy-swirl', label: 'Candy swirl', className: 'page-theme-candy-swirl' },
|
||||
{ key: 'black', label: 'Black', className: 'page-theme-black' },
|
||||
];
|
||||
|
||||
export function normalizePageThemeKey(value) {
|
||||
// Cookie contents can outlive catalog changes or be edited by hand. Always resolve them to a
|
||||
// known entry so the page, preview, and dropdown cannot disagree about the active selection.
|
||||
return PAGE_THEME_OPTIONS.some((theme) => theme.key === value)
|
||||
? value
|
||||
: DEFAULT_PAGE_THEME_KEY;
|
||||
}
|
||||
|
||||
export function getPageTheme(value) {
|
||||
const normalizedKey = normalizePageThemeKey(value);
|
||||
return PAGE_THEME_OPTIONS.find((theme) => theme.key === normalizedKey) || PAGE_THEME_OPTIONS[0];
|
||||
}
|
||||
|
||||
export function getPageThemeClass(value) {
|
||||
// The shared base class owns page-level behavior such as fixed positioning. The modifier only
|
||||
// supplies artwork, which also lets the settings demonstration reuse the exact same theme.
|
||||
return `page-theme ${getPageTheme(value).className}`;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Theme Public API
|
||||
// Purpose: Gives UI consumers one stable import path for catalog resolution and layout constants.
|
||||
// Scope: Re-exports theme behavior while CSS remains independently loaded by the app entrypoint.
|
||||
|
||||
export {
|
||||
DEFAULT_PAGE_THEME_KEY,
|
||||
PAGE_THEME_OPTIONS,
|
||||
getPageTheme,
|
||||
getPageThemeClass,
|
||||
normalizePageThemeKey,
|
||||
} from './catalog.js';
|
||||
export { themeGapClass, themeStackClass } from './layout.js';
|
||||
@@ -0,0 +1,6 @@
|
||||
// Theme-Aware Layout Constants
|
||||
// Purpose: Keeps the narrow gap geometry used to reveal page themes consistent across layouts.
|
||||
// Scope: Layout-only values; selecting a theme never changes panel dimensions or input placement.
|
||||
|
||||
export const themeGapClass = 'gap-0.5';
|
||||
export const themeStackClass = 'space-y-0.5';
|
||||
@@ -0,0 +1,26 @@
|
||||
/* Page Theme Surfaces
|
||||
* Purpose: Defines behavior shared by every page theme and by the settings demonstration.
|
||||
* Scope: Canvas positioning and the explicit no-artwork fallback; individual artwork lives elsewhere.
|
||||
*/
|
||||
|
||||
/* These selectors intentionally remain ordinary CSS. This stylesheet is processed separately from
|
||||
* index.css, so placing them in a Tailwind layer would require duplicating Tailwind's directives. */
|
||||
.page-theme {
|
||||
/* A fixed canvas makes separated card gaps look like windows onto one continuous design.
|
||||
Individual theme classes below supply only their artwork and fallback color. */
|
||||
background-color: #050505;
|
||||
background-attachment: fixed;
|
||||
}
|
||||
|
||||
.page-theme-preview {
|
||||
/* Preview artwork belongs to the demonstration box, not the browser viewport. Overriding the
|
||||
page behavior here keeps the swatch representative while it moves inside a scroll panel. */
|
||||
background-attachment: scroll;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.page-theme-black {
|
||||
/* An explicit quiet option belongs in the catalog rather than being a special-case toggle. */
|
||||
background-color: #000000;
|
||||
background-image: none;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/* Page Theme Stylesheet Entry
|
||||
* Purpose: Loads theme surfaces and artwork as one explicit bundle from main.jsx.
|
||||
* Scope: Import order keeps shared behavior first, followed by the two artwork families.
|
||||
*/
|
||||
|
||||
@import './base.css';
|
||||
@import './pride.css';
|
||||
@import './patterns.css';
|
||||
@@ -0,0 +1,207 @@
|
||||
/* Pattern Page Themes
|
||||
* Purpose: Contains decorative non-pride artwork optimized for narrow horizontal and vertical gaps.
|
||||
* Scope: Static gradients and repeating patterns only; theme identity and ordering live in catalog.js.
|
||||
*/
|
||||
|
||||
/* Ordinary CSS keeps the artwork independent of Tailwind's global component layer. main.jsx loads
|
||||
* this theme bundle after index.css, giving the theme modifiers their original cascade position. */
|
||||
.page-theme-aurora {
|
||||
/* Overlapping oversized glows create slow color movement across the page without animation,
|
||||
avoiding distracting motion and continuous painting behind live video. */
|
||||
background-color: #03120f;
|
||||
background-image:
|
||||
radial-gradient(circle at 18% 24%, rgba(45, 212, 191, 0.95), transparent 34%),
|
||||
radial-gradient(circle at 76% 18%, rgba(168, 85, 247, 0.9), transparent 38%),
|
||||
radial-gradient(circle at 56% 82%, rgba(34, 197, 94, 0.88), transparent 42%),
|
||||
linear-gradient(125deg, #020617, #0f172a 45%, #082f49);
|
||||
background-size: 34rem 28rem, 38rem 32rem, 42rem 36rem, auto;
|
||||
}
|
||||
|
||||
.page-theme-synthwave-grid {
|
||||
/* Two repeating line layers form a broad grid; the magenta horizon glow prevents horizontal
|
||||
gaps from reading as a single flat color when they cross between grid lines. Two-pixel grid
|
||||
strokes remain crisp without bringing back the previous tiny graph-paper appearance. */
|
||||
background-color: #090018;
|
||||
background-image:
|
||||
linear-gradient(rgba(34, 211, 238, 0.7) 2px, transparent 2px),
|
||||
linear-gradient(90deg, rgba(236, 72, 153, 0.72) 2px, transparent 2px),
|
||||
radial-gradient(ellipse at 50% 100%, #7e22ce 0, #1e1b4b 38%, #090018 72%);
|
||||
background-size: 54px 54px, 54px 54px, auto;
|
||||
}
|
||||
|
||||
.page-theme-neon-checker {
|
||||
/* A conic gradient makes a crisp checker with no image asset and keeps intersections equally
|
||||
interesting when exposed through horizontal or vertical card seams. The large tile size
|
||||
turns each color into a strong block instead of a rapidly alternating micro-pattern. */
|
||||
background-color: #111827;
|
||||
background-image: conic-gradient(
|
||||
from 45deg,
|
||||
#06b6d4 0 25%,
|
||||
#7c3aed 0 50%,
|
||||
#ec4899 0 75%,
|
||||
#111827 0
|
||||
);
|
||||
background-size: 84px 84px;
|
||||
}
|
||||
|
||||
.page-theme-ocean-current {
|
||||
/* Crossing translucent diagonals suggest moving water while remaining completely static. The
|
||||
broad transparent intervals create long currents instead of tightly packed hatch marks. */
|
||||
background-color: #082f49;
|
||||
background-image:
|
||||
repeating-linear-gradient(35deg, rgba(103, 232, 249, 0.72) 0 12px, transparent 12px 54px),
|
||||
repeating-linear-gradient(-35deg, rgba(14, 116, 144, 0.72) 0 21px, transparent 21px 72px),
|
||||
linear-gradient(90deg, #0c4a6e, #0891b2, #164e63);
|
||||
}
|
||||
|
||||
.page-theme-ember-lattice {
|
||||
/* Opposing narrow diagonals create bright crossings, with a dark red base keeping the theme
|
||||
readable instead of overpowering the neutral cards around it. The enlarged repeat creates
|
||||
occasional strong crossings rather than a dense mesh along every seam. */
|
||||
background-color: #1c0704;
|
||||
background-image:
|
||||
repeating-linear-gradient(45deg, transparent 0 36px, rgba(251, 146, 60, 0.9) 36px 45px),
|
||||
repeating-linear-gradient(-45deg, transparent 0 48px, rgba(239, 68, 68, 0.78) 48px 57px),
|
||||
radial-gradient(circle at center, #7c2d12, #1c0704 70%);
|
||||
}
|
||||
|
||||
.page-theme-starfield {
|
||||
/* Offset radial layers provide stars at three scales. All layers repeat, so long PTZ and
|
||||
driver pages never reveal an unpainted region while scrolling. Larger spacing and dots make
|
||||
this read as a sparse starfield rather than fine visual noise inside the narrow gaps. */
|
||||
background-color: #020617;
|
||||
background-image:
|
||||
radial-gradient(circle, rgba(255, 255, 255, 0.95) 0 2px, transparent 3px),
|
||||
radial-gradient(circle, rgba(125, 211, 252, 0.9) 0 3px, transparent 4px),
|
||||
radial-gradient(circle, rgba(216, 180, 254, 0.8) 0 2px, transparent 3px),
|
||||
linear-gradient(135deg, #020617, #172554 55%, #3b0764);
|
||||
background-position: 0 0, 57px 81px, 123px 33px, 0 0;
|
||||
background-size: 111px 111px, 183px 183px, 237px 237px, auto;
|
||||
}
|
||||
|
||||
.page-theme-vaporwave-sunset {
|
||||
/* A large horizon glow supplies the sunset while widely spaced scan lines add retro structure.
|
||||
Both layers span enough distance to remain recognizable through narrow card seams instead of
|
||||
collapsing into the fine television-static texture common to smaller vaporwave patterns. */
|
||||
background-color: #2e1065;
|
||||
background-image:
|
||||
repeating-linear-gradient(
|
||||
0deg,
|
||||
transparent 0 24px,
|
||||
rgba(103, 232, 249, 0.48) 24px 27px,
|
||||
transparent 27px 54px
|
||||
),
|
||||
radial-gradient(circle at 50% 78%, #fda4af 0 14%, #f97316 14% 25%, transparent 25.5%),
|
||||
linear-gradient(180deg, #172554 0%, #6b21a8 48%, #db2777 72%, #f97316 100%);
|
||||
background-size: auto, 48rem 34rem, auto;
|
||||
}
|
||||
|
||||
.page-theme-electric-circuit {
|
||||
/* Offset horizontal and vertical traces form large circuit-like routes. Separate repeat sizes
|
||||
keep their intersections irregular, while the dark navy base prevents the cyan and violet
|
||||
traces from overpowering the cards that the pattern is meant to separate. */
|
||||
background-color: #020617;
|
||||
background-image:
|
||||
repeating-linear-gradient(
|
||||
90deg,
|
||||
transparent 0 48px,
|
||||
rgba(34, 211, 238, 0.9) 48px 52px,
|
||||
transparent 52px 96px
|
||||
),
|
||||
repeating-linear-gradient(
|
||||
0deg,
|
||||
transparent 0 68px,
|
||||
rgba(168, 85, 247, 0.86) 68px 72px,
|
||||
transparent 72px 124px
|
||||
),
|
||||
radial-gradient(circle at center, #312e81 0, #0f172a 45%, #020617 78%);
|
||||
}
|
||||
|
||||
.page-theme-lava-flow {
|
||||
/* Oversized overlapping pools create broad molten channels without animation. Their offset
|
||||
positions ensure horizontal and vertical seams cross different hot and cooled regions as
|
||||
they travel around the page. */
|
||||
background-color: #180503;
|
||||
background-image:
|
||||
radial-gradient(ellipse at 18% 28%, #facc15 0 8%, #f97316 9% 18%, #991b1b 24%, transparent 39%),
|
||||
radial-gradient(ellipse at 76% 64%, #fb923c 0 10%, #dc2626 18%, #7f1d1d 29%, transparent 44%),
|
||||
radial-gradient(ellipse at 48% 92%, #fde047 0 7%, #ea580c 17%, transparent 36%),
|
||||
linear-gradient(135deg, #180503, #450a0a 50%, #1c0704);
|
||||
background-size: 42rem 34rem, 48rem 39rem, 37rem 31rem, auto;
|
||||
}
|
||||
|
||||
.page-theme-deep-space-nebula {
|
||||
/* Large soft clouds distinguish this from the sparse Starfield theme. A few broad nebulae are
|
||||
more useful in thin gaps than many tiny stars, and the static layers avoid continuous paint
|
||||
work behind the live rover and PTZ video surfaces. */
|
||||
background-color: #020617;
|
||||
background-image:
|
||||
radial-gradient(ellipse at 22% 38%, rgba(217, 70, 239, 0.92), transparent 34%),
|
||||
radial-gradient(ellipse at 72% 24%, rgba(59, 130, 246, 0.9), transparent 38%),
|
||||
radial-gradient(ellipse at 58% 82%, rgba(14, 165, 233, 0.72), transparent 36%),
|
||||
radial-gradient(circle, rgba(255, 255, 255, 0.9) 0 2px, transparent 3px),
|
||||
linear-gradient(125deg, #020617, #1e1b4b 52%, #3b0764);
|
||||
background-position: center, center, center, 17px 31px, center;
|
||||
background-size: 42rem 34rem, 48rem 38rem, 39rem 35rem, 173px 173px, auto;
|
||||
}
|
||||
|
||||
.page-theme-holographic-waves {
|
||||
/* Broad translucent diagonals cross over an iridescent base. Using two directions means both
|
||||
horizontal and vertical gaps encounter gradual color shifts without resorting to small,
|
||||
noisy rainbow repetitions. */
|
||||
background-color: #164e63;
|
||||
background-image:
|
||||
repeating-linear-gradient(
|
||||
32deg,
|
||||
rgba(255, 255, 255, 0.4) 0 14px,
|
||||
transparent 14px 82px
|
||||
),
|
||||
repeating-linear-gradient(
|
||||
-38deg,
|
||||
rgba(216, 180, 254, 0.38) 0 18px,
|
||||
transparent 18px 104px
|
||||
),
|
||||
linear-gradient(110deg, #67e8f9, #c4b5fd 32%, #f9a8d4 60%, #86efac 100%);
|
||||
}
|
||||
|
||||
.page-theme-hazard-stripes {
|
||||
/* This intentionally uses a simple, very wide two-color repeat. The large bands make the
|
||||
industrial warning motif immediately readable even though only a two-pixel slice is visible
|
||||
between neighboring cards. Its bands use the same shared width as every pride stripe theme. */
|
||||
background-color: #111111;
|
||||
background-image: repeating-linear-gradient(
|
||||
45deg,
|
||||
#facc15 0 48px,
|
||||
#171717 48px 96px
|
||||
);
|
||||
}
|
||||
|
||||
.page-theme-mint-mosaic {
|
||||
/* A large conic tile creates chunky geometric blocks in cool mint, teal, navy, and cream. The
|
||||
restrained palette offers a calmer alternative to Neon checker while retaining clear color
|
||||
changes along both seam directions. */
|
||||
background-color: #064e3b;
|
||||
background-image: conic-gradient(
|
||||
from 45deg,
|
||||
#a7f3d0 0 25%,
|
||||
#0f766e 0 50%,
|
||||
#fef3c7 0 75%,
|
||||
#164e63 0
|
||||
);
|
||||
background-size: 96px 96px;
|
||||
}
|
||||
|
||||
.page-theme-candy-swirl {
|
||||
/* Large repeating rings provide curved movement that the otherwise line-oriented catalog does
|
||||
not have. Wide color stops keep the pastel rings bold and prevent them from turning into a
|
||||
dense target pattern in the preview or real page gaps. */
|
||||
background-color: #fdf2f8;
|
||||
background-image: repeating-radial-gradient(
|
||||
circle at 28% 34%,
|
||||
#f9a8d4 0 28px,
|
||||
#fef3c7 28px 56px,
|
||||
#93c5fd 56px 84px,
|
||||
#c4b5fd 84px 112px,
|
||||
#f9a8d4 112px 140px
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/* Pride Page Themes
|
||||
* Purpose: Contains the broad, equal-width flag-inspired stripe artwork.
|
||||
* Scope: Pride color sequences only; shared canvas behavior lives in base.css.
|
||||
*/
|
||||
|
||||
/* Ordinary CSS keeps the artwork independent of Tailwind's global component layer. main.jsx loads
|
||||
* this theme bundle after index.css, giving the theme modifiers their original cascade position. */
|
||||
.page-theme-pride-mix {
|
||||
/* Every solid stripe theme uses the same forty-eight-pixel band width. Keeping that measurement
|
||||
shared makes browsing predictable: only the colors and sequence change, never the visual
|
||||
density of the card-gap artwork. */
|
||||
background-image: repeating-linear-gradient(
|
||||
45deg,
|
||||
#5bcefa 0 48px,
|
||||
#f5a9b8 48px 96px,
|
||||
#ffffff 96px 144px,
|
||||
#f5a9b8 144px 192px,
|
||||
#5bcefa 192px 240px,
|
||||
#e40303 240px 288px,
|
||||
#ff8c00 288px 336px,
|
||||
#ffed00 336px 384px,
|
||||
#00a651 384px 432px,
|
||||
#0066ff 432px 480px,
|
||||
#9b2fae 480px 528px
|
||||
);
|
||||
}
|
||||
|
||||
.page-theme-rainbow {
|
||||
/* Equal broad diagonal bands keep the familiar six-color flag readable as color fields rather
|
||||
than a dense ribbon when exposed through either horizontal or vertical card gaps. */
|
||||
background-image: repeating-linear-gradient(
|
||||
45deg,
|
||||
#e40303 0 48px,
|
||||
#ff8c00 48px 96px,
|
||||
#ffed00 96px 144px,
|
||||
#008026 144px 192px,
|
||||
#004dff 192px 240px,
|
||||
#750787 240px 288px
|
||||
);
|
||||
}
|
||||
|
||||
.page-theme-progress-pride {
|
||||
/* The inclusive chevron colors are interleaved with the rainbow because a literal large
|
||||
chevron would disappear inside narrow seams. Using the shared stripe width keeps this longer
|
||||
sequence just as bold as the shorter flag themes instead of compressing it to fit one cycle. */
|
||||
background-image: repeating-linear-gradient(
|
||||
45deg,
|
||||
#000000 0 48px,
|
||||
#784f17 48px 96px,
|
||||
#5bcefa 96px 144px,
|
||||
#f5a9b8 144px 192px,
|
||||
#ffffff 192px 240px,
|
||||
#e40303 240px 288px,
|
||||
#ff8c00 288px 336px,
|
||||
#ffed00 336px 384px,
|
||||
#008026 384px 432px,
|
||||
#004dff 432px 480px,
|
||||
#750787 480px 528px
|
||||
);
|
||||
}
|
||||
|
||||
.page-theme-transgender {
|
||||
background-image: repeating-linear-gradient(
|
||||
45deg,
|
||||
#5bcefa 0 48px,
|
||||
#f5a9b8 48px 96px,
|
||||
#ffffff 96px 144px,
|
||||
#f5a9b8 144px 192px,
|
||||
#5bcefa 192px 240px
|
||||
);
|
||||
}
|
||||
|
||||
.page-theme-bisexual {
|
||||
/* Equal bands deliberately prioritize consistency with the other selectable stripe themes.
|
||||
The three-color identity remains clear while browsing no longer changes stripe density. */
|
||||
background-image: repeating-linear-gradient(
|
||||
45deg,
|
||||
#d60270 0 48px,
|
||||
#9b4f96 48px 96px,
|
||||
#0038a8 96px 144px
|
||||
);
|
||||
}
|
||||
|
||||
.page-theme-lesbian {
|
||||
background-image: repeating-linear-gradient(
|
||||
45deg,
|
||||
#d52d00 0 48px,
|
||||
#ef7627 48px 96px,
|
||||
#ff9a56 96px 144px,
|
||||
#ffffff 144px 192px,
|
||||
#d162a4 192px 240px,
|
||||
#b55690 240px 288px,
|
||||
#a30262 288px 336px
|
||||
);
|
||||
}
|
||||
|
||||
.page-theme-nonbinary {
|
||||
background-image: repeating-linear-gradient(
|
||||
45deg,
|
||||
#fff430 0 48px,
|
||||
#ffffff 48px 96px,
|
||||
#9c59d1 96px 144px,
|
||||
#2d2d2d 144px 192px
|
||||
);
|
||||
}
|
||||
|
||||
.page-theme-pansexual {
|
||||
background-image: repeating-linear-gradient(
|
||||
45deg,
|
||||
#ff218c 0 48px,
|
||||
#ffd800 48px 96px,
|
||||
#21b1ff 96px 144px
|
||||
);
|
||||
}
|
||||
|
||||
.page-theme-asexual {
|
||||
background-image: repeating-linear-gradient(
|
||||
45deg,
|
||||
#111111 0 48px,
|
||||
#a3a3a3 48px 96px,
|
||||
#ffffff 96px 144px,
|
||||
#800080 144px 192px
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user