mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 09:31:20 -04:00
tabguhwuh
This commit is contained in:
@@ -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' && (
|
||||
<div className="space-y-1">
|
||||
<DrivePanel />
|
||||
<CameraServoPanel />
|
||||
<TelemetryPanel />
|
||||
</div>
|
||||
)}
|
||||
{active === 'room' && (
|
||||
<div className="space-y-1">
|
||||
<RoomCameraPanel />
|
||||
<RoomControlsPlaceholder />
|
||||
</div>
|
||||
)}
|
||||
{active === 'advanced' && <AdvancedSettings />}
|
||||
{active === 'help' && <HelpPanel layout={layout} />}
|
||||
</div>
|
||||
<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>
|
||||
</TabPanel>
|
||||
<TabPanel id="room">
|
||||
<div className="space-y-1">
|
||||
<RoomCameraPanel />
|
||||
<RoomControlsPlaceholder />
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel id="settings">
|
||||
<AdvancedSettings />
|
||||
</TabPanel>
|
||||
<TabPanel id="help">
|
||||
<HelpPanel layout={layout} />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}</>;
|
||||
}
|
||||
Reference in New Issue
Block a user