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>
96 lines
2.3 KiB
TypeScript
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>
|
|
);
|
|
}
|