finshing architecture for project now we have all layers to build profitionl project app,components ,hooks ,lib,services,styel ,types
This commit is contained in:
59
Components/UI/Alert.tsx
Normal file
59
Components/UI/Alert.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
// Typed, accessible alert banner with optional dismiss button.
|
||||
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
export type AlertType = "error" | "info" | "success" | "warning";
|
||||
|
||||
export interface AlertProps {
|
||||
type?: AlertType;
|
||||
title?: string;
|
||||
message: string;
|
||||
onClose?: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const config: Record<AlertType, { bg: string; icon: string; iconLabel: string }> = {
|
||||
error: { bg: "bg-red-50 border-red-200 text-red-700", icon: "⚠", iconLabel: "Error" },
|
||||
info: { bg: "bg-blue-50 border-blue-200 text-blue-700", icon: "ℹ", iconLabel: "Info" },
|
||||
success: { bg: "bg-emerald-50 border-emerald-200 text-emerald-700", icon: "✓", iconLabel: "Success" },
|
||||
warning: { bg: "bg-amber-50 border-amber-200 text-amber-700", icon: "!", iconLabel: "Warning" },
|
||||
};
|
||||
|
||||
/**
|
||||
* @example
|
||||
* <Alert type="error" message="Invalid credentials." onClose={() => setErr(null)} />
|
||||
* <Alert type="success" title="Saved!" message="Your changes have been applied." />
|
||||
*/
|
||||
export function Alert({ type = "error", title, message, onClose, className }: AlertProps) {
|
||||
const { bg, icon, iconLabel } = config[type];
|
||||
|
||||
return (
|
||||
<div
|
||||
role="alert"
|
||||
aria-live="polite"
|
||||
className={cn(
|
||||
"flex items-start gap-2 border rounded-[var(--radius-md)] px-3 py-2.5 text-[12px] font-medium",
|
||||
bg,
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<span aria-label={iconLabel} className="flex-shrink-0 mt-0.5">
|
||||
{icon}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
{title && <p className="font-semibold mb-0.5">{title}</p>}
|
||||
<p>{message}</p>
|
||||
</div>
|
||||
{onClose && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
aria-label="Dismiss alert"
|
||||
className="flex-shrink-0 opacity-60 hover:opacity-100 text-[16px] leading-none transition-opacity"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
74
Components/UI/Button.tsx
Normal file
74
Components/UI/Button.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
// Design-system button with variant, size, and loading state support.
|
||||
|
||||
import { cn } from "../../lib/utils";
|
||||
import { Spinner } from "./Spinner";
|
||||
|
||||
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
loading?: boolean;
|
||||
variant?: "primary" | "secondary" | "ghost" | "danger";
|
||||
size?: "sm" | "md" | "lg";
|
||||
/** Full-width block button */
|
||||
fullWidth?: boolean;
|
||||
}
|
||||
|
||||
const variantStyles: Record<NonNullable<ButtonProps["variant"]>, string> = {
|
||||
primary:
|
||||
"bg-[var(--color-brand-600)] hover:bg-[var(--color-brand-700)] text-white shadow-sm",
|
||||
secondary:
|
||||
"bg-white border border-[var(--color-border)] text-[var(--color-text-primary)] hover:bg-[var(--color-surface-muted)]",
|
||||
ghost:
|
||||
"text-[var(--color-text-muted)] hover:bg-[var(--color-surface-muted)]",
|
||||
danger:
|
||||
"bg-red-600 hover:bg-red-700 text-white",
|
||||
};
|
||||
|
||||
const sizeStyles: Record<NonNullable<ButtonProps["size"]>, string> = {
|
||||
sm: "h-8 px-3 text-[12px] gap-1.5",
|
||||
md: "h-10 px-4 text-[13px] gap-2",
|
||||
lg: "h-11 px-6 text-[14px] gap-2",
|
||||
};
|
||||
|
||||
/**
|
||||
* @example
|
||||
* <Button onClick={handleSave}>Save</Button>
|
||||
* <Button variant="secondary" size="sm">Cancel</Button>
|
||||
* <Button loading={isSubmitting} fullWidth>Submit</Button>
|
||||
* <Button variant="danger">Delete</Button>
|
||||
*/
|
||||
export function Button({
|
||||
loading,
|
||||
variant = "primary",
|
||||
size = "md",
|
||||
fullWidth,
|
||||
children,
|
||||
className,
|
||||
disabled,
|
||||
...rest
|
||||
}: ButtonProps) {
|
||||
return (
|
||||
<button
|
||||
disabled={loading || disabled}
|
||||
aria-busy={loading}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center rounded-[var(--radius-md)] font-semibold",
|
||||
"transition-all active:scale-[.98]",
|
||||
"disabled:opacity-60 disabled:cursor-not-allowed",
|
||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-brand-600)] focus-visible:ring-offset-2",
|
||||
variantStyles[variant],
|
||||
sizeStyles[size],
|
||||
fullWidth && "w-full",
|
||||
className,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{loading && (
|
||||
<Spinner
|
||||
size="sm"
|
||||
className="text-current"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
91
Components/UI/Input.tsx
Normal file
91
Components/UI/Input.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
// Accessible labelled input with error and hint state.
|
||||
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
/** Visible label text — also used to derive the `htmlFor` id */
|
||||
label: string;
|
||||
/** Validation error message — sets aria-invalid and renders red helper text */
|
||||
error?: string;
|
||||
/** Neutral helper / hint text shown below the input */
|
||||
hint?: string;
|
||||
/** Optional right-side icon node */
|
||||
icon?: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @example
|
||||
* <Input
|
||||
* label="Email"
|
||||
* type="email"
|
||||
* placeholder="you@example.com"
|
||||
* error={errors.email}
|
||||
* autoComplete="email"
|
||||
* />
|
||||
*
|
||||
* <Input
|
||||
* label="Password"
|
||||
* type="password"
|
||||
* hint="At least 8 characters"
|
||||
* />
|
||||
*/
|
||||
export function Input({ label, error, hint, id, icon, className, ...rest }: InputProps) {
|
||||
const inputId = id ?? `input-${label.toLowerCase().replace(/\s+/g, "-")}`;
|
||||
const errorId = `${inputId}-error`;
|
||||
const hintId = `${inputId}-hint`;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<label
|
||||
htmlFor={inputId}
|
||||
className="text-[12px] font-semibold text-[var(--color-text-primary)]"
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
|
||||
<div className="relative">
|
||||
{icon && (
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-[var(--color-text-hint)] pointer-events-none"
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<input
|
||||
id={inputId}
|
||||
aria-invalid={!!error}
|
||||
aria-describedby={
|
||||
error ? errorId : hint ? hintId : undefined
|
||||
}
|
||||
className={cn(
|
||||
"w-full h-10 px-3 border rounded-[var(--radius-md)] text-[13px]",
|
||||
"text-[var(--color-text-primary)] placeholder-[var(--color-text-hint)]",
|
||||
"bg-white outline-none transition",
|
||||
"focus:border-[var(--color-brand-600)] focus:ring-2 focus:ring-[var(--color-brand-600)]/15",
|
||||
"focus-visible:ring-[var(--color-brand-600)]",
|
||||
icon ? "pr-9" : "",
|
||||
error
|
||||
? "border-red-400 bg-red-50"
|
||||
: "border-[var(--color-border)]",
|
||||
className,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<p id={errorId} role="alert" className="text-[11px] text-red-500 font-medium">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{hint && !error && (
|
||||
<p id={hintId} className="text-[11px] text-[var(--color-text-hint)]">
|
||||
{hint}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
80
Components/UI/Spinner.tsx
Normal file
80
Components/UI/Spinner.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
export interface SpinnerProps {
|
||||
/** Visual size of the spinner */
|
||||
size?: "sm" | "md" | "lg";
|
||||
/** Additional Tailwind classes — use `text-{color}` to tint */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const sizeMap: Record<NonNullable<SpinnerProps["size"]>, string> = {
|
||||
sm: "h-4 w-4",
|
||||
md: "h-6 w-6",
|
||||
lg: "h-10 w-10 border-4",
|
||||
};
|
||||
|
||||
/**
|
||||
* @example
|
||||
* // Default (medium, brand blue)
|
||||
* <Spinner />
|
||||
*
|
||||
* // Large, white (inside a dark button)
|
||||
* <Spinner size="lg" className="text-white" />
|
||||
*
|
||||
* // Small, inline
|
||||
* <Spinner size="sm" className="text-emerald-600" />
|
||||
*/
|
||||
export function Spinner({ size = "md", className }: SpinnerProps) {
|
||||
return (
|
||||
<svg
|
||||
role="status"
|
||||
aria-label="Loading"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
className={cn(
|
||||
"motion-safe:animate-spin text-[var(--color-brand-600)] shrink-0",
|
||||
sizeMap[size],
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
/>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8v8H4z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** Full-page loading overlay */
|
||||
export function PageLoader({ message = "Loading…" }: { message?: string }) {
|
||||
return (
|
||||
<div
|
||||
role="status"
|
||||
aria-label={message}
|
||||
className="flex min-h-screen flex-col items-center justify-center gap-4 bg-[var(--color-surface)]"
|
||||
>
|
||||
<Spinner size="lg" />
|
||||
<p className="text-[13px] text-[var(--color-text-muted)]">{message}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Inline loading row — for card/section loading states */
|
||||
export function InlineLoader({ message = "Loading…" }: { message?: string }) {
|
||||
return (
|
||||
<div role="status" className="flex items-center gap-2 py-4">
|
||||
<Spinner size="sm" />
|
||||
<span className="text-[13px] text-[var(--color-text-muted)]">{message}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
12
Components/UI/index.ts
Normal file
12
Components/UI/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
// components/ui/index.ts
|
||||
export { Spinner, PageLoader, InlineLoader } from "./Spinner";
|
||||
export type { SpinnerProps } from "./Spinner";
|
||||
|
||||
export { Alert } from "./Alert";
|
||||
export type { AlertProps, AlertType } from "./Alert";
|
||||
|
||||
export { Button } from "./Button";
|
||||
export type { ButtonProps } from "./Button";
|
||||
|
||||
export { Input } from "./Input";
|
||||
export type { InputProps } from "./Input";
|
||||
Reference in New Issue
Block a user