Files
limitless-ui/src/components/Carousel.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

216 lines
5.7 KiB
TypeScript

"use client";
import React, { useState, useEffect, useCallback, useRef } from 'react';
export interface CarouselItem {
/** Image source */
src?: string;
/** Alt text for image */
alt?: string;
/** Caption title */
title?: string;
/** Caption description */
description?: string;
/** Custom content instead of image */
content?: React.ReactNode;
}
export interface CarouselProps {
/** Carousel items */
items: CarouselItem[];
/** Show indicators */
indicators?: boolean;
/** Show controls (prev/next) */
controls?: boolean;
/** Auto play */
autoPlay?: boolean;
/** Interval in ms */
interval?: number;
/** Pause on hover */
pauseOnHover?: boolean;
/** Enable keyboard navigation */
keyboard?: boolean;
/** Enable touch/swipe */
touch?: boolean;
/** Crossfade animation */
fade?: boolean;
/** Dark variant */
dark?: boolean;
/** Active index (controlled) */
activeIndex?: number;
/** Callback when slide changes */
onSlide?: (index: number) => void;
/** Additional CSS classes */
className?: string;
}
export const Carousel: React.FC<CarouselProps> = ({
items,
indicators = true,
controls = true,
autoPlay = false,
interval = 5000,
pauseOnHover = true,
keyboard = true,
touch = true,
fade = false,
dark = false,
activeIndex: controlledIndex,
onSlide,
className = '',
}) => {
const [currentIndex, setCurrentIndex] = useState(controlledIndex ?? 0);
const [isPaused, setIsPaused] = useState(false);
const [touchStart, setTouchStart] = useState<number | null>(null);
const carouselRef = useRef<HTMLDivElement>(null);
const activeIndex = controlledIndex ?? currentIndex;
const goToSlide = useCallback((index: number) => {
const newIndex = index < 0 ? items.length - 1 : index >= items.length ? 0 : index;
if (controlledIndex === undefined) {
setCurrentIndex(newIndex);
}
onSlide?.(newIndex);
}, [items.length, controlledIndex, onSlide]);
const goToPrev = useCallback(() => {
goToSlide(activeIndex - 1);
}, [activeIndex, goToSlide]);
const goToNext = useCallback(() => {
goToSlide(activeIndex + 1);
}, [activeIndex, goToSlide]);
// Auto play
useEffect(() => {
if (!autoPlay || isPaused) return;
const timer = setInterval(() => {
goToNext();
}, interval);
return () => clearInterval(timer);
}, [autoPlay, isPaused, interval, goToNext]);
// Keyboard navigation
useEffect(() => {
if (!keyboard) return;
const handleKeydown = (e: KeyboardEvent) => {
if (e.key === 'ArrowLeft') {
goToPrev();
} else if (e.key === 'ArrowRight') {
goToNext();
}
};
const carousel = carouselRef.current;
carousel?.addEventListener('keydown', handleKeydown);
return () => carousel?.removeEventListener('keydown', handleKeydown);
}, [keyboard, goToPrev, goToNext]);
// Touch handling
const handleTouchStart = (e: React.TouchEvent) => {
if (!touch) return;
setTouchStart(e.touches[0].clientX);
};
const handleTouchEnd = (e: React.TouchEvent) => {
if (!touch || touchStart === null) return;
const touchEnd = e.changedTouches[0].clientX;
const diff = touchStart - touchEnd;
if (Math.abs(diff) > 50) {
if (diff > 0) {
goToNext();
} else {
goToPrev();
}
}
setTouchStart(null);
};
const classes = [
'll-carousel',
fade && 'll-carousel-fade',
dark && 'll-carousel-dark',
className,
].filter(Boolean).join(' ');
return (
<div
ref={carouselRef}
className={classes}
tabIndex={keyboard ? 0 : undefined}
onMouseEnter={() => pauseOnHover && setIsPaused(true)}
onMouseLeave={() => pauseOnHover && setIsPaused(false)}
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
>
{/* Indicators */}
{indicators && items.length > 1 && (
<div className="ll-carousel-indicators">
{items.map((_, index) => (
<button
key={index}
type="button"
className={`ll-carousel-indicator ${index === activeIndex ? 'active' : ''}`}
onClick={() => goToSlide(index)}
aria-current={index === activeIndex}
aria-label={`Slide ${index + 1}`}
/>
))}
</div>
)}
{/* Slides */}
<div className="ll-carousel-inner">
{items.map((item, index) => (
<div
key={index}
className={`ll-carousel-item ${index === activeIndex ? 'active' : ''}`}
>
{item.content || (
<img
src={item.src}
alt={item.alt || ''}
className="ll-carousel-image"
/>
)}
{(item.title || item.description) && (
<div className="ll-carousel-caption">
{item.title && <h5>{item.title}</h5>}
{item.description && <p>{item.description}</p>}
</div>
)}
</div>
))}
</div>
{/* Controls */}
{controls && items.length > 1 && (
<>
<button
type="button"
className="ll-carousel-control ll-carousel-control-prev"
onClick={goToPrev}
aria-label="Previous"
>
<span className="ll-carousel-control-icon ll-carousel-control-prev-icon" aria-hidden="true" />
</button>
<button
type="button"
className="ll-carousel-control ll-carousel-control-next"
onClick={goToNext}
aria-label="Next"
>
<span className="ll-carousel-control-icon ll-carousel-control-next-icon" aria-hidden="true" />
</button>
</>
)}
</div>
);
};