tabguhwuh

This commit is contained in:
legop3
2025-11-18 04:29:09 -05:00
parent 519d2e2836
commit 22c5858ab6
6 changed files with 182 additions and 59 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -5,8 +5,8 @@
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>webui</title>
<script type="module" crossorigin src="/assets/index-Cgpf9lLe.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DxO2_uri.css">
<script type="module" crossorigin src="/assets/index-CnjaYiQQ.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-C_9jprs0.css">
</head>
<body>
<div id="root"></div>
+21 -39
View File
@@ -1,24 +1,10 @@
import { useState } from 'react';
import TelemetryPanel from './TelemetryPanel.jsx';
import DrivePanel from './DrivePanel.jsx';
import CameraServoPanel from './CameraServoPanel.jsx';
import RoomCameraPanel from './RoomCameraPanel.jsx';
import AdvancedSettings from './AdvancedSettings.jsx';
import HelpPanel from './HelpPanel.jsx';
function TabButton({ active, onClick, children }) {
return (
<button
type="button"
onClick={onClick}
className={`flex-1 rounded-sm px-1 py-1 text-xs font-semibold ${
active ? 'bg-slate-100 text-slate-900' : 'bg-black/30 text-slate-300'
}`}
>
{children}
</button>
);
}
import Tabs, { Tab, TabList, TabPanel, TabPanels } from './Tabs.jsx';
function RoomControlsPlaceholder() {
return (
@@ -30,41 +16,37 @@ function RoomControlsPlaceholder() {
}
export default function RightPaneTabs({ layout }) {
const [active, setActive] = useState('telemetry');
return (
<section className="rounded-sm bg-[#1a1d23] p-1 text-base text-slate-100">
<div className="flex gap-1">
<TabButton active={active === 'telemetry'} onClick={() => setActive('telemetry')}>
Rover Controls
</TabButton>
<TabButton active={active === 'room'} onClick={() => setActive('room')}>
Room Controls
</TabButton>
<TabButton active={active === 'advanced'} onClick={() => setActive('advanced')}>
Settings
</TabButton>
<TabButton active={active === 'help'} onClick={() => setActive('help')}>
Help
</TabButton>
</div>
<div className="mt-1 space-y-1">
{active === 'telemetry' && (
<Tabs defaultTab="telemetry">
<TabList>
<Tab id="telemetry">Rover Controls</Tab>
<Tab id="room">Room Controls</Tab>
<Tab id="settings">Settings</Tab>
<Tab id="help">Help</Tab>
</TabList>
<TabPanels>
<TabPanel id="telemetry">
<div className="space-y-1">
<DrivePanel />
<CameraServoPanel />
<TelemetryPanel />
</div>
)}
{active === 'room' && (
</TabPanel>
<TabPanel id="room">
<div className="space-y-1">
<RoomCameraPanel />
<RoomControlsPlaceholder />
</div>
)}
{active === 'advanced' && <AdvancedSettings />}
{active === 'help' && <HelpPanel layout={layout} />}
</div>
</TabPanel>
<TabPanel id="settings">
<AdvancedSettings />
</TabPanel>
<TabPanel id="help">
<HelpPanel layout={layout} />
</TabPanel>
</TabPanels>
</Tabs>
</section>
);
}
+141
View File
@@ -0,0 +1,141 @@
import { createContext, useCallback, useContext, useMemo, useState, useEffect } from 'react';
const TabsContext = createContext(null);
function useTabsContext() {
const context = useContext(TabsContext);
if (!context) {
throw new Error('Tabs components must be used within a <Tabs> provider.');
}
return context;
}
const TAB_VARIANTS = {
primary: {
base: 'flex-1 rounded-sm px-1 py-1 text-xs font-semibold transition-colors focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-1 focus-visible:outline-slate-500',
active: 'bg-slate-100 text-slate-900',
inactive: 'bg-black/30 text-slate-300 hover:text-slate-100',
},
};
const DEFAULT_VARIANT = 'primary';
function classNames(...parts) {
return parts.filter(Boolean).join(' ');
}
export default function Tabs({ children, defaultTab, currentTab, onTabChange, variant = DEFAULT_VARIANT }) {
const [internalTab, setInternalTab] = useState(defaultTab ?? null);
const [tabOrder, setTabOrder] = useState([]);
const registerTab = useCallback((id) => {
if (!id) {
return () => {};
}
setTabOrder((prev) => {
if (prev.includes(id)) {
return prev;
}
return [...prev, id];
});
return () =>
setTabOrder((prev) => {
if (!prev.includes(id)) {
return prev;
}
return prev.filter((existing) => existing !== id);
});
}, []);
const activeTab = currentTab ?? internalTab ?? tabOrder[0] ?? null;
useEffect(() => {
if (defaultTab && currentTab === undefined) {
setInternalTab(defaultTab);
}
}, [defaultTab, currentTab]);
const setActiveTab = useCallback(
(id) => {
if (!id) {
return;
}
if (currentTab === undefined) {
setInternalTab(id);
}
if (onTabChange) {
onTabChange(id);
}
},
[currentTab, onTabChange]
);
const value = useMemo(
() => ({
activeTab,
setActiveTab,
registerTab,
variant,
}),
[activeTab, setActiveTab, registerTab, variant]
);
return <TabsContext.Provider value={value}>{children}</TabsContext.Provider>;
}
export function TabList({ children, className = '' }) {
return <div className={classNames('flex gap-1', className)}>{children}</div>;
}
export function Tab({ id, children, className = '', disabled = false }) {
const { activeTab, setActiveTab, registerTab, variant } = useTabsContext();
useEffect(() => registerTab(id), [id, registerTab]);
const variantStyles = TAB_VARIANTS[variant] ?? TAB_VARIANTS[DEFAULT_VARIANT];
const isActive = activeTab === id;
const buttonClassName = classNames(
variantStyles.base,
isActive ? variantStyles.active : variantStyles.inactive,
disabled ? 'cursor-not-allowed opacity-50' : '',
className
);
return (
<button
type="button"
className={buttonClassName}
onClick={() => !disabled && setActiveTab(id)}
aria-pressed={isActive}
aria-disabled={disabled}
>
{children}
</button>
);
}
export function TabPanels({ children, className = '' }) {
return <div className={classNames('mt-1 space-y-1', className)}>{children}</div>;
}
export function TabPanel({ id, children, keepMounted = false }) {
const { activeTab } = useTabsContext();
const isActive = activeTab === id;
if (!isActive && !keepMounted) {
return null;
}
if (!isActive && keepMounted) {
return (
<div hidden aria-hidden="true">
{children}
</div>
);
}
return <>{children}</>;
}