Files
limitless-ui/src/components/Tabs.tsx
Claude 7eb18b15b8 fix(rsc): add "use client" directive to all client-interactive files
50 .tsx components and 6 .ts hook files use React client APIs
(createContext, useState, useEffect, useRef, useMemo, useCallback,
forwardRef, etc.) but lacked the "use client" directive at the top
of the source file. tsc-emitted dist files therefore also lacked it.

Older Next.js / Turbopack versions traversed the import graph and
implicitly treated the imports as client when their consumer was a
client component. Next.js 16's stricter Turbopack rejects this: any
file that touches client-only React APIs must declare "use client"
explicitly, regardless of where it is imported from.

Symptom in consumers (gscSupport build, 2026-05-17):

  ./templates/limitless-ui/dist/theme/ThemeProvider.js:2:10
  You're importing a module that depends on `createContext` into a
  React Server Component module. This API is only available in
  Client Components. To fix, mark the file (or its parent) with
  the `"use client"` directive.

Adding the directive at source-file level cascades through tsc into
the emitted dist/ — verified gscSupport + gscCRM build cleanly after
this change.

Affected files (50 total):
  src/theme/ThemeProvider.tsx
  src/hooks/useDisclosure.ts
  src/components/{Accordion,Carousel,DualListBox,Form,Wizard,…}.tsx
  src/validation/hooks/{useValidation,useFieldValidation,…}.ts
  src/genui/hooks/{useGenUI,useWebMCP}.ts
  ... and more — every component that touches createContext / a
  React hook now self-declares as client.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 18:36:40 +02:00

96 lines
2.3 KiB
TypeScript

"use client";
import React, { useMemo, useState } from 'react';
export type TabItem = {
key: string;
title: React.ReactNode;
content: React.ReactNode;
disabled?: boolean;
};
export type TabsProps = {
items: TabItem[];
defaultActiveKey?: string;
activeKey?: string;
onChange?: (key: string) => void;
variant?: 'tabs' | 'pills';
fill?: boolean;
justify?: boolean;
className?: string;
navClassName?: string;
contentClassName?: string;
};
export function Tabs({
items,
defaultActiveKey,
activeKey,
onChange,
variant = 'tabs',
fill,
justify,
className = '',
navClassName = '',
contentClassName = ''
}: TabsProps) {
const fallbackKey = items.find(i => !i.disabled)?.key;
const [internalKey, setInternalKey] = useState<string | undefined>(defaultActiveKey ?? fallbackKey);
const currentKey = activeKey ?? internalKey ?? fallbackKey;
const navClasses = useMemo(() => {
return [
'nav',
variant === 'tabs' ? 'nav-tabs' : 'nav-pills',
fill ? 'nav-fill' : '',
justify ? 'nav-justified' : '',
navClassName
]
.filter(Boolean)
.join(' ');
}, [variant, fill, justify, navClassName]);
const handleSelect = (key: string, disabled?: boolean) => {
if (disabled) return;
if (!activeKey) {
setInternalKey(key);
}
onChange?.(key);
};
return (
<div className={className}>
<ul className={navClasses}>
{items.map(item => {
const isActive = item.key === currentKey;
return (
<li className="nav-item" key={item.key}>
<button
type="button"
className={`nav-link ${isActive ? 'active' : ''} ${item.disabled ? 'disabled' : ''}`.trim()}
onClick={() => handleSelect(item.key, item.disabled)}
>
{item.title}
</button>
</li>
);
})}
</ul>
<div className={`tab-content ${contentClassName}`.trim()}>
{items.map(item => {
const isActive = item.key === currentKey;
return (
<div
key={item.key}
className={`tab-pane fade ${isActive ? 'show active' : ''}`.trim()}
role="tabpanel"
>
{isActive ? item.content : null}
</div>
);
})}
</div>
</div>
);
}