Refine root UI around shadcn primitives
CI / quality (push) Waiting to run

This commit is contained in:
MOH
2026-03-07 22:28:52 +01:00
parent 985fbe1745
commit a18370d003
36 changed files with 1344 additions and 597 deletions
+1 -1
View File
@@ -41,7 +41,7 @@ export function AppSidebar({
<Link
href={item.href}
className={cn(
"flex items-center gap-2 rounded-nested px-3 py-2 text-sm transition-colors",
"flex items-center gap-2 rounded-md 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",
+1 -1
View File
@@ -20,7 +20,7 @@ export function FloatingPreferences({
return (
<div className={cn("fixed inset-x-0 top-0 z-50 px-4 pt-4 sm:px-6", className)}>
<div className="mx-auto flex w-full max-w-max items-center justify-center gap-2">
<ThemeToggle ariaLabel={t("themeToggle")} className="bg-surface-1/90" />
<ThemeToggle ariaLabel={t("themeToggle")} className="bg-background/90" />
<LocaleToggle locale={locale} />
</div>
</div>
+1 -1
View File
@@ -22,7 +22,7 @@ export function SiteFooter({ isAdmin = false }: SiteFooterProps) {
const tFooter = useTranslations("footer");
return (
<footer className="border-t border-border bg-surface-2">
<footer className="border-t border-border bg-background">
<Container className="flex flex-col gap-4 py-8">
<nav className="flex flex-wrap gap-x-5 gap-y-3 text-sm">
{navItems.map((item) => (
+2 -2
View File
@@ -71,7 +71,7 @@ export function SiteHeader({ isAdmin = false }: SiteHeaderProps) {
<div className="flex items-center gap-4">
<Link
href={getLocalizedPath(locale)}
className="inline-flex items-center rounded-nested border border-border bg-surface-1 px-3 py-2 shadow-xs"
className="inline-flex items-center rounded-md border border-input bg-background px-3 py-2 shadow-sm"
>
<Image
src="/logos/light-primary.svg"
@@ -115,7 +115,7 @@ export function SiteHeader({ isAdmin = false }: SiteHeaderProps) {
</Container>
{isOpen ? (
<div className="border-t border-border/80 bg-surface-1 md:hidden">
<div className="border-t border-border/80 bg-background md:hidden">
<Container className="py-4">
<nav className="flex flex-col gap-3">
<NavLinks isAdmin={isAdmin} onNavigate={() => setIsOpen(false)} />
+1 -1
View File
@@ -55,7 +55,7 @@ export function FlashMessage({
return (
<p
className={cn(
"rounded-nested border px-4 py-3 text-sm",
"rounded-md border px-4 py-3 text-sm",
type === "success"
? "border-status-success/30 bg-status-success/10 text-status-success"
: "border-destructive/30 bg-destructive/10 text-destructive",
+40 -29
View File
@@ -3,7 +3,7 @@
/* eslint-disable @next/next/no-img-element */
import type { MediaKind } from "@prisma/client";
import { Check, Search } from "lucide-react";
import { Check, Link2, Search, Type, Upload } from "lucide-react";
import { useEffect, useId, useMemo, useRef, useState } from "react";
import type { MediaOption } from "@/lib/media";
@@ -110,9 +110,9 @@ export function MediaFieldPicker({
}, [serializedValue]);
return (
<div className="space-y-3 rounded-surface border border-border p-4">
<div className="space-y-3 rounded-lg border bg-card p-4 shadow-sm">
<div className="space-y-2">
<Label>{title}</Label>
<Label className="sr-only">{title}</Label>
<div className="flex flex-wrap gap-2">
{modeOptions.map((mode) => (
<button
@@ -128,10 +128,10 @@ export function MediaFieldPicker({
})
}
className={cn(
"rounded-pill border px-4 py-2 text-sm transition-colors",
"rounded-md border px-4 py-2 text-sm transition-colors",
value.mode === mode
? "border-border-strong bg-foreground text-background"
: "border-border bg-background text-foreground/75 hover:border-border-strong hover:text-foreground",
? "border-input bg-primary text-primary-foreground"
: "border-input bg-background text-foreground/75 hover:bg-accent hover:text-accent-foreground",
)}
>
{mode}
@@ -150,7 +150,7 @@ export function MediaFieldPicker({
isCleared: true,
})
}
className="rounded-pill border border-destructive/25 px-4 py-2 text-sm text-destructive transition-colors hover:border-destructive/50 hover:bg-destructive/5"
className="rounded-md border border-destructive/25 px-4 py-2 text-sm text-destructive transition-colors hover:bg-destructive/5"
>
{clearLabel}
</button>
@@ -167,37 +167,48 @@ export function MediaFieldPicker({
{value.mode !== "library" ? (
<div className="space-y-2">
<Label>Label</Label>
<Input
value={value.label}
onChange={(event) => onChange({ ...value, label: event.target.value, isCleared: false })}
placeholder="Homepage Hero"
/>
<Label className="sr-only">Label</Label>
<div className="relative">
<Type className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
value={value.label}
onChange={(event) => onChange({ ...value, label: event.target.value, isCleared: false })}
placeholder="Label"
className="pl-9"
/>
</div>
</div>
) : null}
{value.mode === "upload" ? (
<div className="space-y-2">
<Label>{fileLabel ?? fileFieldName}</Label>
<Input name={fileFieldName} type="file" accept={accept} />
<Label className="sr-only">{fileLabel ?? fileFieldName}</Label>
<div className="relative">
<Upload className="pointer-events-none absolute left-3 top-1/2 z-10 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input name={fileFieldName} type="file" accept={accept} className="pl-9 file:mr-3" />
</div>
</div>
) : null}
{value.mode === "external" ? (
<div className="space-y-2">
<Label>{externalLabel ?? "External URL"}</Label>
<Input
value={value.url}
onChange={(event) => onChange({ ...value, url: event.target.value, isCleared: false })}
placeholder="https://example.com/image.jpg"
/>
<Label className="sr-only">{externalLabel ?? "External URL"}</Label>
<div className="relative">
<Link2 className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
value={value.url}
onChange={(event) => onChange({ ...value, url: event.target.value, isCleared: false })}
placeholder={externalLabel ?? "External URL"}
className="pl-9"
/>
</div>
</div>
) : null}
{value.mode === "library" ? (
<div className="space-y-3">
<Label>{libraryLabel ?? "Media Library"}</Label>
<div className="space-y-2 rounded-nested border border-border bg-surface-1 p-3">
<Label className="sr-only">{libraryLabel ?? "Media Library"}</Label>
<div className="space-y-2 rounded-md border bg-card p-3">
<div className="relative">
<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
@@ -227,10 +238,10 @@ export function MediaFieldPicker({
})
}
className={cn(
"flex w-full items-center gap-3 rounded-nested border px-3 py-2 text-left transition-colors",
"flex w-full items-center gap-3 rounded-md border px-3 py-2 text-left transition-colors",
isActive
? "border-border-strong bg-background"
: "border-border bg-background/70 hover:border-border-strong hover:bg-background",
? "border-input bg-accent/40"
: "border-input bg-background hover:bg-accent/20",
)}
>
{option.url ? (
@@ -247,7 +258,7 @@ export function MediaFieldPicker({
);
})
) : (
<div className="rounded-nested border border-dashed border-border px-3 py-4 text-sm text-muted-foreground">
<div className="rounded-md border border-dashed border-input px-3 py-4 text-sm text-muted-foreground">
No media found.
</div>
)}
@@ -258,11 +269,11 @@ export function MediaFieldPicker({
{previewUrl ? (
value.kind === "IMAGE" ? (
<div className="overflow-hidden rounded-nested border border-border bg-surface-1">
<div className="overflow-hidden rounded-md border bg-card">
<img src={previewUrl} alt={value.label || title} className="h-40 w-full object-cover" />
</div>
) : (
<div className="rounded-nested border border-border bg-surface-1 px-4 py-3 text-sm text-muted-foreground">
<div className="rounded-md border bg-card px-4 py-3 text-sm text-muted-foreground">
{previewUrl}
</div>
)
+166 -87
View File
@@ -1,15 +1,34 @@
"use client";
import type { MediaKind, PortfolioAssetKind, PortfolioSectionType } from "@prisma/client";
import { ArrowDown, ArrowUp, Plus, Trash2 } from "lucide-react";
import {
ArrowDown,
ArrowUp,
BriefcaseBusiness,
CalendarRange,
FolderTree,
Hash,
Link2,
Plus,
Sparkles,
Trash2,
} from "lucide-react";
import { useState } from "react";
import { MediaFieldPicker, type MediaFieldState } from "@/components/root/media-field-picker";
import { AppCard } from "@/components/ui/app-card";
import { Button } from "@/components/ui/button";
import { CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Textarea } from "@/components/ui/textarea";
import { moveArrayItem } from "@/lib/array";
@@ -176,6 +195,8 @@ export function PortfolioProjectForm({
}))
: [],
);
const [isFeatured, setIsFeatured] = useState(project?.isFeatured ?? false);
const [isPublished, setIsPublished] = useState(project?.isPublished ?? false);
const sectionsPayload = JSON.stringify(
sections.map((section, index) => ({
@@ -205,71 +226,106 @@ export function PortfolioProjectForm({
</CardHeader>
<CardContent className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="categoryId">Kategorie</Label>
<select
id="categoryId"
name="categoryId"
defaultValue={project?.category.id ?? categories[0]?.id ?? ""}
className="flex h-11 w-full rounded-pill border border-border bg-background px-4 text-sm text-foreground shadow-sm"
required
>
{categories.map((category) => (
<option key={category.id} value={category.id}>
{category.name.de} / {category.name.en}
</option>
))}
</select>
<Label htmlFor="categoryId" className="sr-only">Kategorie</Label>
<div className="relative">
<FolderTree className="pointer-events-none absolute left-3 top-1/2 z-10 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Select
name="categoryId"
defaultValue={project?.category.id ?? categories[0]?.id ?? ""}
required
>
<SelectTrigger id="categoryId" className="pl-9">
<SelectValue placeholder="Kategorie" />
</SelectTrigger>
<SelectContent>
{categories.map((category) => (
<SelectItem key={category.id} value={category.id}>
{category.name.de} / {category.name.en}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="slug">Slug</Label>
<Input id="slug" name="slug" defaultValue={project?.slug ?? ""} required />
<Label htmlFor="slug" className="sr-only">Slug</Label>
<div className="relative">
<Hash className="pointer-events-none absolute left-3 top-1/2 z-10 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
id="slug"
name="slug"
defaultValue={project?.slug ?? ""}
placeholder="Slug"
className="pl-9"
required
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="clientName">Kundenname</Label>
<Input
id="clientName"
name="clientName"
defaultValue={project?.clientName ?? ""}
required
/>
<Label htmlFor="clientName" className="sr-only">Kundenname</Label>
<div className="relative">
<BriefcaseBusiness className="pointer-events-none absolute left-3 top-1/2 z-10 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
id="clientName"
name="clientName"
defaultValue={project?.clientName ?? ""}
placeholder="Kundenname"
className="pl-9"
required
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="projectYear">Projektjahr</Label>
<Input
id="projectYear"
name="projectYear"
type="number"
min="2000"
max="2100"
defaultValue={project?.projectYear ?? new Date().getFullYear()}
required
/>
<Label htmlFor="projectYear" className="sr-only">Projektjahr</Label>
<div className="relative">
<CalendarRange className="pointer-events-none absolute left-3 top-1/2 z-10 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
id="projectYear"
name="projectYear"
type="number"
min="2000"
max="2100"
defaultValue={project?.projectYear ?? new Date().getFullYear()}
placeholder="Projektjahr"
className="pl-9"
required
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="previewUrl">Preview URL</Label>
<Input
id="previewUrl"
name="previewUrl"
type="url"
defaultValue={project?.previewUrl ?? ""}
placeholder="https://example.com"
/>
<Label htmlFor="previewUrl" className="sr-only">Preview URL</Label>
<div className="relative">
<Link2 className="pointer-events-none absolute left-3 top-1/2 z-10 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
id="previewUrl"
name="previewUrl"
type="url"
defaultValue={project?.previewUrl ?? ""}
placeholder="https://example.com"
className="pl-9"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="sortOrder">Sortierung</Label>
<Input
id="sortOrder"
name="sortOrder"
type="number"
min="0"
defaultValue={project?.sortOrder ?? 0}
required
/>
<Label htmlFor="sortOrder" className="sr-only">Sortierung</Label>
<div className="relative">
<Hash className="pointer-events-none absolute left-3 top-1/2 z-10 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
id="sortOrder"
name="sortOrder"
type="number"
min="0"
defaultValue={project?.sortOrder ?? 0}
placeholder="Sortierung"
className="pl-9"
required
/>
</div>
</div>
<div className="space-y-2 md:col-span-2">
@@ -284,13 +340,23 @@ export function PortfolioProjectForm({
/>
</div>
<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} />
<label className="flex items-center gap-3 rounded-md border border-border px-4 py-3 text-sm">
<Checkbox
name="isFeatured"
checked={isFeatured}
onCheckedChange={(checked) => setIsFeatured(checked === true)}
/>
<Sparkles className="h-4 w-4 text-muted-foreground" />
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} />
<label className="flex items-center gap-3 rounded-md border border-border px-4 py-3 text-sm">
<Checkbox
name="isPublished"
checked={isPublished}
onCheckedChange={(checked) => setIsPublished(checked === true)}
/>
<Sparkles className="h-4 w-4 text-muted-foreground" />
Veroeffentlicht
</label>
</CardContent>
@@ -411,25 +477,29 @@ export function PortfolioProjectForm({
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label>Typ</Label>
<select
<Select
value={section.type}
onChange={(event) =>
onValueChange={(value) =>
setSections((current) =>
current.map((item, currentIndex) =>
currentIndex === index
? { ...item, type: event.target.value as PortfolioSectionType }
? { ...item, type: value as PortfolioSectionType }
: item,
),
)
}
className="flex h-11 w-full rounded-pill border border-border bg-background px-4 text-sm text-foreground shadow-sm"
>
{sectionTypeOptions.map((type) => (
<option key={type} value={type}>
{type}
</option>
))}
</select>
<SelectTrigger>
<SelectValue placeholder="Typ" />
</SelectTrigger>
<SelectContent>
{sectionTypeOptions.map((type) => (
<SelectItem key={type} value={type}>
{type}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2 md:col-span-2">
@@ -457,17 +527,22 @@ export function PortfolioProjectForm({
</div>
<div className="space-y-2 md:col-span-2">
<Label>Link URL</Label>
<Input
value={section.linkUrl}
onChange={(event) =>
setSections((current) =>
current.map((item, currentIndex) =>
currentIndex === index ? { ...item, linkUrl: event.target.value } : item,
),
)
}
/>
<Label className="sr-only">Link URL</Label>
<div className="relative">
<Link2 className="pointer-events-none absolute left-3 top-1/2 z-10 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
value={section.linkUrl}
onChange={(event) =>
setSections((current) =>
current.map((item, currentIndex) =>
currentIndex === index ? { ...item, linkUrl: event.target.value } : item,
),
)
}
placeholder="Link URL"
className="pl-9"
/>
</div>
</div>
{localeFieldConfig.map((locale) => (
@@ -575,32 +650,36 @@ export function PortfolioProjectForm({
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label>Typ</Label>
<select
<Select
value={asset.kind}
onChange={(event) =>
onValueChange={(value) =>
setAssets((current) =>
current.map((item, currentIndex) =>
currentIndex === index
? {
...item,
kind: event.target.value as PortfolioAssetKind,
kind: value as PortfolioAssetKind,
media: {
...item.media,
kind: event.target.value as PortfolioAssetKind,
kind: value as PortfolioAssetKind,
},
}
: item,
),
)
}
className="flex h-11 w-full rounded-pill border border-border bg-background px-4 text-sm text-foreground shadow-sm"
>
{assetKindOptions.map((kind) => (
<option key={kind} value={kind}>
{kind}
</option>
))}
</select>
<SelectTrigger>
<SelectValue placeholder="Typ" />
</SelectTrigger>
<SelectContent>
{assetKindOptions.map((kind) => (
<SelectItem key={kind} value={kind}>
{kind}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2 md:col-span-2">
@@ -0,0 +1,316 @@
import { Boxes, ExternalLink, Filter, FolderKanban, Layers3, Plus, Tags } from "lucide-react";
import Link from "next/link";
import { MotionFade } from "@/components/motion-fade";
import { PortfolioSubnav } from "@/components/root/portfolio-subnav";
import { AppCard } from "@/components/ui/app-card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { getLocalizedPath } from "@/lib/locale";
import { getLocalizedValue, type PortfolioCategoryView, type PortfolioProjectView } from "@/lib/portfolio";
type PortfolioProjectsOverviewProps = {
categories: PortfolioCategoryView[];
projects: PortfolioProjectView[];
selectedCategory: string;
selectedStatus: "all" | "draft" | "published";
};
const copy = {
summary: "Portfolio cockpit",
description: "Alle Projekte, Filter und Einstiegspunkte fuer die Bearbeitung an einem Ort.",
totalCategories: "Kategorien",
totalProjects: "Projekte",
publishedProjects: "Veroeffentlicht",
newProject: "Neues Projekt",
newCategory: "Neue Kategorie",
category: "Kategorie",
all: "Alle",
status: "Status",
draft: "Entwurf",
published: "Veroeffentlicht",
filter: "Filter anwenden",
empty: "Noch keine Projekte vorhanden. Lege das erste Projekt an und beginne direkt mit Inhalt, Abschnitten und Dateien.",
openProject: "Projekt ansehen",
editProject: "Projekt bearbeiten",
review: "Bearbeitungsstand",
reviewDone: "Bereit",
reviewMissing: "Fehlt etwas",
untitled: "Unbenanntes Projekt",
noCategory: "Ohne Kategorie",
noPreview: "Kein Preview Link",
hasPreview: "Preview Link vorhanden",
hasCover: "Cover gesetzt",
missingCover: "Cover fehlt",
hasSections: "Abschnitte vorhanden",
noSections: "Keine Abschnitte",
hasAssets: "Dateien vorhanden",
noAssets: "Keine Dateien",
};
function getProjectCompletion(project: PortfolioProjectView) {
const completedChecks = [
Boolean(project.slug.trim()),
Boolean(project.coverImagePath),
project.sections.length > 0,
project.assets.length > 0,
Boolean(project.title.ar.trim() && project.title.en.trim() && project.title.de.trim()),
Boolean(project.summary.ar.trim() && project.summary.en.trim() && project.summary.de.trim()),
].filter(Boolean).length;
return {
completedChecks,
totalChecks: 6,
ready: completedChecks === 6,
};
}
export function PortfolioProjectsOverview({
categories,
projects,
selectedCategory,
selectedStatus,
}: PortfolioProjectsOverviewProps) {
const publishedProjects = projects.filter((project) => project.isPublished).length;
return (
<div className="space-y-6">
<PortfolioSubnav active="projects" />
<MotionFade delay={0.05}>
<AppCard level={3}>
<CardContent className="flex flex-col gap-6 p-6 lg:flex-row lg:items-end lg:justify-between lg:p-8">
<div className="space-y-3">
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground">
{copy.summary}
</p>
<div className="space-y-2">
<h2 className="text-2xl font-semibold text-foreground">Projektverwaltung</h2>
<p className="max-w-2xl text-sm text-muted-foreground">{copy.description}</p>
</div>
</div>
<div className="flex flex-wrap gap-3">
<Button asChild>
<Link href="/root/portfolio/projects/new">
<Plus className="h-4 w-4" />
{copy.newProject}
</Link>
</Button>
<Button asChild variant="outline">
<Link href="/root/portfolio/categories">
<Tags className="h-4 w-4" />
{copy.newCategory}
</Link>
</Button>
</div>
</CardContent>
</AppCard>
</MotionFade>
<section className="grid gap-4 md:grid-cols-3">
{[
{
icon: Layers3,
label: copy.totalCategories,
value: categories.length,
},
{
icon: FolderKanban,
label: copy.totalProjects,
value: projects.length,
},
{
icon: Boxes,
label: copy.publishedProjects,
value: publishedProjects,
},
].map((item, index) => {
const Icon = item.icon;
return (
<MotionFade key={item.label} delay={0.08 + index * 0.04}>
<AppCard level={2}>
<CardContent className="flex items-center gap-4 p-6">
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-muted">
<Icon className="h-5 w-5 text-brand-primary" />
</div>
<div>
<p className="text-sm text-muted-foreground">{item.label}</p>
<p className="text-3xl font-semibold text-foreground">{item.value}</p>
</div>
</CardContent>
</AppCard>
</MotionFade>
);
})}
</section>
<MotionFade delay={0.14}>
<AppCard>
<CardContent className="p-6">
<form className="grid gap-4 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto]">
<div className="space-y-2">
<Label htmlFor="portfolio-filter-category" className="sr-only">
{copy.category}
</Label>
<div className="relative">
<Tags className="pointer-events-none absolute left-3 top-1/2 z-10 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Select name="category" defaultValue={selectedCategory || "__all__"}>
<SelectTrigger id="portfolio-filter-category" className="pl-9">
<SelectValue placeholder={copy.category} />
</SelectTrigger>
<SelectContent>
<SelectItem value="__all__">{copy.all}</SelectItem>
{categories.map((category) => (
<SelectItem key={category.id} value={category.id}>
{category.name.de}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="portfolio-filter-status" className="sr-only">
{copy.status}
</Label>
<div className="relative">
<Filter className="pointer-events-none absolute left-3 top-1/2 z-10 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Select name="status" defaultValue={selectedStatus}>
<SelectTrigger id="portfolio-filter-status" className="pl-9">
<SelectValue placeholder={copy.status} />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">{copy.all}</SelectItem>
<SelectItem value="draft">{copy.draft}</SelectItem>
<SelectItem value="published">{copy.published}</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="flex items-end">
<Button type="submit" variant="outline">
{copy.filter}
</Button>
</div>
</form>
</CardContent>
</AppCard>
</MotionFade>
<div className="grid gap-4">
{projects.map((project, index) => {
const completion = getProjectCompletion(project);
return (
<MotionFade key={project.id} delay={0.18 + index * 0.03}>
<AppCard interactive>
<CardHeader className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div className="space-y-2">
<div className="flex flex-wrap items-center gap-2">
<CardTitle className="text-xl">
{getLocalizedValue(project.title, "de") || copy.untitled}
</CardTitle>
<Badge variant={project.isPublished ? "success" : "warning"}>
{project.isPublished ? copy.published : copy.draft}
</Badge>
</div>
<div className="flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
<span>{project.category.name.de || copy.noCategory}</span>
<span></span>
<span>{project.projectYear}</span>
<span></span>
<span>{project.slug}</span>
</div>
</div>
<div className="flex flex-wrap gap-2">
<Badge variant={completion.ready ? "success" : "warning"}>
{copy.review}: {completion.completedChecks}/{completion.totalChecks}
</Badge>
<Badge variant="outline">
{completion.ready ? copy.reviewDone : copy.reviewMissing}
</Badge>
</div>
</CardHeader>
<CardContent className="space-y-5">
<p className="max-w-3xl text-sm text-muted-foreground">
{getLocalizedValue(project.summary, "de") || "Noch keine Kurzbeschreibung hinterlegt."}
</p>
<div className="flex flex-wrap gap-2">
<Badge variant={project.previewUrl ? "success" : "outline"}>
{project.previewUrl ? copy.hasPreview : copy.noPreview}
</Badge>
<Badge variant={project.coverImagePath ? "success" : "outline"}>
{project.coverImagePath ? copy.hasCover : copy.missingCover}
</Badge>
<Badge variant={project.sections.length > 0 ? "success" : "outline"}>
{project.sections.length > 0 ? copy.hasSections : copy.noSections}
</Badge>
<Badge variant={project.assets.length > 0 ? "success" : "outline"}>
{project.assets.length > 0 ? copy.hasAssets : copy.noAssets}
</Badge>
</div>
<div className="flex flex-wrap gap-3">
<Button asChild variant="outline">
<Link
href={getLocalizedPath("de", `/portfolio/${project.slug}`)}
target="_blank"
rel="noreferrer"
>
<ExternalLink className="h-4 w-4" />
{copy.openProject}
</Link>
</Button>
<Button asChild>
<Link href={`/root/portfolio/projects/${project.id}`}>{copy.editProject}</Link>
</Button>
</div>
</CardContent>
</AppCard>
</MotionFade>
);
})}
{projects.length === 0 ? (
<MotionFade delay={0.18}>
<AppCard>
<CardContent className="space-y-4 p-6">
<p className="text-sm text-muted-foreground">{copy.empty}</p>
<div className="flex flex-wrap gap-3">
<Button asChild>
<Link href="/root/portfolio/projects/new">
<Plus className="h-4 w-4" />
{copy.newProject}
</Link>
</Button>
<Button asChild variant="outline">
<Link href="/root/portfolio/categories">
<Tags className="h-4 w-4" />
{copy.newCategory}
</Link>
</Button>
</div>
</CardContent>
</AppCard>
</MotionFade>
) : null}
</div>
</div>
);
}
+6 -6
View File
@@ -5,13 +5,13 @@ import { CardContent } from "@/components/ui/card";
import { cn } from "@/lib/utils";
type PortfolioSubnavProps = {
active: "overview" | "categories";
active: "projects" | "categories";
};
const items = [
{
key: "overview",
label: "Uebersicht",
key: "projects",
label: "Projekte",
href: "/root/portfolio",
},
{
@@ -30,10 +30,10 @@ export function PortfolioSubnav({ active }: PortfolioSubnavProps) {
key={item.key}
href={item.href}
className={cn(
"rounded-pill border px-4 py-2 text-sm transition-colors",
"rounded-md 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",
? "border-input bg-primary text-primary-foreground"
: "border-input bg-background text-foreground/75 hover:bg-accent hover:text-accent-foreground",
)}
>
{item.label}
@@ -5,6 +5,7 @@ import { usePathname } from "next/navigation";
import { useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Checkbox } from "@/components/ui/checkbox";
import { cn } from "@/lib/utils";
type SidebarMaintenanceControlProps = {
@@ -33,10 +34,10 @@ export function SidebarMaintenanceControl({
<label
htmlFor="sidebar-maintenance-enabled"
className={cn(
"flex cursor-pointer items-center justify-between gap-3 rounded-nested border px-3 py-2 transition-colors",
"flex cursor-pointer items-center justify-between gap-3 rounded-md border px-3 py-2 transition-colors",
enabled
? "border-status-warning/40 bg-status-warning-soft/80"
: "border-border bg-surface-1 hover:bg-surface-2",
: "border-input bg-card hover:bg-accent/20",
)}
>
<span className="flex min-w-0 items-center gap-3">
@@ -63,11 +64,10 @@ export function SidebarMaintenanceControl({
</Badge>
</label>
<input
<Checkbox
id="sidebar-maintenance-enabled"
type="checkbox"
checked={enabled}
onChange={(event) => setEnabled(event.target.checked)}
onCheckedChange={(checked) => setEnabled(checked === true)}
className="sr-only"
/>
</form>
+95 -76
View File
@@ -3,7 +3,7 @@
/* eslint-disable @next/next/no-img-element */
import { MediaKind } from "@prisma/client";
import { ImageIcon, Search, Type } from "lucide-react";
import { FileText, ImageIcon, Search, Type } from "lucide-react";
import { useState } from "react";
import { MediaFieldPicker, type MediaFieldState } from "@/components/root/media-field-picker";
@@ -179,95 +179,114 @@ export function SiteSettingsForm({
</CardHeader>
<CardContent className="grid gap-4 lg:grid-cols-3">
{localeFields.map((locale) => (
<div key={locale.key} className="space-y-4 rounded-surface border border-border bg-surface-1 p-4">
<div key={locale.key} className="space-y-4 rounded-lg border bg-card p-4 shadow-sm">
<div>
<p className="text-sm font-medium text-foreground">{locale.label}</p>
<p className="mt-1 text-xs text-muted-foreground">Globale Standardwerte fuer diese Sprache.</p>
</div>
<div className="space-y-2">
<Label htmlFor={`siteName${locale.suffix}`}>Site Name</Label>
<Input
id={`siteName${locale.suffix}`}
name={`siteName${locale.suffix}`}
value={settings.locales[locale.key].siteName}
onChange={(event) =>
setSettings((current) => ({
...current,
locales: {
...current.locales,
[locale.key]: {
...current.locales[locale.key],
siteName: event.target.value,
<Label htmlFor={`siteName${locale.suffix}`} className="sr-only">Site Name</Label>
<div className="relative">
<Type className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
id={`siteName${locale.suffix}`}
name={`siteName${locale.suffix}`}
value={settings.locales[locale.key].siteName}
onChange={(event) =>
setSettings((current) => ({
...current,
locales: {
...current.locales,
[locale.key]: {
...current.locales[locale.key],
siteName: event.target.value,
},
},
},
}))
}
/>
}))
}
placeholder="Site Name"
className="pl-9"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor={`titleTemplate${locale.suffix}`}>Title Template</Label>
<Input
id={`titleTemplate${locale.suffix}`}
name={`titleTemplate${locale.suffix}`}
value={settings.locales[locale.key].titleTemplate}
onChange={(event) =>
setSettings((current) => ({
...current,
locales: {
...current.locales,
[locale.key]: {
...current.locales[locale.key],
titleTemplate: event.target.value,
<Label htmlFor={`titleTemplate${locale.suffix}`} className="sr-only">Title Template</Label>
<div className="relative">
<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
id={`titleTemplate${locale.suffix}`}
name={`titleTemplate${locale.suffix}`}
value={settings.locales[locale.key].titleTemplate}
onChange={(event) =>
setSettings((current) => ({
...current,
locales: {
...current.locales,
[locale.key]: {
...current.locales[locale.key],
titleTemplate: event.target.value,
},
},
},
}))
}
placeholder="{pageTitle} | {siteName}"
/>
}))
}
placeholder="Title Template"
className="pl-9"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor={`siteDescription${locale.suffix}`}>Site Description</Label>
<Input
id={`siteDescription${locale.suffix}`}
name={`siteDescription${locale.suffix}`}
value={settings.locales[locale.key].siteDescription}
onChange={(event) =>
setSettings((current) => ({
...current,
locales: {
...current.locales,
[locale.key]: {
...current.locales[locale.key],
siteDescription: event.target.value,
<Label htmlFor={`siteDescription${locale.suffix}`} className="sr-only">Site Description</Label>
<div className="relative">
<FileText className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
id={`siteDescription${locale.suffix}`}
name={`siteDescription${locale.suffix}`}
value={settings.locales[locale.key].siteDescription}
onChange={(event) =>
setSettings((current) => ({
...current,
locales: {
...current.locales,
[locale.key]: {
...current.locales[locale.key],
siteDescription: event.target.value,
},
},
},
}))
}
/>
}))
}
placeholder="Site Description"
className="pl-9"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor={`subhead${locale.suffix}`}>Subhead</Label>
<Input
id={`subhead${locale.suffix}`}
name={`subhead${locale.suffix}`}
value={settings.locales[locale.key].subhead}
onChange={(event) =>
setSettings((current) => ({
...current,
locales: {
...current.locales,
[locale.key]: {
...current.locales[locale.key],
subhead: event.target.value,
<Label htmlFor={`subhead${locale.suffix}`} className="sr-only">Subhead</Label>
<div className="relative">
<Type className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
id={`subhead${locale.suffix}`}
name={`subhead${locale.suffix}`}
value={settings.locales[locale.key].subhead}
onChange={(event) =>
setSettings((current) => ({
...current,
locales: {
...current.locales,
[locale.key]: {
...current.locales[locale.key],
subhead: event.target.value,
},
},
},
}))
}
/>
}))
}
placeholder="Subhead"
className="pl-9"
/>
</div>
</div>
</div>
))}
@@ -344,7 +363,7 @@ export function SiteSettingsForm({
: `${locale.sampleTitle} | ${localeSettings.siteName}`;
return (
<div key={locale.key} className="rounded-nested border border-border bg-background p-3">
<div key={locale.key} className="rounded-md border bg-card p-3">
<p className="font-medium text-foreground">{locale.label}</p>
<p className="mt-2 text-xs text-muted-foreground">
Example page title: {locale.sampleTitle}
@@ -365,12 +384,12 @@ export function SiteSettingsForm({
<Search className="h-4 w-4 text-brand-primary" />
Search Preview
</div>
<div className="rounded-nested border border-border bg-background p-4">
<div className="rounded-md border bg-card p-4">
<div className="flex items-center gap-3">
{faviconPreviewUrl ? (
<img src={faviconPreviewUrl} alt="Favicon" className="h-5 w-5 rounded-sm" />
) : (
<div className="flex h-5 w-5 items-center justify-center rounded-sm border border-border bg-surface-1">
<div className="flex h-5 w-5 items-center justify-center rounded-sm border bg-muted/40">
<Type className="h-3.5 w-3.5 text-muted-foreground" />
</div>
)}
@@ -394,11 +413,11 @@ export function SiteSettingsForm({
<ImageIcon className="h-4 w-4 text-brand-secondary" />
Social Preview
</div>
<div className="overflow-hidden rounded-nested border border-border bg-background">
<div className="overflow-hidden rounded-md border bg-card">
{defaultOgImagePreviewUrl ? (
<img src={defaultOgImagePreviewUrl} alt="Default OG" className="h-32 w-full object-cover" />
) : (
<div className="flex h-32 items-center justify-center bg-surface-1 text-sm text-muted-foreground">
<div className="flex h-32 items-center justify-center bg-muted/40 text-sm text-muted-foreground">
No OG image selected
</div>
)}
+6 -6
View File
@@ -5,14 +5,14 @@ import { Card } from "@/components/ui/card";
import { cn } from "@/lib/utils";
const appCardVariants = cva(
"rounded-surface border transition-all duration-200",
"rounded-lg transition-colors",
{
variants: {
level: {
1: "border-border bg-surface-1 text-foreground shadow-card",
2: "border-border bg-surface-2 text-foreground shadow-sm",
3: "border-border/90 bg-surface-3 text-foreground shadow-panel",
inverse: "border-transparent bg-surface-inverse text-surface-inverse-foreground shadow-lg",
1: "border bg-card text-card-foreground shadow-sm",
2: "border bg-card text-card-foreground shadow-sm",
3: "border bg-card text-card-foreground shadow-md",
inverse: "border-transparent bg-foreground text-background shadow-sm",
},
padding: {
none: "",
@@ -21,7 +21,7 @@ const appCardVariants = cva(
lg: "p-8",
},
interactive: {
true: "hover:-translate-y-0.5 hover:border-border-strong hover:shadow-md",
true: "hover:bg-accent/30 hover:text-accent-foreground",
false: "",
},
},
+1 -1
View File
@@ -10,7 +10,7 @@ const badgeVariants = cva(
variant: {
default: "border-transparent bg-primary text-primary-foreground",
secondary: "border-transparent bg-secondary text-secondary-foreground",
outline: "border-border bg-surface-1 text-foreground",
outline: "border-input bg-background text-foreground",
success: "border-transparent bg-status-success-soft text-status-success",
warning: "border-transparent bg-status-warning-soft text-status-warning",
},
+5 -5
View File
@@ -5,16 +5,16 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-nested text-sm font-medium ring-offset-background transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground shadow-xs hover:brightness-95",
secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
outline: "border border-border bg-surface-1 text-foreground shadow-xs hover:border-border-strong hover:bg-surface-2",
default: "bg-primary text-primary-foreground hover:bg-primary/90",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
ghost: "text-muted-foreground hover:bg-muted hover:text-foreground",
link: "rounded-none text-primary underline-offset-4 hover:underline",
destructive: "bg-destructive text-destructive-foreground shadow-xs hover:brightness-95",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
},
size: {
default: "h-10 px-4 py-2",
+1 -1
View File
@@ -7,7 +7,7 @@ const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElemen
<div
ref={ref}
className={cn(
"rounded-surface border border-border bg-surface-1 text-card-foreground shadow-card",
"rounded-lg border bg-card text-card-foreground shadow-sm",
className,
)}
{...props}
+50
View File
@@ -0,0 +1,50 @@
"use client";
import * as React from "react";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { Check } from "lucide-react";
import { cn } from "@/lib/utils";
type CheckboxProps = React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> & {
name?: string;
};
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
CheckboxProps
>(({ className, name, checked, defaultChecked, onCheckedChange, ...props }, ref) => {
const [internalChecked, setInternalChecked] = React.useState(defaultChecked === true);
const isControlled = checked !== undefined;
const currentChecked = isControlled ? checked === true : internalChecked;
return (
<>
{name ? <input type="hidden" name={name} value={currentChecked ? "on" : "false"} /> : null}
<CheckboxPrimitive.Root
ref={ref}
checked={checked}
defaultChecked={defaultChecked}
onCheckedChange={(nextChecked) => {
if (!isControlled) {
setInternalChecked(nextChecked === true);
}
onCheckedChange?.(nextChecked);
}}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator className="flex items-center justify-center text-current">
<Check className="h-3.5 w-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
</>
);
});
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
export { Checkbox };
+1 -1
View File
@@ -7,7 +7,7 @@ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
<input
type={type}
className={cn(
"flex h-10 w-full rounded-nested border border-input bg-surface-1 px-3 py-2 text-sm text-foreground ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base text-foreground ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className,
)}
ref={ref}
+195
View File
@@ -0,0 +1,195 @@
"use client";
import * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select";
import { Check, ChevronDown, ChevronUp } from "lucide-react";
import { cn } from "@/lib/utils";
type SelectProps = React.ComponentPropsWithoutRef<typeof SelectPrimitive.Root> & {
name?: string;
required?: boolean;
};
function Select({
name,
required,
value,
defaultValue,
onValueChange,
children,
...props
}: SelectProps) {
const [internalValue, setInternalValue] = React.useState(defaultValue ?? "");
const isControlled = value !== undefined;
const currentValue = (isControlled ? value : internalValue) as string;
return (
<>
{name ? <input type="hidden" name={name} value={currentValue} required={required} /> : null}
<SelectPrimitive.Root
value={value}
defaultValue={defaultValue}
onValueChange={(nextValue) => {
if (!isControlled) {
setInternalValue(nextValue);
}
onValueChange?.(nextValue);
}}
{...props}
>
{children}
</SelectPrimitive.Root>
</>
);
}
const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn("flex cursor-default items-center justify-center py-1", className)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
));
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn("flex cursor-default items-center justify-center py-1", className)}
{...props}
>
<ChevronUp className="h-4 w-4 rotate-180" />
</SelectPrimitive.ScrollDownButton>
));
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className,
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props}
/>
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground 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">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
};
+3 -3
View File
@@ -44,7 +44,7 @@ function TabsList({
return (
<div
className={cn(
"inline-flex h-auto flex-wrap items-center gap-2 rounded-surface border border-border bg-surface-2 p-1.5",
"inline-flex h-auto flex-wrap items-center gap-2 rounded-lg border bg-muted/40 p-1.5",
className,
)}
{...props}
@@ -69,8 +69,8 @@ function TabsTrigger({
<button
type="button"
className={cn(
"inline-flex items-center justify-center rounded-nested px-3 py-2 text-sm font-medium text-muted-foreground transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
isActive && "bg-surface-1 text-foreground shadow-xs",
"inline-flex items-center justify-center rounded-md px-3 py-2 text-sm font-medium text-muted-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
isActive && "bg-background text-foreground shadow-sm",
className,
)}
onClick={(event) => {
+1 -1
View File
@@ -6,7 +6,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, React.ComponentProps<"tex
({ className, ...props }, ref) => (
<textarea
className={cn(
"flex min-h-[112px] w-full rounded-nested border border-input bg-surface-1 px-3 py-2 text-sm text-foreground ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base text-foreground ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className,
)}
ref={ref}
+6 -6
View File
@@ -224,7 +224,7 @@ export function UiKitShowcase({ localeKey }: UiKitShowcaseProps) {
<div className="flex flex-col gap-section">
<AppCard level={3}>
<CardContent className="flex flex-col gap-4 p-6 lg:p-10">
<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">
<div className="inline-flex w-fit items-center gap-2 rounded-full border border-input bg-background px-3 py-1 text-xs font-semibold uppercase tracking-[0.14em] text-foreground/80">
<Layers3 className="h-3.5 w-3.5 text-brand-secondary" />
{copy.title}
</div>
@@ -292,10 +292,10 @@ export function UiKitShowcase({ localeKey }: UiKitShowcaseProps) {
</AppCard>
<AppCard level="inverse">
<CardContent className="p-5">
<p className="text-sm font-medium text-surface-inverse-foreground/80">
<p className="text-sm font-medium text-background/80">
{copy.inverseOuterCard}
</p>
<p className="mt-2 text-sm text-surface-inverse-foreground/88">
<p className="mt-2 text-sm text-background/88">
{copy.inverseOuterCardText}
</p>
</CardContent>
@@ -452,8 +452,8 @@ export function UiKitShowcase({ localeKey }: UiKitShowcaseProps) {
</AppCard>
<AppCard level="inverse">
<CardContent className="p-5">
<p className="font-medium text-surface-inverse-foreground">{copy.inverse}</p>
<p className="mt-2 text-sm text-surface-inverse-foreground/88">{copy.inverseText}</p>
<p className="font-medium text-background">{copy.inverse}</p>
<p className="mt-2 text-sm text-background/88">{copy.inverseText}</p>
</CardContent>
</AppCard>
</div>
@@ -492,7 +492,7 @@ export function UiKitShowcase({ localeKey }: UiKitShowcaseProps) {
<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">
<div className="inline-flex w-fit items-center gap-2 rounded-full border border-input bg-background px-3 py-1 text-xs font-semibold uppercase tracking-[0.14em] text-foreground/80">
<Type className="h-3.5 w-3.5 text-brand-primary" />
{copy.pageHeader}
</div>