diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2ff4c7c..a42720b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -7141,6 +7141,17 @@ } } }, + "node_modules/next-intl/node_modules/@swc/helpers": { + "version": "0.5.20", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.20.tgz", + "integrity": "sha512-2egEBHUMasdypIzrprsu8g+OEVd7Vp2MM3a2eVlM/cyFYto0nGz5BX5BTgh/ShZZI9ed+ozEq+Ngt+rgmUs8tw==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/next-tick": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", diff --git a/frontend/src/components/ui/Tabs.tsx b/frontend/src/components/ui/Tabs.tsx new file mode 100644 index 0000000..a2cc0f2 --- /dev/null +++ b/frontend/src/components/ui/Tabs.tsx @@ -0,0 +1,98 @@ + +import { useState, useRef, useEffect } from "react"; +import type { Tab, TabsProps } from "../../types/ui"; + +export function Tabs({ tabs, defaultTab, className = "" }: TabsProps) { + const [activeTab, setActiveTab] = useState(defaultTab ?? tabs[0]?.id); + const [underlineStyle, setUnderlineStyle] = useState({ left: 0, width: 0 }); + const tabRefs = useRef>({}); + + useEffect(() => { + const el = tabRefs.current[activeTab]; + if (!el) return; + setUnderlineStyle({ left: el.offsetLeft, width: el.offsetWidth }); + }, [activeTab]); + + useEffect(() => { + const el = tabRefs.current[activeTab]; + if (!el) return; + setUnderlineStyle({ left: el.offsetLeft, width: el.offsetWidth }); + }, []); + + const activeContent = tabs.find((t) => t.id === activeTab)?.content; + + return ( +
+ {/* Tab bar */} +
+ {tabs.map((tab) => ( + + ))} + + {/* Animated underline */} + +
+ + {/* Content panel */} +
+ {activeContent} +
+ + +
+ ); +} + +export default Tabs; \ No newline at end of file diff --git a/frontend/src/types/ui.ts b/frontend/src/types/ui.ts index 2c2dea0..69365de 100644 --- a/frontend/src/types/ui.ts +++ b/frontend/src/types/ui.ts @@ -151,4 +151,17 @@ export interface UploadResult { success: boolean url?: string error?: string +} + +export interface Tab { + id: string; + label: string; + icon?: React.ReactNode; + content: React.ReactNode; +} + +export interface TabsProps { + tabs: Tab[]; + defaultTab?: string; + className?: string; } \ No newline at end of file