Mega UI revamp

This commit is contained in:
akhisud3195 2025-03-27 18:52:17 +05:30 committed by Ramnique Singh
parent 650f481a96
commit bcb686a20d
94 changed files with 6984 additions and 3889 deletions

View file

@ -0,0 +1,77 @@
import clsx from 'clsx';
import { ButtonHTMLAttributes, forwardRef } from "react";
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'tertiary';
size?: 'sm' | 'md' | 'lg';
startContent?: React.ReactNode;
endContent?: React.ReactNode;
isLoading?: boolean;
hoverContent?: React.ReactNode;
showHoverContent?: boolean;
}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(({
className,
variant = 'primary',
size = 'md',
startContent,
endContent,
isLoading,
children,
disabled,
hoverContent,
showHoverContent = false,
...props
}, ref) => {
return (
<button
ref={ref}
disabled={isLoading || disabled}
className={clsx(
"inline-flex items-center justify-center rounded-full font-medium transition-all",
"focus-visible:outline-none transform hover:scale-[1.02] hover:shadow-md",
"disabled:pointer-events-none disabled:opacity-50",
{
'primary': "text-sm px-3 py-1.5 bg-indigo-50 hover:bg-indigo-100 text-indigo-700 dark:bg-indigo-950 dark:hover:bg-indigo-900 dark:text-indigo-400",
'secondary': "bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-100",
'tertiary': "bg-transparent hover:bg-gray-100 text-gray-700 dark:hover:bg-gray-800 dark:text-gray-300",
}[variant],
{
'sm': "min-h-[2rem] px-3 text-sm py-1",
'md': "min-h-[2.5rem] px-4 py-1",
'lg': "min-h-[3rem] px-4 py-2 text-sm",
}[size],
"group",
className
)}
{...props}
>
{isLoading && (
<svg className="animate-spin -ml-1 mr-2 h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
)}
{startContent && (
<span className={clsx(
"shrink-0",
children || hoverContent ? "mr-2" : ""
)}>
{startContent}
</span>
)}
<span className="truncate">
{showHoverContent ? (
<>
<span className="group-hover:hidden">{children}</span>
<span className="hidden group-hover:inline">{hoverContent}</span>
</>
) : children}
</span>
{endContent && <span className="ml-2 shrink-0">{endContent}</span>}
</button>
);
});
Button.displayName = "Button";

View file

@ -0,0 +1,49 @@
import { Select, SelectItem, SelectProps } from "@heroui/react";
import { ReactNode, ChangeEvent } from "react";
export interface DropdownOption {
key: string;
label: string;
startContent?: ReactNode;
endContent?: ReactNode;
}
interface DropdownProps extends Omit<SelectProps, 'children' | 'onChange'> {
options: DropdownOption[];
value: string;
onChange: (value: string) => void;
className?: string;
width?: string | number;
containerClassName?: string;
}
export function Dropdown({
options,
value,
onChange,
className = "",
width = "100%",
containerClassName = "",
...selectProps
}: DropdownProps) {
return (
<div className={`${containerClassName}`} style={{ width }}>
<Select
{...selectProps}
selectedKeys={[value]}
onChange={(e: ChangeEvent<HTMLSelectElement>) => onChange(e.target.value)}
className={`${className}`}
>
{options.map((option) => (
<SelectItem
key={option.key}
startContent={option.startContent}
endContent={option.endContent}
>
{option.label}
</SelectItem>
))}
</Select>
</div>
);
}

View file

@ -0,0 +1,14 @@
import clsx from 'clsx';
interface HorizontalDividerProps {
className?: string;
}
export function HorizontalDivider({ className }: HorizontalDividerProps) {
return (
<div className={clsx(
"border-t border-gray-200 dark:border-gray-700",
className
)} />
);
}

View file

@ -0,0 +1,42 @@
import { clsx } from "clsx";
import { InputHTMLAttributes, forwardRef } from "react";
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
error?: string;
label?: string;
}
export const Input = forwardRef<HTMLInputElement, InputProps>(({
className,
error,
label,
...props
}, ref) => {
return (
<div className="space-y-2">
{label && (
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
{label}
</label>
)}
<input
ref={ref}
className={clsx(
"flex h-10 w-full rounded-md border border-gray-300 bg-white px-3 py-2",
"text-sm placeholder:text-gray-400",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400",
"disabled:cursor-not-allowed disabled:opacity-50",
"dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100",
error && "border-red-500 focus-visible:ring-red-500",
className
)}
{...props}
/>
{error && (
<p className="text-sm text-red-500">{error}</p>
)}
</div>
);
});
Input.displayName = "Input";

View file

@ -0,0 +1,32 @@
import clsx from 'clsx';
import { tokens } from "@/app/styles/design-tokens";
interface PageHeadingProps {
title: string;
description?: string;
}
export function PageHeading({ title, description }: PageHeadingProps) {
return (
<div>
<h1 className={clsx(
tokens.typography.weights.semibold,
tokens.typography.sizes["2xl"],
tokens.colors.light.text.primary,
tokens.colors.dark.text.primary
)}>
{title}
</h1>
{description && (
<p className={clsx(
"mt-2",
tokens.typography.sizes.base,
tokens.colors.light.text.secondary,
tokens.colors.dark.text.secondary
)}>
{description}
</p>
)}
</div>
);
}

View file

