mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 09:31:20 -04:00
new themeses!!
This commit is contained in:
+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