Refactor root admin dashboard UI
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
|
||||
type DashboardCardProps = {
|
||||
title: string;
|
||||
value: string;
|
||||
description: string;
|
||||
icon: LucideIcon;
|
||||
};
|
||||
|
||||
export function DashboardCard({
|
||||
title,
|
||||
value,
|
||||
description,
|
||||
icon: Icon,
|
||||
}: DashboardCardProps) {
|
||||
return (
|
||||
<Card className="border-border/70 bg-card/95 shadow-sm">
|
||||
<CardHeader className="flex flex-row items-start justify-between space-y-0 pb-3">
|
||||
<div className="space-y-1">
|
||||
<CardDescription>{title}</CardDescription>
|
||||
<CardTitle className="text-3xl font-semibold tracking-tight">{value}</CardTitle>
|
||||
</div>
|
||||
<div className="rounded-md border border-border bg-muted/60 p-2 text-muted-foreground">
|
||||
<Icon className="h-4 w-4" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">{description}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { Menu, type LucideIcon } from "lucide-react";
|
||||
|
||||
import { DashboardSidebar } from "@/components/dashboard/sidebar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from "@/components/ui/sheet";
|
||||
import type { RootNavItem } from "@/lib/root-navigation";
|
||||
|
||||
type DashboardLayoutProps = {
|
||||
title: string;
|
||||
description: string;
|
||||
icon?: LucideIcon;
|
||||
items: RootNavItem[];
|
||||
sidebarFooter?: ReactNode;
|
||||
headerActions?: ReactNode;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export function DashboardLayout({
|
||||
title,
|
||||
description,
|
||||
icon: Icon,
|
||||
items,
|
||||
sidebarFooter,
|
||||
headerActions,
|
||||
children,
|
||||
}: DashboardLayoutProps) {
|
||||
return (
|
||||
<div className="min-h-screen bg-muted/30">
|
||||
<div className="grid min-h-screen lg:grid-cols-[280px_minmax(0,1fr)]">
|
||||
<aside className="hidden border-r border-border/70 bg-sidebar/60 lg:block">
|
||||
<DashboardSidebar items={items} footer={sidebarFooter} />
|
||||
</aside>
|
||||
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
<header className="sticky top-0 z-30 flex h-16 items-center gap-3 border-b border-border/70 bg-background/95 px-4 backdrop-blur lg:px-6">
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="outline" size="icon" className="lg:hidden">
|
||||
<Menu className="h-4 w-4" />
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="left" className="p-0">
|
||||
<SheetHeader className="sr-only">
|
||||
<SheetTitle>{title}</SheetTitle>
|
||||
<SheetDescription>{description}</SheetDescription>
|
||||
</SheetHeader>
|
||||
<DashboardSidebar items={items} footer={sidebarFooter} />
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
<div className="flex min-w-0 flex-1 items-center gap-3">
|
||||
{Icon ? (
|
||||
<div className="hidden rounded-lg border border-border/70 bg-muted/50 p-2 text-muted-foreground sm:flex">
|
||||
<Icon className="h-4 w-4" />
|
||||
</div>
|
||||
) : null}
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-lg font-semibold text-foreground">{title}</p>
|
||||
<p className="truncate text-sm text-muted-foreground">{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{headerActions ? (
|
||||
<div className="flex items-center gap-2">{headerActions}</div>
|
||||
) : null}
|
||||
</header>
|
||||
|
||||
<main className="flex-1 p-4 lg:p-6">{children}</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import type { ReactNode } from "react";
|
||||
import Link from "next/link";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import { ShieldCheck } from "lucide-react";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type DashboardSidebarItem = {
|
||||
label: string;
|
||||
href: string;
|
||||
icon: LucideIcon;
|
||||
active?: boolean;
|
||||
children?: DashboardSidebarItem[];
|
||||
};
|
||||
|
||||
type DashboardSidebarProps = {
|
||||
items: DashboardSidebarItem[];
|
||||
footer?: ReactNode;
|
||||
};
|
||||
|
||||
export function DashboardSidebar({ items, footer }: DashboardSidebarProps) {
|
||||
function renderItem(item: DashboardSidebarItem, nested = false) {
|
||||
const Icon = item.icon;
|
||||
|
||||
return (
|
||||
<div key={item.href} className="space-y-1">
|
||||
<Link
|
||||
href={item.href}
|
||||
className={cn(
|
||||
"flex items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors",
|
||||
item.active
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
||||
nested ? "ml-4" : undefined,
|
||||
)}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
<span>{item.label}</span>
|
||||
</Link>
|
||||
{item.children?.length ? item.children.map((child) => renderItem(child, true)) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col bg-sidebar/40">
|
||||
<div className="flex items-center gap-3 px-4 py-5">
|
||||
<div className="rounded-xl bg-primary/10 p-2 text-primary">
|
||||
<ShieldCheck className="h-5 w-5" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-semibold text-foreground">Mohs Admin</p>
|
||||
<p className="truncate text-xs text-muted-foreground">Website owner workspace</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-4">
|
||||
<div className="rounded-xl border border-border/70 bg-background/80 p-4">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-foreground">Private panel</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">Single owner access</p>
|
||||
</div>
|
||||
<Badge variant="outline">Private</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator className="my-4" />
|
||||
|
||||
<div className="flex-1 space-y-6 overflow-y-auto px-4 pb-4">
|
||||
<nav className="space-y-1">{items.map((item) => renderItem(item))}</nav>
|
||||
</div>
|
||||
|
||||
{footer ? (
|
||||
<>
|
||||
<Separator />
|
||||
<div className="space-y-2 p-4">{footer}</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -27,59 +27,76 @@ export function AppSidebar({
|
||||
items,
|
||||
footer,
|
||||
}: AppSidebarProps) {
|
||||
const portfolioItem = items.find((item) => item.href === "/root/portfolio");
|
||||
const systemOrder = ["/root", "/root/media", "/root/maintenance", "/root/ui-kit"];
|
||||
const systemItems = items
|
||||
.filter((item) => item.href !== "/root/portfolio")
|
||||
.sort((left, right) => systemOrder.indexOf(left.href) - systemOrder.indexOf(right.href));
|
||||
|
||||
function renderItem(item: SidebarItem, nested = false) {
|
||||
const Icon = item.icon;
|
||||
|
||||
return (
|
||||
<div key={item.label} className="space-y-1">
|
||||
<Link
|
||||
href={item.href}
|
||||
className={cn(
|
||||
"flex items-center gap-2 rounded-nested px-3 py-2 text-sm transition-colors",
|
||||
item.active
|
||||
? "bg-sidebar-primary text-sidebar-primary-foreground"
|
||||
: "text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||
nested ? "text-sidebar-foreground/72" : undefined,
|
||||
)}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
<span>{item.label}</span>
|
||||
</Link>
|
||||
|
||||
{item.children?.length ? (
|
||||
<div className="ml-3 space-y-1 border-l border-sidebar-border pl-3">
|
||||
{item.children.map((child) => renderItem(child, true))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AppCard className="sticky top-6 overflow-hidden bg-sidebar text-sidebar-foreground shadow-sidebar">
|
||||
<CardHeader className="pb-4">
|
||||
<CardTitle className="text-base font-semibold">{title}</CardTitle>
|
||||
<p className="text-sm text-sidebar-foreground/72">{description}</p>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-1 px-3 pb-3 pt-0">
|
||||
{items.map((item) => {
|
||||
const Icon = item.icon;
|
||||
<div className="sticky top-6 space-y-4">
|
||||
<AppCard className="overflow-hidden bg-sidebar text-sidebar-foreground shadow-sidebar">
|
||||
<CardHeader className="pb-4">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-sidebar-foreground/56">
|
||||
Root Access
|
||||
</p>
|
||||
<CardTitle className="text-base font-semibold">{title}</CardTitle>
|
||||
<p className="text-sm leading-6 text-sidebar-foreground/72">{description}</p>
|
||||
</CardHeader>
|
||||
{footer ? <div className="border-t border-sidebar-border px-4 py-3">{footer}</div> : null}
|
||||
</AppCard>
|
||||
|
||||
return (
|
||||
<div key={item.label} className="space-y-1">
|
||||
<Link
|
||||
href={item.href}
|
||||
className={cn(
|
||||
"flex items-center gap-2 rounded-nested px-3 py-2 text-sm transition-colors",
|
||||
item.active
|
||||
? "bg-sidebar-primary text-sidebar-primary-foreground"
|
||||
: "text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||
)}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
<span>{item.label}</span>
|
||||
</Link>
|
||||
<AppCard className="overflow-hidden bg-sidebar text-sidebar-foreground shadow-sidebar">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-semibold uppercase tracking-[0.2em] text-sidebar-foreground/72">
|
||||
Overview
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-1 px-3 pb-3 pt-0">
|
||||
{systemItems.map((item) => renderItem(item))}
|
||||
</CardContent>
|
||||
</AppCard>
|
||||
|
||||
{item.children?.length ? (
|
||||
<div className="ml-3 space-y-1 border-l border-sidebar-border pl-3">
|
||||
{item.children.map((child) => {
|
||||
const ChildIcon = child.icon;
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={child.label}
|
||||
href={child.href}
|
||||
className={cn(
|
||||
"flex items-center gap-2 rounded-nested px-3 py-2 text-sm transition-colors",
|
||||
child.active
|
||||
? "bg-sidebar-primary text-sidebar-primary-foreground"
|
||||
: "text-sidebar-foreground/72 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||
)}
|
||||
>
|
||||
<ChildIcon className="h-4 w-4" />
|
||||
<span>{child.label}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</CardContent>
|
||||
{footer ? <div className="border-t border-sidebar-border px-4 py-3">{footer}</div> : null}
|
||||
</AppCard>
|
||||
{portfolioItem ? (
|
||||
<AppCard className="overflow-hidden bg-sidebar text-sidebar-foreground shadow-sidebar">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-semibold uppercase tracking-[0.2em] text-sidebar-foreground/72">
|
||||
Portfolio
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-1 px-3 pb-3 pt-0">
|
||||
{renderItem(portfolioItem)}
|
||||
</CardContent>
|
||||
</AppCard>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -199,11 +199,11 @@ export function PortfolioProjectForm({
|
||||
|
||||
<AppCard>
|
||||
<CardHeader>
|
||||
<CardTitle>Project Basics</CardTitle>
|
||||
<CardTitle>Projekt Basisdaten</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-4 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="categoryId">Category</Label>
|
||||
<Label htmlFor="categoryId">Kategorie</Label>
|
||||
<select
|
||||
id="categoryId"
|
||||
name="categoryId"
|
||||
@@ -225,7 +225,7 @@ export function PortfolioProjectForm({
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="clientName">Client Name</Label>
|
||||
<Label htmlFor="clientName">Kundenname</Label>
|
||||
<Input
|
||||
id="clientName"
|
||||
name="clientName"
|
||||
@@ -235,7 +235,7 @@ export function PortfolioProjectForm({
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="projectYear">Project Year</Label>
|
||||
<Label htmlFor="projectYear">Projektjahr</Label>
|
||||
<Input
|
||||
id="projectYear"
|
||||
name="projectYear"
|
||||
@@ -259,7 +259,7 @@ export function PortfolioProjectForm({
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="sortOrder">Sort Order</Label>
|
||||
<Label htmlFor="sortOrder">Sortierung</Label>
|
||||
<Input
|
||||
id="sortOrder"
|
||||
name="sortOrder"
|
||||
@@ -272,7 +272,7 @@ export function PortfolioProjectForm({
|
||||
|
||||
<div className="space-y-2 md:col-span-2">
|
||||
<MediaFieldPicker
|
||||
title="Cover Image"
|
||||
title="Cover Bild"
|
||||
value={coverMedia}
|
||||
onChange={setCoverMedia}
|
||||
options={mediaOptions}
|
||||
@@ -284,19 +284,19 @@ export function PortfolioProjectForm({
|
||||
|
||||
<label className="flex items-center gap-3 rounded-nested border border-border px-4 py-3 text-sm">
|
||||
<input type="checkbox" name="isFeatured" defaultChecked={project?.isFeatured ?? false} />
|
||||
Featured
|
||||
Hervorgehoben
|
||||
</label>
|
||||
|
||||
<label className="flex items-center gap-3 rounded-nested border border-border px-4 py-3 text-sm">
|
||||
<input type="checkbox" name="isPublished" defaultChecked={project?.isPublished ?? false} />
|
||||
Published
|
||||
Veroeffentlicht
|
||||
</label>
|
||||
</CardContent>
|
||||
</AppCard>
|
||||
|
||||
<AppCard>
|
||||
<CardHeader>
|
||||
<CardTitle>Localized Content</CardTitle>
|
||||
<CardTitle>Lokalisierte Inhalte</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs defaultValue="de">
|
||||
@@ -312,13 +312,13 @@ export function PortfolioProjectForm({
|
||||
<div className="grid gap-4 rounded-surface border border-border p-4 md:grid-cols-2">
|
||||
<div className="md:col-span-2 text-sm text-muted-foreground">
|
||||
{locale.key === "ar"
|
||||
? "Arabic content for the Arabic website."
|
||||
? "Arabische Inhalte fuer die arabische Website."
|
||||
: locale.key === "en"
|
||||
? "English content for the English website."
|
||||
: "German content for the German website."}
|
||||
? "Englische Inhalte fuer die englische Website."
|
||||
: "Deutsche Inhalte fuer die deutsche Website."}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={`title${locale.suffix}`}>{`Title ${locale.label}`}</Label>
|
||||
<Label htmlFor={`title${locale.suffix}`}>{`Titel ${locale.label}`}</Label>
|
||||
<Input
|
||||
id={`title${locale.suffix}`}
|
||||
name={`title${locale.suffix}`}
|
||||
@@ -327,7 +327,7 @@ export function PortfolioProjectForm({
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={`serviceLabel${locale.suffix}`}>{`Service Label ${locale.label}`}</Label>
|
||||
<Label htmlFor={`serviceLabel${locale.suffix}`}>{`Leistungslabel ${locale.label}`}</Label>
|
||||
<Input
|
||||
id={`serviceLabel${locale.suffix}`}
|
||||
name={`serviceLabel${locale.suffix}`}
|
||||
@@ -336,7 +336,7 @@ export function PortfolioProjectForm({
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2 md:col-span-2">
|
||||
<Label htmlFor={`summary${locale.suffix}`}>{`Summary ${locale.label}`}</Label>
|
||||
<Label htmlFor={`summary${locale.suffix}`}>{`Kurzbeschreibung ${locale.label}`}</Label>
|
||||
<Textarea
|
||||
id={`summary${locale.suffix}`}
|
||||
name={`summary${locale.suffix}`}
|
||||
@@ -354,14 +354,14 @@ export function PortfolioProjectForm({
|
||||
|
||||
<AppCard>
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardTitle>Sections</CardTitle>
|
||||
<CardTitle>Abschnitte</CardTitle>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setSections((current) => [...current, createEmptySection(current.length)])}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
Add Section
|
||||
Abschnitt hinzufuegen
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
@@ -369,7 +369,7 @@ export function PortfolioProjectForm({
|
||||
<AppCard key={section.id ?? `${section.type}-${index}`} level={2}>
|
||||
<CardContent className="space-y-4 p-4">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<p className="text-sm font-medium text-foreground">Section #{index + 1}</p>
|
||||
<p className="text-sm font-medium text-foreground">{`Abschnitt #${index + 1}`}</p>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
@@ -401,14 +401,14 @@ export function PortfolioProjectForm({
|
||||
disabled={sections.length === 1}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
Remove
|
||||
Entfernen
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label>Type</Label>
|
||||
<Label>Typ</Label>
|
||||
<select
|
||||
value={section.type}
|
||||
onChange={(event) =>
|
||||
@@ -432,7 +432,7 @@ export function PortfolioProjectForm({
|
||||
|
||||
<div className="space-y-2 md:col-span-2">
|
||||
<MediaFieldPicker
|
||||
title="Section Image"
|
||||
title="Abschnitt Bild"
|
||||
value={section.media}
|
||||
onChange={(media) =>
|
||||
setSections((current) =>
|
||||
@@ -470,7 +470,7 @@ export function PortfolioProjectForm({
|
||||
|
||||
{localeFieldConfig.map((locale) => (
|
||||
<div key={`${locale.key}-title-${index}`} className="space-y-2">
|
||||
<Label>{`Title ${locale.label}`}</Label>
|
||||
<Label>{`Titel ${locale.label}`}</Label>
|
||||
<Input
|
||||
value={section[`title${locale.suffix}` as keyof SectionFormValue] as string}
|
||||
onChange={(event) =>
|
||||
@@ -491,7 +491,7 @@ export function PortfolioProjectForm({
|
||||
|
||||
{localeFieldConfig.map((locale) => (
|
||||
<div key={`${locale.key}-body-${index}`} className="space-y-2 md:col-span-2">
|
||||
<Label>{`Body ${locale.label}`}</Label>
|
||||
<Label>{`Text ${locale.label}`}</Label>
|
||||
<Textarea
|
||||
rows={4}
|
||||
value={section[`body${locale.suffix}` as keyof SectionFormValue] as string}
|
||||
@@ -519,26 +519,26 @@ export function PortfolioProjectForm({
|
||||
|
||||
<AppCard>
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardTitle>Assets</CardTitle>
|
||||
<CardTitle>Dateien</CardTitle>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setAssets((current) => [...current, createEmptyAsset(current.length)])}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
Add Asset
|
||||
Datei hinzufuegen
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{assets.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">No assets added yet.</p>
|
||||
<p className="text-sm text-muted-foreground">Noch keine Dateien hinzugefuegt.</p>
|
||||
) : null}
|
||||
|
||||
{assets.map((asset, index) => (
|
||||
<AppCard key={asset.id ?? `${asset.kind}-${index}`} level={2}>
|
||||
<CardContent className="space-y-4 p-4">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<p className="text-sm font-medium text-foreground">Asset #{index + 1}</p>
|
||||
<p className="text-sm font-medium text-foreground">{`Datei #${index + 1}`}</p>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
@@ -565,14 +565,14 @@ export function PortfolioProjectForm({
|
||||
}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
Remove
|
||||
Entfernen
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label>Kind</Label>
|
||||
<Label>Typ</Label>
|
||||
<select
|
||||
value={asset.kind}
|
||||
onChange={(event) =>
|
||||
@@ -603,7 +603,7 @@ export function PortfolioProjectForm({
|
||||
|
||||
<div className="space-y-2 md:col-span-2">
|
||||
<MediaFieldPicker
|
||||
title="Asset File"
|
||||
title="Datei"
|
||||
value={asset.media}
|
||||
onChange={(media) =>
|
||||
setAssets((current) =>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import Link from "next/link";
|
||||
|
||||
import { AppCard } from "@/components/ui/app-card";
|
||||
import { CardContent } from "@/components/ui/card";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type PortfolioSubnavProps = {
|
||||
@@ -9,38 +11,40 @@ type PortfolioSubnavProps = {
|
||||
const items = [
|
||||
{
|
||||
key: "overview",
|
||||
label: "Overview",
|
||||
label: "Uebersicht",
|
||||
href: "/root/portfolio",
|
||||
},
|
||||
{
|
||||
key: "categories",
|
||||
label: "Categories",
|
||||
label: "Kategorien",
|
||||
href: "/root/portfolio/categories",
|
||||
},
|
||||
{
|
||||
key: "projects",
|
||||
label: "Projects",
|
||||
label: "Projekte",
|
||||
href: "/root/portfolio/projects",
|
||||
},
|
||||
] as const;
|
||||
|
||||
export function PortfolioSubnav({ active }: PortfolioSubnavProps) {
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{items.map((item) => (
|
||||
<Link
|
||||
key={item.key}
|
||||
href={item.href}
|
||||
className={cn(
|
||||
"rounded-pill border px-4 py-2 text-sm transition-colors",
|
||||
active === item.key
|
||||
? "border-border-strong bg-foreground text-background"
|
||||
: "border-border bg-background text-foreground/75 hover:border-border-strong hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<AppCard level={2}>
|
||||
<CardContent className="flex flex-wrap gap-2 p-4">
|
||||
{items.map((item) => (
|
||||
<Link
|
||||
key={item.key}
|
||||
href={item.href}
|
||||
className={cn(
|
||||
"rounded-pill border px-4 py-2 text-sm transition-colors",
|
||||
active === item.key
|
||||
? "border-border-strong bg-foreground text-background"
|
||||
: "border-border bg-background text-foreground/75 hover:border-border-strong hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
</CardContent>
|
||||
</AppCard>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { ArrowLeft, LogOut } from "lucide-react";
|
||||
import {
|
||||
ArrowLeft,
|
||||
FolderKanban,
|
||||
ImageIcon,
|
||||
LayoutDashboard,
|
||||
LogOut,
|
||||
PlusSquare,
|
||||
ShieldAlert,
|
||||
SwatchBook,
|
||||
Tags,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
import { AppHeader } from "@/components/layout/app-header";
|
||||
import { AppShell } from "@/components/layout/app-shell";
|
||||
import { AppSidebar } from "@/components/layout/app-sidebar";
|
||||
import { DashboardLayout } from "@/components/dashboard/dashboard-layout";
|
||||
import { MotionFade } from "@/components/motion-fade";
|
||||
import { ThemeToggle } from "@/components/theme-toggle";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { getLocalizedPath } from "@/lib/locale";
|
||||
@@ -30,6 +39,7 @@ type RootDashboardShellProps = {
|
||||
headerTitle: string;
|
||||
headerDescription: string;
|
||||
headerActions?: ReactNode;
|
||||
toolbar?: ReactNode;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
@@ -41,47 +51,85 @@ export function RootDashboardShell({
|
||||
headerTitle,
|
||||
headerDescription,
|
||||
headerActions,
|
||||
toolbar,
|
||||
children,
|
||||
}: RootDashboardShellProps) {
|
||||
const sidebarItems = getRootNavigation(copy, active, portfolioChild);
|
||||
const sidebarItems = getRootNavigation(copy, active, portfolioChild).filter(
|
||||
(item) => item.href !== "/root/maintenance" && item.href !== "/root/ui-kit",
|
||||
);
|
||||
const headerIcon =
|
||||
active === "overview"
|
||||
? LayoutDashboard
|
||||
: active === "maintenance"
|
||||
? ShieldAlert
|
||||
: active === "ui-kit"
|
||||
? SwatchBook
|
||||
: active === "media"
|
||||
? ImageIcon
|
||||
: portfolioChild === "categories"
|
||||
? Tags
|
||||
: portfolioChild === "new-project"
|
||||
? PlusSquare
|
||||
: FolderKanban;
|
||||
const sharedActions = (
|
||||
<>
|
||||
<ThemeToggle ariaLabel="Theme wechseln" />
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<Link href={getLocalizedPath("de")}>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
{copy.backToSite}
|
||||
</Link>
|
||||
</Button>
|
||||
<form action={logoutAction}>
|
||||
<Button type="submit" variant="ghost" size="sm" className="text-destructive hover:bg-destructive/10 hover:text-destructive">
|
||||
<LogOut className="h-4 w-4" />
|
||||
{copy.logout}
|
||||
</Button>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
sidebar={
|
||||
<AppSidebar
|
||||
title={copy.title}
|
||||
description={copy.subtitle}
|
||||
items={sidebarItems}
|
||||
footer={
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<ThemeToggle ariaLabel="Theme wechseln" />
|
||||
</div>
|
||||
<Button asChild variant="outline" className="w-full justify-start">
|
||||
<Link href={getLocalizedPath("de")}>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
{copy.backToSite}
|
||||
</Link>
|
||||
</Button>
|
||||
<form action={logoutAction}>
|
||||
<Button type="submit" variant="destructive" className="w-full justify-start">
|
||||
<LogOut className="h-4 w-4" />
|
||||
{copy.logout}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<DashboardLayout
|
||||
title={headerTitle}
|
||||
description={headerDescription}
|
||||
icon={headerIcon}
|
||||
items={sidebarItems}
|
||||
sidebarFooter={
|
||||
<>
|
||||
<Button
|
||||
asChild
|
||||
variant={active === "maintenance" ? "default" : "outline"}
|
||||
className="w-full justify-between"
|
||||
>
|
||||
<Link href="/root/maintenance">
|
||||
{copy.maintenance}
|
||||
<ShieldAlert className="h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
asChild
|
||||
variant={active === "ui-kit" ? "default" : "outline"}
|
||||
className="w-full justify-between"
|
||||
>
|
||||
<Link href="/root/ui-kit">
|
||||
{copy.uiKit}
|
||||
<SwatchBook className="h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
header={
|
||||
<AppHeader
|
||||
title={headerTitle}
|
||||
description={headerDescription}
|
||||
actions={headerActions}
|
||||
/>
|
||||
headerActions={
|
||||
<div className="flex flex-wrap items-center justify-end gap-2">
|
||||
{headerActions}
|
||||
{sharedActions}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</AppShell>
|
||||
<div className="space-y-6">
|
||||
{toolbar ? <MotionFade delay={0.05}>{toolbar}</MotionFade> : null}
|
||||
{children}
|
||||
</div>
|
||||
</DashboardLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,13 +4,22 @@ import { Moon, Sun } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Button, type ButtonProps } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type ThemeToggleProps = {
|
||||
ariaLabel?: string;
|
||||
label?: string;
|
||||
variant?: ButtonProps["variant"];
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function ThemeToggle({ ariaLabel = "Toggle theme" }: ThemeToggleProps) {
|
||||
export function ThemeToggle({
|
||||
ariaLabel = "Toggle theme",
|
||||
label,
|
||||
variant = "outline",
|
||||
className,
|
||||
}: ThemeToggleProps) {
|
||||
const { setTheme, theme, resolvedTheme } = useTheme();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
@@ -20,8 +29,15 @@ export function ThemeToggle({ ariaLabel = "Toggle theme" }: ThemeToggleProps) {
|
||||
|
||||
if (!mounted) {
|
||||
return (
|
||||
<Button type="button" variant="outline" size="icon" aria-label={ariaLabel}>
|
||||
<Button
|
||||
type="button"
|
||||
variant={variant}
|
||||
size={label ? "sm" : "icon"}
|
||||
aria-label={ariaLabel}
|
||||
className={cn(label ? "justify-start" : undefined, className)}
|
||||
>
|
||||
<Moon className="h-4 w-4" />
|
||||
{label ? <span>{label}</span> : null}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -30,8 +46,16 @@ export function ThemeToggle({ ariaLabel = "Toggle theme" }: ThemeToggleProps) {
|
||||
const isDark = activeTheme === "dark";
|
||||
|
||||
return (
|
||||
<Button type="button" variant="outline" size="icon" onClick={() => setTheme(isDark ? "light" : "dark")} aria-label={ariaLabel}>
|
||||
<Button
|
||||
type="button"
|
||||
variant={variant}
|
||||
size={label ? "sm" : "icon"}
|
||||
onClick={() => setTheme(isDark ? "light" : "dark")}
|
||||
aria-label={ariaLabel}
|
||||
className={cn(label ? "justify-start" : undefined, className)}
|
||||
>
|
||||
{isDark ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
|
||||
{label ? <span>{label}</span> : null}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import { Check, ChevronRight, Circle } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root;
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto h-4 w-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
));
|
||||
|
||||
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
));
|
||||
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
));
|
||||
|
||||
DropdownMenuCheckboxItem.displayName =
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName;
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Circle className="h-2 w-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
));
|
||||
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
||||
|
||||
const DropdownMenuShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => (
|
||||
<span className={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...props} />
|
||||
);
|
||||
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
};
|
||||
@@ -0,0 +1,112 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as SheetPrimitive from "@radix-ui/react-dialog";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Sheet = SheetPrimitive.Root;
|
||||
const SheetTrigger = SheetPrimitive.Trigger;
|
||||
const SheetClose = SheetPrimitive.Close;
|
||||
const SheetPortal = SheetPrimitive.Portal;
|
||||
|
||||
const SheetOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Overlay
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
));
|
||||
|
||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
|
||||
|
||||
const sheetVariants = {
|
||||
right:
|
||||
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=open]:slide-in-from-right data-[state=closed]:slide-out-to-right sm:max-w-sm",
|
||||
left:
|
||||
"inset-y-0 left-0 h-full w-3/4 border-r data-[state=open]:slide-in-from-left data-[state=closed]:slide-out-to-left sm:max-w-sm",
|
||||
top:
|
||||
"inset-x-0 top-0 border-b data-[state=open]:slide-in-from-top data-[state=closed]:slide-out-to-top",
|
||||
bottom:
|
||||
"inset-x-0 bottom-0 border-t data-[state=open]:slide-in-from-bottom data-[state=closed]:slide-out-to-bottom",
|
||||
};
|
||||
|
||||
type SheetContentProps = React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content> & {
|
||||
side?: keyof typeof sheetVariants;
|
||||
};
|
||||
|
||||
const SheetContent = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Content>,
|
||||
SheetContentProps
|
||||
>(({ side = "right", className, children, ...props }, ref) => (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out",
|
||||
sheetVariants[side],
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
));
|
||||
|
||||
SheetContent.displayName = SheetPrimitive.Content.displayName;
|
||||
|
||||
const SheetHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn("flex flex-col space-y-2 text-left", className)} {...props} />
|
||||
);
|
||||
|
||||
const SheetTitle = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-lg font-semibold text-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
SheetTitle.displayName = SheetPrimitive.Title.displayName;
|
||||
|
||||
const SheetDescription = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
SheetDescription.displayName = SheetPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
};
|
||||
@@ -0,0 +1,112 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div className="relative w-full overflow-auto">
|
||||
<table ref={ref} className={cn("w-full caption-bottom text-sm", className)} {...props} />
|
||||
</div>
|
||||
),
|
||||
);
|
||||
|
||||
Table.displayName = "Table";
|
||||
|
||||
const TableHeader = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
||||
));
|
||||
|
||||
TableHeader.displayName = "TableHeader";
|
||||
|
||||
const TableBody = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tbody
|
||||
ref={ref}
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
TableBody.displayName = "TableBody";
|
||||
|
||||
const TableFooter = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tfoot
|
||||
ref={ref}
|
||||
className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
TableFooter.displayName = "TableFooter";
|
||||
|
||||
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
|
||||
TableRow.displayName = "TableRow";
|
||||
|
||||
const TableHead = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"h-11 px-4 text-left align-middle font-medium text-muted-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
TableHead.displayName = "TableHead";
|
||||
|
||||
const TableCell = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<td ref={ref} className={cn("p-4 align-middle", className)} {...props} />
|
||||
));
|
||||
|
||||
TableCell.displayName = "TableCell";
|
||||
|
||||
const TableCaption = React.forwardRef<
|
||||
HTMLTableCaptionElement,
|
||||
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<caption
|
||||
ref={ref}
|
||||
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
TableCaption.displayName = "TableCaption";
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCaption,
|
||||
TableCell,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
};
|
||||
@@ -253,7 +253,7 @@ export function UiKitShowcase({ localeKey }: UiKitShowcaseProps) {
|
||||
|
||||
<TabsContent value="cards">
|
||||
<UiKitSection title={copy.cards} description={copy.cardsDesc}>
|
||||
<div className="grid gap-6 lg:grid-cols-2">
|
||||
<div className="grid gap-6">
|
||||
<AppCard level={1}>
|
||||
<CardContent className="space-y-4 p-6">
|
||||
<div>
|
||||
@@ -307,7 +307,7 @@ export function UiKitShowcase({ localeKey }: UiKitShowcaseProps) {
|
||||
|
||||
<TabsContent value="buttons">
|
||||
<UiKitSection title={copy.buttons} description={copy.buttonsDesc}>
|
||||
<div className="grid gap-6 lg:grid-cols-2">
|
||||
<div className="grid gap-6">
|
||||
<AppCard level={2}>
|
||||
<CardContent className="flex flex-wrap gap-3 p-5">
|
||||
<Button>{copy.default}</Button>
|
||||
@@ -335,7 +335,7 @@ export function UiKitShowcase({ localeKey }: UiKitShowcaseProps) {
|
||||
|
||||
<TabsContent value="inputs">
|
||||
<UiKitSection title={copy.inputs} description={copy.inputsDesc}>
|
||||
<div className="grid gap-6 lg:grid-cols-2">
|
||||
<div className="grid gap-6">
|
||||
<AppCard level={2}>
|
||||
<CardContent className="grid gap-4 p-5">
|
||||
<Label className="grid gap-2">
|
||||
@@ -431,7 +431,7 @@ export function UiKitShowcase({ localeKey }: UiKitShowcaseProps) {
|
||||
|
||||
<TabsContent value="surfaces">
|
||||
<UiKitSection title={copy.surfaces} description={copy.surfacesDesc}>
|
||||
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
<div className="grid gap-4">
|
||||
<AppCard level={1}>
|
||||
<CardContent className="p-5">
|
||||
<p className="font-medium">{copy.surfaceOne}</p>
|
||||
@@ -489,7 +489,7 @@ export function UiKitShowcase({ localeKey }: UiKitShowcaseProps) {
|
||||
|
||||
<TabsContent value="headers">
|
||||
<UiKitSection title={copy.headers} description={copy.headersDesc}>
|
||||
<div className="grid gap-6 lg:grid-cols-2">
|
||||
<div className="grid gap-6">
|
||||
<AppCard level={3}>
|
||||
<CardContent className="space-y-3 p-6">
|
||||
<div className="inline-flex w-fit items-center gap-2 rounded-pill border border-border bg-surface-1 px-3 py-1 text-xs font-semibold uppercase tracking-[0.14em] text-foreground/80">
|
||||
|
||||
Reference in New Issue
Block a user