mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
tabguhwuh
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
@@ -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>
|
||||
|
||||
@@ -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