Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 123 additions & 33 deletions src/app/(dashboard)/library/shared-components/new/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { useEnvironmentStore } from "@/stores/environment-store";
import { VECTOR_CATALOG } from "@/lib/vector/catalog";
import { toast } from "sonner";
import Link from "next/link";
import { ArrowLeft, Loader2, Plus, Search } from "lucide-react";
import { ArrowLeft, ChevronDown, Loader2, Plus, Search } from "lucide-react";

import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
Expand All @@ -22,12 +22,18 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { SchemaForm } from "@/components/config-forms/schema-form";
import { cn } from "@/lib/utils";

import type { VectorComponentDef } from "@/lib/vector/types";

/* ------------------------------------------------------------------ */
/* Kind badge styling */
/* Kind styling */
/* ------------------------------------------------------------------ */

const kindVariant: Record<string, string> = {
Expand All @@ -38,6 +44,23 @@ const kindVariant: Record<string, string> = {
sink: "bg-orange-100 text-orange-800 dark:bg-orange-900/40 dark:text-orange-300",
};

const kindSectionConfig: Record<string, { label: string; accent: string }> = {
source: {
label: "Sources",
accent: "text-emerald-600 dark:text-emerald-400",
},
transform: {
label: "Transforms",
accent: "text-sky-600 dark:text-sky-400",
},
sink: {
label: "Sinks",
accent: "text-orange-600 dark:text-orange-400",
},
};

const KIND_ORDER = ["source", "transform", "sink"] as const;

/* ------------------------------------------------------------------ */
/* Page Component */
/* ------------------------------------------------------------------ */
Expand Down Expand Up @@ -71,6 +94,16 @@ export default function NewSharedComponentPage() {
);
}, [search]);

const groupedCatalog = useMemo(
() =>
KIND_ORDER.map((kind) => ({
kind,
...kindSectionConfig[kind],
items: filteredCatalog.filter((c) => c.kind === kind),
})),
[filteredCatalog],
);

