feat(ui): complete phase 4 Poyraz UI foundation
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "poyraz-ui/atoms";
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "poyraz-ui/molecules";
|
||||
|
||||
type DestructiveConfirmationProps = {
|
||||
cancelLabel?: string;
|
||||
confirmLabel: string;
|
||||
description: string;
|
||||
loading?: boolean;
|
||||
onConfirm: () => void;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
open: boolean;
|
||||
title: string;
|
||||
};
|
||||
|
||||
export function DestructiveConfirmation({
|
||||
cancelLabel = "Vazgeç",
|
||||
confirmLabel,
|
||||
description,
|
||||
loading = false,
|
||||
onConfirm,
|
||||
onOpenChange,
|
||||
open,
|
||||
title,
|
||||
}: DestructiveConfirmationProps) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent surface="solid" radius="lg" mobile="floating">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button type="button" variant="outline" disabled={loading}>
|
||||
{cancelLabel}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
loading={loading}
|
||||
aria-busy={loading}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
{confirmLabel}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { Button, Card, CardContent, Skeleton, Typography } from "poyraz-ui/atoms";
|
||||
import { Alert, AlertDescription, AlertTitle } from "poyraz-ui/molecules";
|
||||
import { Ban, CircleAlert, Inbox } from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
type FeedbackStateProps = {
|
||||
action?: ReactNode;
|
||||
description: string;
|
||||
title: string;
|
||||
variant: "empty" | "error" | "forbidden";
|
||||
};
|
||||
|
||||
export function FeedbackState({ action, description, title, variant }: FeedbackStateProps) {
|
||||
if (variant === "empty") {
|
||||
return (
|
||||
<Card variant="soft">
|
||||
<CardContent className="flex min-h-56 flex-col items-center justify-center gap-3 p-8 text-center">
|
||||
<span className="flex h-11 w-11 items-center justify-center rounded-full bg-primary-muted text-primary-muted-foreground">
|
||||
<Inbox className="h-5 w-5" aria-hidden="true" />
|
||||
</span>
|
||||
<div className="space-y-1">
|
||||
<Typography component="h2" variant="h4">{title}</Typography>
|
||||
<Typography component="p" variant="muted" className="max-w-lg">{description}</Typography>
|
||||
</div>
|
||||
{action}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const forbidden = variant === "forbidden";
|
||||
return (
|
||||
<Alert
|
||||
role="alert"
|
||||
variant={forbidden ? "warning" : "destructive"}
|
||||
appearance="soft"
|
||||
icon={forbidden ? <Ban aria-hidden="true" /> : <CircleAlert aria-hidden="true" />}
|
||||
>
|
||||
<AlertTitle>{title}</AlertTitle>
|
||||
<AlertDescription className="space-y-3">
|
||||
<p>{description}</p>
|
||||
{action}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
export function LoadingState({ label = "İçerik yükleniyor" }: { label?: string }) {
|
||||
return (
|
||||
<div className="space-y-4" aria-busy="true" aria-label={label} role="status">
|
||||
<span className="sr-only">{label}</span>
|
||||
<Skeleton className="h-8 w-2/5" />
|
||||
<Skeleton className="h-4 w-3/5" />
|
||||
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<Skeleton key={index} className="h-36 w-full" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function RetryAction({ onClick }: { onClick: () => void }) {
|
||||
return (
|
||||
<Button type="button" variant="outline" size="sm" onClick={onClick}>
|
||||
Yeniden dene
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Typography } from "poyraz-ui/atoms";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
type PageHeaderProps = {
|
||||
title: string;
|
||||
description?: string;
|
||||
eyebrow?: string;
|
||||
primaryAction?: ReactNode;
|
||||
secondaryActions?: ReactNode;
|
||||
};
|
||||
|
||||
export function PageHeader({
|
||||
title,
|
||||
description,
|
||||
eyebrow,
|
||||
primaryAction,
|
||||
secondaryActions,
|
||||
}: PageHeaderProps) {
|
||||
return (
|
||||
<header className="flex flex-col gap-4 border-b border-border pb-5 lg:flex-row lg:items-end lg:justify-between">
|
||||
<div className="min-w-0 space-y-1.5">
|
||||
{eyebrow ? (
|
||||
<Typography component="p" variant="caption" className="font-semibold uppercase tracking-wider text-primary">
|
||||
{eyebrow}
|
||||
</Typography>
|
||||
) : null}
|
||||
<Typography component="h1" variant="h1" balance>
|
||||
{title}
|
||||
</Typography>
|
||||
{description ? (
|
||||
<Typography component="p" variant="muted" className="max-w-3xl">
|
||||
{description}
|
||||
</Typography>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{primaryAction || secondaryActions ? (
|
||||
<div className="flex shrink-0 flex-wrap items-center gap-2">
|
||||
{secondaryActions}
|
||||
{primaryAction}
|
||||
</div>
|
||||
) : null}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Badge } from "poyraz-ui/atoms";
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
const statusPresentation = {
|
||||
accepted: { label: "Kabul edildi", variant: "success" },
|
||||
active: { label: "Aktif", variant: "success" },
|
||||
archived: { label: "Arşivlendi", variant: "outline" },
|
||||
cancelled: { label: "İptal edildi", variant: "outline" },
|
||||
completed: { label: "Tamamlandı", variant: "success" },
|
||||
done: { label: "Tamamlandı", variant: "success" },
|
||||
draft: { label: "Taslak", variant: "secondary" },
|
||||
expired: { label: "Süresi doldu", variant: "destructive" },
|
||||
in_progress: { label: "Devam ediyor", variant: "info" },
|
||||
overdue: { label: "Gecikmiş", variant: "destructive" },
|
||||
paid: { label: "Ödendi", variant: "success" },
|
||||
paused: { label: "Duraklatıldı", variant: "warning" },
|
||||
pending: { label: "Bekliyor", variant: "warning" },
|
||||
planned: { label: "Planlandı", variant: "secondary" },
|
||||
planning: { label: "Planlanıyor", variant: "secondary" },
|
||||
rejected: { label: "Reddedildi", variant: "destructive" },
|
||||
revoked: { label: "İptal edildi", variant: "destructive" },
|
||||
sent: { label: "Gönderildi", variant: "info" },
|
||||
todo: { label: "Yapılacak", variant: "secondary" },
|
||||
} as const satisfies Record<string, { label: string; variant: NonNullable<ComponentProps<typeof Badge>["variant"]> }>;
|
||||
|
||||
export type NetaStatus = keyof typeof statusPresentation;
|
||||
|
||||
type StatusBadgeProps = Omit<ComponentProps<typeof Badge>, "children" | "variant"> & {
|
||||
status: NetaStatus;
|
||||
};
|
||||
|
||||
export function StatusBadge({ status, ...props }: StatusBadgeProps) {
|
||||
const presentation = statusPresentation[status];
|
||||
return (
|
||||
<Badge variant={presentation.variant} {...props}>
|
||||
{presentation.label}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
export { statusPresentation };
|
||||
Reference in New Issue
Block a user