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" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>webui</title> <title>webui</title>
<script type="module" crossorigin src="/assets/index-Cgpf9lLe.js"></script> <script type="module" crossorigin src="/assets/index-CnjaYiQQ.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DxO2_uri.css"> <link rel="stylesheet" crossorigin href="/assets/index-C_9jprs0.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+21 -39
View File
@@ -1,24 +1,10 @@
import { useState } from 'react';
import TelemetryPanel from './TelemetryPanel.jsx'; import TelemetryPanel from './TelemetryPanel.jsx';
import DrivePanel from './DrivePanel.jsx'; import DrivePanel from './DrivePanel.jsx';
import CameraServoPanel from './CameraServoPanel.jsx'; import CameraServoPanel from './CameraServoPanel.jsx';
import RoomCameraPanel from './RoomCameraPanel.jsx'; import RoomCameraPanel from './RoomCameraPanel.jsx';
import AdvancedSettings from './AdvancedSettings.jsx'; import AdvancedSettings from './AdvancedSettings.jsx';
import HelpPanel from './HelpPanel.jsx'; import HelpPanel from './HelpPanel.jsx';
import Tabs, { Tab, TabList, TabPanel, TabPanels } from './Tabs.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>
);
}
function RoomControlsPlaceholder() { function RoomControlsPlaceholder() {
return ( return (
@@ -30,41 +16,37 @@ function RoomControlsPlaceholder() {
} }
export default function RightPaneTabs({ layout }) { export default function RightPaneTabs({ layout }) {
const [active, setActive] = useState('telemetry');
return ( return (
<section className="rounded-sm bg-[#1a1d23] p-1 text-base text-slate-100"> <section className="rounded-sm bg-[#1a1d23] p-1 text-base text-slate-100">
<div className="flex gap-1"> <Tabs defaultTab="telemetry">
<TabButton active={active === 'telemetry'} onClick={() => setActive('telemetry')}> <TabList>
Rover Controls <Tab id="telemetry">Rover Controls</Tab>
</TabButton> <Tab id="room">Room Controls</Tab>
<TabButton active={active === 'room'} onClick={() => setActive('room')}> <Tab id="settings">Settings</Tab>
Room Controls <Tab id="help">Help</Tab>
</TabButton> </TabList>
<TabButton active={active === 'advanced'} onClick={() => setActive('advanced')}> <TabPanels>
Settings <TabPanel id="telemetry">
</TabButton>
<TabButton active={active === 'help'} onClick={() => setActive('help')}>
Help
</TabButton>
</div>
<div className="mt-1 space-y-1">
{active === 'telemetry' && (
<div className="space-y-1"> <div className="space-y-1">
<DrivePanel /> <DrivePanel />
<CameraServoPanel /> <CameraServoPanel />
<TelemetryPanel /> <TelemetryPanel />
</div> </div>
)} </TabPanel>
{active === 'room' && ( <TabPanel id="room">
<div className="space-y-1"> <div className="space-y-1">
<RoomCameraPanel /> <RoomCameraPanel />
<RoomControlsPlaceholder /> <RoomControlsPlaceholder />
</div> </div>
)} </TabPanel>
{active === 'advanced' && <AdvancedSettings />} <TabPanel id="settings">
{active === 'help' && <HelpPanel layout={layout} />} <AdvancedSettings />
</div> </TabPanel>
<TabPanel id="help">
<HelpPanel layout={layout} />
</TabPanel>
</TabPanels>
</Tabs>
</section> </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}</>;
}