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:
m7amedez5511
2026-06-08 23:05:19 +03:00
parent 70853958f5
commit 53c03e9867
45 changed files with 1408 additions and 78 deletions

59
Components/UI/Alert.tsx Normal file
View 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
View 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
View 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
View 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
View 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";