Files
sass-mohfarawati/components/ui/tabs.tsx
T
moh d78450fc47 FIXED - Make adding a portfolio project simpler and actually saveable
- Fix critical bug: TabsContent unmounted inactive panels, so the project
  form only submitted the active tab's fields and could never save; add an
  opt-in forceMount that keeps panels mounted (hidden) and use it on all
  four project-form tabs
- Make Sections and Assets optional: a project saves with just Basics and
  Localized Content; new projects start with no sections/assets, and the
  last one can now be removed
- Update wizard progress so empty sections/assets steps count as complete
- Update save hint copy and step labels to reflect the optional steps
- Correct .claude/launch.json dev port to 3014
- Update portfolio-form-progress tests for the optional-steps behaviour
2026-09-20 01:56:50 +02:00

135 lines
3.2 KiB
TypeScript

"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
type TabsContextValue = {
value: string;
setValue: (value: string) => void;
};
const TabsContext = React.createContext<TabsContextValue | null>(null);
function useTabsContext() {
const context = React.useContext(TabsContext);
if (!context) {
throw new Error("Tabs components must be used within Tabs.");
}
return context;
}
type TabsProps = {
defaultValue?: string;
value?: string;
onValueChange?: (value: string) => void;
children: React.ReactNode;
className?: string;
};
function Tabs({ defaultValue, value, onValueChange, children, className }: TabsProps) {
const [internalValue, setInternalValue] = React.useState(defaultValue ?? "");
const activeValue = value ?? internalValue;
const setValue = React.useCallback(
(nextValue: string) => {
if (value === undefined) {
setInternalValue(nextValue);
}
onValueChange?.(nextValue);
},
[onValueChange, value],
);
return (
<TabsContext.Provider value={{ value: activeValue, setValue }}>
<div className={cn("w-full", className)}>{children}</div>
</TabsContext.Provider>
);
}
function TabsList({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn(
"inline-flex h-auto flex-wrap items-center gap-2 rounded-surface border border-border/80 bg-muted/40 p-1.5",
className,
)}
{...props}
/>
);
}
type TabsTriggerProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
value: string;
};
function TabsTrigger({
className,
value,
onClick,
...props
}: TabsTriggerProps) {
const { value: activeValue, setValue } = useTabsContext();
const isActive = activeValue === value;
return (
<button
type="button"
className={cn(
"inline-flex items-center justify-center rounded-nested px-3 py-2 text-sm font-medium text-muted-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0 disabled:pointer-events-none disabled:opacity-50",
isActive && "border border-border/80 bg-background text-foreground shadow-xs",
className,
)}
onClick={(event) => {
setValue(value);
onClick?.(event);
}}
{...props}
/>
);
}
type TabsContentProps = React.HTMLAttributes<HTMLDivElement> & {
value: string;
// Keep the panel mounted while inactive (hidden via `hidden`) so its form
// fields still submit. Use for tabbed forms that share one submit button.
forceMount?: boolean;
};
function TabsContent({
className,
value,
children,
forceMount = false,
...props
}: TabsContentProps) {
const { value: activeValue } = useTabsContext();
const isActive = activeValue === value;
if (!isActive && !forceMount) {
return null;
}
return (
<div
hidden={!isActive}
className={cn(
"mt-6 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0",
className,
)}
{...props}
>
{children}
</div>
);
}
export { Tabs, TabsContent, TabsList, TabsTrigger };