const createMutation = useMutation(
trpc.sharedComponent.create.mutationOptions({
onSuccess: (sc) => {
Expand Down Expand Up @@ -162,42 +195,30 @@ export default function NewSharedComponentPage() {
/>
</div>

{/* Component grid */}
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{filteredCatalog.map((comp) => (
<Card
key={`${comp.kind}-${comp.type}`}
className="cursor-pointer transition-colors hover:bg-accent/50"
onClick={() => handleSelectComponent(comp)}
>
<CardHeader className="pb-2">
<div className="flex items-start justify-between gap-2">
<CardTitle className="text-sm">
{comp.displayName}
</CardTitle>
<Badge
variant="outline"
className={kindVariant[comp.kind] ?? ""}
>
{comp.kind}
</Badge>
</div>
</CardHeader>
<CardContent>
<p className="text-xs text-muted-foreground line-clamp-2">
{comp.description}
</p>
</CardContent>
</Card>
))}
</div>

{filteredCatalog.length === 0 && (
{/* Component sections by kind */}
{filteredCatalog.length === 0 ? (
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed p-12 text-center">
<p className="text-muted-foreground">
No components match your search.
</p>
</div>
) : (
<div className="space-y-3">
{groupedCatalog.map((group) => {
if (group.items.length === 0) return null;
return (
<CatalogKindSection
key={group.kind}
label={group.label}
accent={group.accent}
badgeClass={kindVariant[group.kind] ?? ""}
count={group.items.length}
items={group.items}
onSelect={handleSelectComponent}
/>
);
})}
</div>
)}
</>
)}
Expand Down Expand Up @@ -289,3 +310,72 @@ export default function NewSharedComponentPage() {
</div>
);
}

/* ------------------------------------------------------------------ */
/* Catalog Kind Section (collapsible) */
/* ------------------------------------------------------------------ */

function CatalogKindSection({
label,
accent,
badgeClass,
count,
items,
onSelect,
}: {
label: string;
accent: string;
badgeClass: string;
count: number;
items: VectorComponentDef[];
onSelect: (comp: VectorComponentDef) => void;
}) {
const [open, setOpen] = useState(true);

return (
<Collapsible open={open} onOpenChange={setOpen}>
<CollapsibleTrigger className="flex w-full cursor-pointer items-center gap-2 rounded-lg border bg-muted/40 px-4 py-3 text-left transition-colors hover:bg-muted/70 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2">
<ChevronDown
className={cn(
"h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200",
!open && "-rotate-90",
)}
/>
<span className={cn("text-sm font-semibold", accent)}>{label}</span>
<Badge variant="secondary" className="ml-auto text-xs">
{count}
</Badge>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="mt-2 grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{items.map((comp) => (
<Card
key={`${comp.kind}-${comp.type}`}
className="cursor-pointer transition-colors hover:bg-accent/50"
onClick={() => onSelect(comp)}
>
<CardHeader className="pb-2">
<div className="flex items-start justify-between gap-2">
<CardTitle className="text-sm">
{comp.displayName}
</CardTitle>
<Badge
variant="outline"
className={badgeClass}
>
{comp.kind}
</Badge>
</div>
</CardHeader>
<CardContent>
<p className="text-xs text-muted-foreground line-clamp-2">
{comp.description}
</p>
</CardContent>
</Card>
))}
</div>
</CollapsibleContent>
</Collapsible>
);
}
40 changes: 25 additions & 15 deletions src/app/(dashboard)/library/shared-components/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import { Skeleton } from "@/components/ui/skeleton";
import { PageHeader } from "@/components/page-header";
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Collapsible,
CollapsibleContent,
Expand Down Expand Up @@ -216,26 +222,30 @@ function KindSection({
</Badge>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="mt-1 divide-y rounded-lg border">
<div className="mt-2 grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{items.map((sc) => (
<button
<Card
key={sc.id}
className="cursor-pointer transition-colors hover:bg-accent/50"
onClick={() => onItemClick(sc.id)}
className="flex w-full cursor-pointer items-center gap-4 px-4 py-3 text-left transition-colors hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset"
>
<Link2 className="h-4 w-4 shrink-0 text-muted-foreground" />
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium">{sc.name}</p>
<CardHeader className="pb-2">
<div className="flex items-start justify-between gap-2">
<CardTitle className="text-sm">{sc.name}</CardTitle>
<Badge variant="outline" className={cn("text-xs", badgeClass)}>
v{sc.version}
</Badge>
</div>
</CardHeader>
<CardContent>
<p className="text-xs text-muted-foreground">{sc.componentType}</p>
</div>
<div className="flex items-center gap-3 shrink-0 text-xs text-muted-foreground">
<span>{sc.linkedPipelineCount} linked</span>
<Badge variant="outline" className={cn("text-xs", badgeClass)}>
v{sc.version}
</Badge>
<span className="w-16 text-right">{formatRelativeTime(sc.updatedAt)}</span>
</div>
</button>
<div className="mt-2 flex items-center gap-2 text-xs text-muted-foreground">
<Link2 className="h-3 w-3" />
<span>{sc.linkedPipelineCount} linked</span>
<span className="ml-auto">{formatRelativeTime(sc.updatedAt)}</span>
</div>
</CardContent>
</Card>
))}
Comment on lines 226 to 249
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keyboard accessibility regression

The previous implementation used a native <button> element (see the diff), which is natively keyboard-focusable (Tab) and activatable (Enter/Space). The replacement <Card> renders as a <div>, which receives no keyboard focus by default, and the focus-visible:ring styles were removed along with it.

Users who navigate with a keyboard or assistive technology can no longer reach or activate individual component cards. To restore the original behaviour, add tabIndex={0}, role="button", an onKeyDown handler, and the focus-visible ring classes back to the <Card>.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/(dashboard)/library/shared-components/page.tsx
Line: 226-249

Comment:
**Keyboard accessibility regression**

The previous implementation used a native `<button>` element (see the diff), which is natively keyboard-focusable (Tab) and activatable (Enter/Space). The replacement `<Card>` renders as a `<div>`, which receives no keyboard focus by default, and the `focus-visible:ring` styles were removed along with it.

Users who navigate with a keyboard or assistive technology can no longer reach or activate individual component cards. To restore the original behaviour, add `tabIndex={0}`, `role="button"`, an `onKeyDown` handler, and the focus-visible ring classes back to the `<Card>`.

How can I resolve this? If you propose a fix, please make it concise.

</div>
</CollapsibleContent>
Expand Down
Loading