@ -3,14 +3,14 @@
import { GripVertical } from "lucide-react"
import * as ResizablePrimitive from "react-resizable-panels"
import { cn } from "../../lib/utils"
import clsx from 'clsx';
const ResizablePanelGroup = ({
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
<ResizablePrimitive.PanelGroup
className={cn(
className={clsx(
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
className
)}
@ -28,7 +28,7 @@ const ResizableHandle = ({
withHandle?: boolean
}) => (
<ResizablePrimitive.PanelResizeHandle
className={cn(
className={clsx(
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
className
)}

View file

@ -0,0 +1,44 @@
'use client';
import { Input } from "@/components/ui/input";
import { SearchIcon, XIcon } from "lucide-react";
import { InputHTMLAttributes } from "react";
import clsx from 'clsx';
interface SearchBarProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'onChange'> {
value: string;
onChange: (value: string) => void;
onClear?: () => void;
}
export function SearchBar({
value,
onChange,
onClear,
className,
...props
}: SearchBarProps) {
return (
<div className="relative">
<SearchIcon
size={16}
className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"
/>
<Input
type="text"
value={value}
onChange={(e) => onChange(e.target.value)}
className={clsx("pl-9 pr-8 bg-transparent", className)}
{...props}
/>
{value && (
<button
type="button"
onClick={onClear}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<XIcon size={14} />
</button>
)}
</div>
);
}

View file

@ -0,0 +1,32 @@
import clsx from 'clsx';
import { tokens } from "@/app/styles/design-tokens";
interface SectionHeadingProps {
children: React.ReactNode;
subheading?: React.ReactNode;
}
export function SectionHeading({ children, subheading }: SectionHeadingProps) {
return (
<div className="space-y-1">
<div className={clsx(
tokens.typography.weights.medium,
tokens.typography.sizes.lg,
tokens.colors.light.text.primary,
tokens.colors.dark.text.primary
)}>
{children}
</div>
{subheading && (
<p className={clsx(
tokens.typography.sizes.sm,
tokens.typography.weights.normal,
tokens.colors.light.text.secondary,
tokens.colors.dark.text.secondary
)}>
{subheading}
</p>
)}
</div>
);
}

View file

@ -0,0 +1,151 @@
import clsx from 'clsx';
import { TextareaHTMLAttributes, forwardRef, useEffect, useRef, useState } from "react";
interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
label?: string;
autoResize?: boolean;
maxHeight?: number;
useValidation?: boolean;
validate?: (value: string) => { valid: boolean; errorMessage?: string };
onValidatedChange?: (value: string) => void;
updateOnBlur?: boolean;
}
export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(({
className,
label,
autoResize = false,
maxHeight = 120, // default max height (roughly 5 lines)
value: propValue,
onChange,
// New validation props
useValidation = false,
validate,
onValidatedChange,
updateOnBlur = false,
onBlur,
onKeyDown,
...props
}, ref) => {
const internalRef = useRef<HTMLTextAreaElement>(null);
const textareaRef = (ref as any) || internalRef;
// Local state for validation mode
const [localValue, setLocalValue] = useState(propValue as string);
const [validationError, setValidationError] = useState<string | undefined>();
const [isEditing, setIsEditing] = useState(false);
// Sync local state with prop value when not editing
useEffect(() => {
if (!isEditing) {
setLocalValue(propValue as string);
}
}, [propValue, isEditing]);
useEffect(() => {
if (!autoResize) return;
const textarea = textareaRef.current;
if (!textarea) return;
const adjustHeight = () => {
textarea.style.height = 'auto';
const scrollHeight = textarea.scrollHeight;
textarea.style.height = `${Math.min(scrollHeight, maxHeight)}px`;
// Add scrolling if content exceeds maxHeight
textarea.style.overflowY = scrollHeight > maxHeight ? 'auto' : 'hidden';
};
adjustHeight();
// Add window resize listener
window.addEventListener('resize', adjustHeight);
return () => window.removeEventListener('resize', adjustHeight);
}, [localValue, autoResize, maxHeight, textareaRef]);
const validateAndUpdate = (value: string) => {
if (validate) {
const result = validate(value);
setValidationError(result.errorMessage);
if (result.valid && onValidatedChange) {
onValidatedChange(value);
return true;
}
return false;
} else if (onValidatedChange) {
onValidatedChange(value);
return true;
}
return false;
};
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const newValue = e.target.value;
setLocalValue(newValue);
setIsEditing(true);
if (!updateOnBlur) {
if (useValidation) {
validateAndUpdate(newValue);
} else {
onChange?.(e);
}
}
};
const handleBlur = (e: React.FocusEvent<HTMLTextAreaElement>) => {
setIsEditing(false);
if (updateOnBlur) {
if (useValidation) {
validateAndUpdate(localValue);
} else {
const syntheticEvent = {
...e,
target: { ...e.target, value: localValue },
currentTarget: { ...e.currentTarget, value: localValue }
};
onChange?.(syntheticEvent as any);
}
}
onBlur?.(e);
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (updateOnBlur && e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
textareaRef.current?.blur();
}
onKeyDown?.(e);
};
return (
<div className="space-y-2">
{label && (
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
{label}
</label>
)}
<textarea
ref={textareaRef}
onChange={handleChange}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
value={localValue}
className={clsx(
"flex w-full text-sm focus-visible:outline-none",
"disabled:cursor-not-allowed disabled:opacity-50",
"transition-colors",
className
)}
style={{
...props.style,
minHeight: autoResize ? '24px' : undefined,
}}
{...props}
/>
</div>
);
});
Textarea.displayName = "Textarea";