bolt.diy/app/components/ui/TabsWithSlider.tsx
Stijnus 870bfc58ee
Some checks are pending
Docker Publish / docker-build-publish (push) Waiting to run
Update Stable Branch / prepare-release (push) Waiting to run
feat: github fix and ui improvements (#1685)
* feat: Add reusable UI components and fix GitHub repository display

* style: Fix linting issues in UI components

* fix: Add close icon to GitHub Connection Required dialog

* fix: Add CloseButton component to fix white background issue in dialog close icons

* Fix close button styling in dialog components to address ghost white issue in dark mode

* fix: update icon color to tertiary for consistency

The icon color was changed from `text-bolt-elements-icon-info` to `text-bolt-elements-icon-tertiary`

* fix: improve repository selection dialog tab styling for dark mode

- Update tab menu styling to prevent white background in dark mode
- Use explicit color values for better dark/light mode compatibility
- Improve hover and active states for better visual hierarchy
- Remove unused Tabs imports

---------

Co-authored-by: KevIsDev <zennerd404@gmail.com>
2025-05-09 15:23:20 +02:00

113 lines
3.4 KiB
TypeScript

import React, { useState, useRef, useEffect } from 'react';
import { motion } from 'framer-motion';
import { classNames } from '~/utils/classNames';
interface Tab {
/** Unique identifier for the tab */
id: string;
/** Content to display in the tab */
label: React.ReactNode;
/** Optional icon to display before the label */
icon?: string;
}
interface TabsWithSliderProps {
/** Array of tab objects */
tabs: Tab[];
/** ID of the currently active tab */
activeTab: string;
/** Function called when a tab is clicked */
onChange: (tabId: string) => void;
/** Additional class name for the container */
className?: string;
/** Additional class name for inactive tabs */
tabClassName?: string;
/** Additional class name for the active tab */
activeTabClassName?: string;
/** Additional class name for the slider */
sliderClassName?: string;
}
/**
* TabsWithSlider component
*
* A tabs component with an animated slider that moves to the active tab.
*/
export function TabsWithSlider({
tabs,
activeTab,
onChange,
className,
tabClassName,
activeTabClassName,
sliderClassName,
}: TabsWithSliderProps) {
// State for slider dimensions
const [sliderDimensions, setSliderDimensions] = useState({ width: 0, left: 0 });
// Refs for tab elements
const tabsRef = useRef<(HTMLButtonElement | null)[]>([]);
// Update slider position when active tab changes
useEffect(() => {
const activeIndex = tabs.findIndex((tab) => tab.id === activeTab);
if (activeIndex !== -1 && tabsRef.current[activeIndex]) {
const activeTabElement = tabsRef.current[activeIndex];
if (activeTabElement) {
setSliderDimensions({
width: activeTabElement.offsetWidth,
left: activeTabElement.offsetLeft,
});
}
}
}, [activeTab, tabs]);
return (
<div className={classNames('relative flex gap-2', className)}>
{/* Tab buttons */}
{tabs.map((tab, index) => (
<button
key={tab.id}
ref={(el) => (tabsRef.current[index] = el)}
onClick={() => onChange(tab.id)}
className={classNames(
'px-4 py-2 h-10 rounded-lg transition-all duration-200 flex items-center gap-2 min-w-[120px] justify-center relative overflow-hidden',
tab.id === activeTab
? classNames('text-white shadow-sm shadow-purple-500/20', activeTabClassName)
: classNames(
'bg-bolt-elements-background-depth-2 dark:bg-bolt-elements-background-depth-3 text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary-dark hover:bg-bolt-elements-background-depth-3 dark:hover:bg-bolt-elements-background-depth-4 border border-bolt-elements-borderColor dark:border-bolt-elements-borderColor-dark',
tabClassName,
),
)}
>
<span className={classNames('flex items-center gap-2', tab.id === activeTab ? 'font-medium' : '')}>
{tab.icon && <span className={tab.icon} />}
{tab.label}
</span>
</button>
))}
{/* Animated slider */}
<motion.div
className={classNames('absolute bottom-0 left-0 h-10 rounded-lg bg-purple-500 -z-10', sliderClassName)}
initial={false}
animate={{
width: sliderDimensions.width,
x: sliderDimensions.left,
}}
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
/>
</div>
);
}