claude-code

This commit is contained in:
ashutoshpythoncs@gmail.com
2026-03-31 18:58:05 +05:30
parent a2a44a5841
commit b564857c0b
2148 changed files with 564518 additions and 2 deletions

View File

@@ -0,0 +1,100 @@
"use client";
import { useEffect, useRef } from "react";
import { cn } from "@/lib/utils";
import { useTouchGesture } from "@/hooks/useTouchGesture";
interface BottomSheetProps {
isOpen: boolean;
onClose: () => void;
title?: string;
children: React.ReactNode;
className?: string;
}
/**
* iOS-style bottom sheet.
* - Slides up from the bottom of the screen
* - Swipe down on the drag handle or sheet body to close
* - Tap backdrop to close
* - Locks body scroll while open
*/
export function BottomSheet({ isOpen, onClose, title, children, className }: BottomSheetProps) {
const sheetRef = useRef<HTMLDivElement>(null);
const swipeHandlers = useTouchGesture({ onSwipeDown: onClose });
// Lock body scroll while open
useEffect(() => {
if (isOpen) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "";
}
return () => {
document.body.style.overflow = "";
};
}, [isOpen]);
// Close on Escape
useEffect(() => {
if (!isOpen) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
};
document.addEventListener("keydown", onKey);
return () => document.removeEventListener("keydown", onKey);
}, [isOpen, onClose]);
return (
<>
{/* Backdrop */}
<div
className={cn(
"fixed inset-0 z-40 bg-black/60",
"transition-opacity duration-300",
isOpen ? "opacity-100 pointer-events-auto" : "opacity-0 pointer-events-none"
)}
onClick={onClose}
aria-hidden="true"
/>
{/* Sheet */}
<div
ref={sheetRef}
role="dialog"
aria-modal="true"
aria-label={title ?? "Options"}
className={cn(
"fixed bottom-0 left-0 right-0 z-50",
"bg-surface-900 border-t border-surface-800 rounded-t-2xl",
"max-h-[85dvh] flex flex-col",
"transition-transform duration-300 ease-out",
isOpen ? "translate-y-0" : "translate-y-full",
className
)}
{...swipeHandlers}
>
{/* Drag handle */}
<div className="flex justify-center pt-3 pb-1 flex-shrink-0 cursor-grab active:cursor-grabbing">
<div className="w-10 h-1 bg-surface-600 rounded-full" />
</div>
{/* Optional title */}
{title && (
<div className="px-4 pb-2 flex-shrink-0">
<h2 className="text-sm font-semibold text-surface-100 text-center">{title}</h2>
</div>
)}
{/* Content */}
<div className="flex-1 overflow-y-auto overscroll-contain">
{children}
</div>
{/* iOS safe area bottom padding */}
<div className="pb-safe flex-shrink-0" style={{ paddingBottom: "env(safe-area-inset-bottom)" }} />
</div>
</>
);
}

View File

@@ -0,0 +1,107 @@
"use client";
import { useEffect } from "react";
import { X, Download } from "lucide-react";
import { cn } from "@/lib/utils";
import { useTouchGesture } from "@/hooks/useTouchGesture";
interface MobileFileViewerProps {
isOpen: boolean;
onClose: () => void;
fileName?: string;
children?: React.ReactNode;
className?: string;
}
/**
* Full-screen file viewer overlay for mobile.
* - Slides up from the bottom on open
* - Swipe down to close
* - Back/close button in header
* - Content area is scrollable with pinch-to-zoom enabled on images
*/
export function MobileFileViewer({
isOpen,
onClose,
fileName,
children,
className,
}: MobileFileViewerProps) {
const swipeHandlers = useTouchGesture({ onSwipeDown: onClose, threshold: 80 });
// Lock body scroll and close on Escape
useEffect(() => {
if (isOpen) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "";
}
return () => {
document.body.style.overflow = "";
};
}, [isOpen]);
useEffect(() => {
if (!isOpen) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
};
document.addEventListener("keydown", onKey);
return () => document.removeEventListener("keydown", onKey);
}, [isOpen, onClose]);
return (
<div
className={cn(
"fixed inset-0 z-50 bg-surface-950 flex flex-col",
"transition-transform duration-300 ease-out",
isOpen ? "translate-y-0" : "translate-y-full",
className
)}
role="dialog"
aria-modal="true"
aria-label={fileName ?? "File viewer"}
>
{/* Header — swipe-down handle zone */}
<div
className="flex items-center gap-2 px-2 border-b border-surface-800 bg-surface-900/80 backdrop-blur-sm h-[52px] flex-shrink-0"
{...swipeHandlers}
>
{/* Drag handle */}
<div className="absolute left-1/2 -translate-x-1/2 top-2 w-10 h-1 bg-surface-600 rounded-full" />
<button
onClick={onClose}
className="min-w-[44px] min-h-[44px] flex items-center justify-center rounded-md text-surface-400 hover:text-surface-100 active:bg-surface-800 transition-colors"
aria-label="Close file viewer"
>
<X className="w-5 h-5" />
</button>
<span className="flex-1 text-sm font-medium text-surface-100 truncate">{fileName}</span>
<button
className="min-w-[44px] min-h-[44px] flex items-center justify-center rounded-md text-surface-400 hover:text-surface-100 active:bg-surface-800 transition-colors"
aria-label="Download file"
>
<Download className="w-5 h-5" />
</button>
</div>
{/* File content — pinch-to-zoom enabled via touch-action */}
<div
className="flex-1 overflow-auto overscroll-contain"
style={{ touchAction: "pan-x pan-y pinch-zoom" }}
>
{children ?? (
<div className="flex items-center justify-center h-full text-surface-500 text-sm">
No file selected
</div>
)}
</div>
{/* iOS safe area inset */}
<div style={{ paddingBottom: "env(safe-area-inset-bottom)" }} className="flex-shrink-0" />
</div>
);
}

