Files
explorer-monorepo/frontend/src/components/common/SectionTabs.tsx
defiQUG b87ebee6a1
Some checks failed
Deploy Explorer Live / deploy (push) Failing after 20s
Validate Explorer / frontend (push) Failing after 24s
Validate Explorer / smoke-e2e (push) Has been skipped
feat(explorer): dual-chain wallet metadata, native coin pricing, and UI refresh.
Add Chain 138 wallet network metadata and stats coin-price enrichment; sync frontend explorer SPA, command center, and address/token pages with backend config.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-19 16:16:17 -07:00

121 lines
3.3 KiB
TypeScript

import { useCallback, useId } from 'react'
export interface SectionTab<T extends string> {
id: T
label: string
count?: number
}
interface SectionTabsProps<T extends string> {
tabs: SectionTab<T>[]
activeTab: T
onChange: (tab: T) => void
className?: string
idPrefix?: string
ariaLabel?: string
}
export function sectionTabPanelProps<T extends string>(
idPrefix: string,
tabId: T,
activeTab: T,
) {
return {
id: `${idPrefix}-panel-${tabId}`,
role: 'tabpanel' as const,
'aria-labelledby': `${idPrefix}-tab-${tabId}`,
hidden: activeTab !== tabId,
}
}
export default function SectionTabs<T extends string>({
tabs,
activeTab,
onChange,
className = '',
idPrefix,
ariaLabel = 'Sections',
}: SectionTabsProps<T>) {
const generatedId = useId().replace(/:/g, '')
const tabListPrefix = idPrefix || generatedId
const focusTab = useCallback(
(tabId: T) => {
const element = document.getElementById(`${tabListPrefix}-tab-${tabId}`)
element?.focus()
},
[tabListPrefix],
)
const handleKeyDown = useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
const currentIndex = tabs.findIndex((tab) => tab.id === activeTab)
if (currentIndex < 0) {
return
}
let nextIndex = currentIndex
if (event.key === 'ArrowRight') {
nextIndex = (currentIndex + 1) % tabs.length
} else if (event.key === 'ArrowLeft') {
nextIndex = (currentIndex - 1 + tabs.length) % tabs.length
} else if (event.key === 'Home') {
nextIndex = 0
} else if (event.key === 'End') {
nextIndex = tabs.length - 1
} else {
return
}
event.preventDefault()
const nextTab = tabs[nextIndex]
onChange(nextTab.id)
focusTab(nextTab.id)
},
[activeTab, focusTab, onChange, tabs],
)
return (
<div
className={`sticky top-0 z-20 border-b border-gray-200 bg-white/95 py-3 backdrop-blur dark:border-gray-800 dark:bg-gray-950/95 ${className}`}
>
<div
role="tablist"
aria-label={ariaLabel}
className="flex gap-2 overflow-x-auto"
onKeyDown={handleKeyDown}
>
{tabs.map((tab) => {
const isActive = activeTab === tab.id
return (
<button
key={tab.id}
id={`${tabListPrefix}-tab-${tab.id}`}
type="button"
role="tab"
aria-selected={isActive}
aria-controls={`${tabListPrefix}-panel-${tab.id}`}
tabIndex={isActive ? 0 : -1}
onClick={() => onChange(tab.id)}
className={
isActive
? 'whitespace-nowrap rounded-lg bg-primary-600 px-3 py-2 text-sm font-semibold text-white'
: 'whitespace-nowrap rounded-lg border border-gray-300 px-3 py-2 text-sm font-medium text-gray-700 transition hover:border-primary-400 hover:text-primary-700 dark:border-gray-700 dark:text-gray-300 dark:hover:text-primary-300'
}
>
{tab.label}
{typeof tab.count === 'number' ? (
<span className={isActive ? 'ml-2 text-primary-100' : 'ml-2 text-gray-500 dark:text-gray-400'}>
{tab.count.toLocaleString()}
</span>
) : null}
</button>
)
})}
</div>
</div>
)
}