refactor: extract SettingsTabBar as reusable infrastructure component

Replace one-off inline tab bar in InstancePanel with a reusable
SettingsTabBar that reads from SettingsSectionsContext. Any panel
using tab mode can drop it in with zero configuration.

Polish: proper underline indicator, consistent gap spacing,
negative top margin to sit flush with content area padding.
This commit is contained in:
Jannis Braun
2026-03-22 04:42:46 +01:00
parent 2d7395ba64
commit 7ff8614127
2 changed files with 34 additions and 15 deletions
@@ -0,0 +1,32 @@
import { useSettingsSectionsContext } from './SettingsSectionsContext';
/**
* Reusable tab bar for settings panels in tab mode.
* Reads sections and activeSection from SettingsSectionsContext.
* Drop this into any panel that uses useSettingsSections with onNavigate.
*/
export function SettingsTabBar() {
const ctx = useSettingsSectionsContext();
if (!ctx || ctx.sections.length === 0) return null;
return (
<div className="flex gap-4 border-b border-white/[0.06] -mt-2 mb-4">
{ctx.sections.map((s) => (
<button
key={s.id}
onClick={() => ctx.scrollToSection(s.id)}
className={`pb-2.5 text-sm transition-colors relative ${
ctx.activeSection === s.id
? 'text-txt-primary font-medium'
: 'text-txt-tertiary hover:text-txt-secondary'
}`}
>
{s.label}
{ctx.activeSection === s.id && (
<span className="absolute bottom-0 left-0 right-0 h-0.5 bg-accent-primary rounded-full" />
)}
</button>
))}
</div>
);
}
@@ -2,6 +2,7 @@ import { useState, useEffect, useCallback } from 'react';
import { useSettingsStore } from '../../../stores/settingsStore'; import { useSettingsStore } from '../../../stores/settingsStore';
import { useSettingsSections } from '../../../hooks/useSettingsSections'; import { useSettingsSections } from '../../../hooks/useSettingsSections';
import type { SettingsSection } from '../SettingsSectionsContext'; import type { SettingsSection } from '../SettingsSectionsContext';
import { SettingsTabBar } from '../SettingsTabBar';
import { GeneralPanel } from '../instanceSettingsPanels/GeneralPanel'; import { GeneralPanel } from '../instanceSettingsPanels/GeneralPanel';
import { StreamingPanel } from '../instanceSettingsPanels/StreamingPanel'; import { StreamingPanel } from '../instanceSettingsPanels/StreamingPanel';
import { StoragePanel } from '../instanceSettingsPanels/StoragePanel'; import { StoragePanel } from '../instanceSettingsPanels/StoragePanel';
@@ -34,23 +35,9 @@ export function InstancePanel() {
fetchStreamingLimits(); fetchStreamingLimits();
}, [fetchInstanceSettings, fetchStreamingLimits]); }, [fetchInstanceSettings, fetchStreamingLimits]);
const tabPillClass = (t: SubTab) =>
`px-3 py-1.5 text-sm rounded-md transition-colors ${
subTab === t
? 'text-txt-primary font-medium border-b-2 border-accent-primary'
: 'text-txt-tertiary hover:text-txt-secondary'
}`;
return ( return (
<div className="space-y-4"> <div className="space-y-4">
{/* Top tab bar */} <SettingsTabBar />
<div className="flex gap-1 border-b border-white/[0.04] pb-0">
{SECTIONS.map((s) => (
<button key={s.id} onClick={() => setSubTab(s.id as SubTab)} className={tabPillClass(s.id as SubTab)}>
{s.label}
</button>
))}
</div>
{subTab === 'general' && <GeneralPanel />} {subTab === 'general' && <GeneralPanel />}
{subTab === 'streaming' && <StreamingPanel />} {subTab === 'streaming' && <StreamingPanel />}