View File

@@ -0,0 +1,59 @@
"use client";
import { Menu, ChevronLeft } from "lucide-react";
import { cn } from "@/lib/utils";
interface MobileHeaderProps {
title?: string;
onMenuOpen: () => void;
onBack?: () => void;
right?: React.ReactNode;
className?: string;
}
/**
* Compact top bar for mobile: hamburger (or back) on the left, title in centre, optional actions on the right.
* Tap targets are at least 44×44 px per WCAG / Apple HIG guidelines.
*/
export function MobileHeader({
title = "Chat",
onMenuOpen,
onBack,
right,
className,
}: MobileHeaderProps) {
return (
<header
className={cn(
"flex items-center gap-2 px-2 border-b border-surface-800 bg-surface-900/80 backdrop-blur-sm",
"h-[52px] flex-shrink-0",
className
)}
>
{/* Left action — back or hamburger */}
{onBack ? (
<button
onClick={onBack}
className="min-w-[44px] min-h-[44px] flex items-center justify-center rounded-md text-surface-400 hover:text-surface-100 active:bg-surface-800 transition-colors"
aria-label="Go back"
>
<ChevronLeft className="w-5 h-5" />
</button>
) : (
<button
onClick={onMenuOpen}
className="min-w-[44px] min-h-[44px] flex items-center justify-center rounded-md text-surface-400 hover:text-surface-100 active:bg-surface-800 transition-colors"
aria-label="Open sidebar"
>
<Menu className="w-5 h-5" />
</button>
)}
{/* Title */}
<h1 className="flex-1 text-sm font-medium text-surface-100 truncate">{title}</h1>
{/* Right actions */}
{right && <div className="flex items-center">{right}</div>}
</header>
);
}

View File

@@ -0,0 +1,26 @@
"use client";
import { ChatInput } from "@/components/chat/ChatInput";
interface MobileInputProps {
conversationId: string;
/** Height of the software keyboard in px — shifts input above it */
keyboardHeight: number;
}
/**
* Mobile-optimised chat input wrapper.
* Uses a paddingBottom equal to the keyboard height so the input floats
* above the virtual keyboard without relying on position:fixed (which
* breaks on iOS Safari when the keyboard is open).
*/
export function MobileInput({ conversationId, keyboardHeight }: MobileInputProps) {
return (
<div
style={{ paddingBottom: keyboardHeight }}
className="transition-[padding] duration-100"
>
<ChatInput conversationId={conversationId} />
</div>
);
}

View File

@@ -0,0 +1,75 @@
"use client";
import { useEffect } from "react";
import { cn } from "@/lib/utils";
import { Sidebar } from "@/components/layout/Sidebar";
import { useTouchGesture } from "@/hooks/useTouchGesture";
interface MobileSidebarProps {
isOpen: boolean;
onClose: () => void;
}
/**
* Slide-in drawer sidebar for mobile / tablet.
* - Opens from the left as an overlay
* - Swipe left or tap backdrop to close
* - Traps focus while open and restores on close
* - Locks body scroll while open
*/
export function MobileSidebar({ isOpen, onClose }: MobileSidebarProps) {
// Swipe left on the drawer to close
const swipeHandlers = useTouchGesture({ onSwipeLeft: onClose });
// Lock body scroll while drawer is open
useEffect(() => {
if (isOpen) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "";
}
return () => {
document.body.style.overflow = "";
};
}, [isOpen]);
// Close on Escape
useEffect(() => {
if (!isOpen) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
};
document.addEventListener("keydown", onKey);
return () => document.removeEventListener("keydown", onKey);
}, [isOpen, onClose]);
return (
<>
{/* Backdrop */}
<div
className={cn(
"fixed inset-0 z-40 bg-black/60 lg:hidden",
"transition-opacity duration-300",
isOpen ? "opacity-100 pointer-events-auto" : "opacity-0 pointer-events-none"
)}
onClick={onClose}
aria-hidden="true"
/>
{/* Drawer */}
<div
className={cn(
"fixed top-0 left-0 bottom-0 z-50 w-72 lg:hidden",
"transition-transform duration-300 ease-in-out",
isOpen ? "translate-x-0" : "-translate-x-full"
)}
role="dialog"
aria-modal="true"
aria-label="Navigation"
{...swipeHandlers}
>
<Sidebar onNavigate={onClose} />
</div>
</>
);
}

View File

@@ -0,0 +1,152 @@
"use client";
import { useRef, useState, useCallback } from "react";
import { cn } from "@/lib/utils";
interface SwipeAction {
label: string;
icon?: React.ReactNode;
onClick: () => void;
className?: string;
}
interface SwipeableRowProps {
children: React.ReactNode;
leftActions?: SwipeAction[];
rightActions?: SwipeAction[];
className?: string;
/** Width of each action button in px (default 72) */
actionWidth?: number;
}
/**
* Row that reveals swipe actions when the user drags left (right-actions)
* or right (left-actions). Used in the sidebar conversation list for
* one-swipe delete.
*/
export function SwipeableRow({
children,
leftActions = [],
rightActions = [],
className,
actionWidth = 72,
}: SwipeableRowProps) {
const [translateX, setTranslateX] = useState(0);
const [isDragging, setIsDragging] = useState(false);
const startXRef = useRef<number | null>(null);
const currentXRef = useRef(0);
const maxLeft = leftActions.length * actionWidth;
const maxRight = rightActions.length * actionWidth;
const handleTouchStart = useCallback((e: React.TouchEvent) => {
startXRef.current = e.touches[0].clientX;
setIsDragging(true);
}, []);
const handleTouchMove = useCallback(
(e: React.TouchEvent) => {
if (startXRef.current === null) return;
const dx = e.touches[0].clientX - startXRef.current + currentXRef.current;
const clamped = Math.max(-maxRight, Math.min(maxLeft, dx));
setTranslateX(clamped);
},
[maxLeft, maxRight]
);
const handleTouchEnd = useCallback(() => {
setIsDragging(false);
startXRef.current = null;
// Snap: if dragged > half an action width, show actions; otherwise reset
if (translateX < -(actionWidth / 2) && maxRight > 0) {
const snapped = -maxRight;
setTranslateX(snapped);
currentXRef.current = snapped;
} else if (translateX > actionWidth / 2 && maxLeft > 0) {
const snapped = maxLeft;
setTranslateX(snapped);
currentXRef.current = snapped;
} else {
setTranslateX(0);
currentXRef.current = 0;
}
}, [translateX, actionWidth, maxLeft, maxRight]);
const resetPosition = useCallback(() => {
setTranslateX(0);
currentXRef.current = 0;
}, []);
return (
<div className={cn("relative overflow-hidden", className)}>
{/* Left action buttons (revealed on swipe-right) */}
{leftActions.length > 0 && (
<div
className="absolute inset-y-0 left-0 flex"
style={{ width: maxLeft }}
>
{leftActions.map((action) => (
<button
key={action.label}
onClick={() => {
action.onClick();
resetPosition();
}}
className={cn(
"flex flex-col items-center justify-center gap-1 text-xs font-medium min-w-[44px]",
"bg-brand-600 text-white",
action.className
)}
style={{ width: actionWidth }}
>
{action.icon}
{action.label}
</button>
))}
</div>
)}
{/* Right action buttons (revealed on swipe-left) */}
{rightActions.length > 0 && (
<div
className="absolute inset-y-0 right-0 flex"
style={{ width: maxRight }}
>
{rightActions.map((action) => (
<button
key={action.label}
onClick={() => {
action.onClick();
resetPosition();
}}
className={cn(
"flex flex-col items-center justify-center gap-1 text-xs font-medium min-w-[44px]",
"bg-red-600 text-white",
action.className
)}
style={{ width: actionWidth }}
>
{action.icon}
{action.label}
</button>
))}
</div>
)}
{/* Content row */}
<div
className={cn(
"relative z-10 bg-surface-900",
!isDragging && "transition-transform duration-200"
)}
style={{ transform: `translateX(${translateX}px)` }}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
>
{children}
</div>
</div>
);
}