@alpic-ai/ui 0.0.0 → 1.111.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dist/components/alert.d.mts +1 -1
  2. package/dist/components/avatar.d.mts +1 -1
  3. package/dist/components/badge.d.mts +1 -1
  4. package/dist/components/button.d.mts +2 -2
  5. package/dist/components/combobox.mjs +1 -1
  6. package/dist/components/dialog.mjs +2 -2
  7. package/dist/components/form.d.mts +119 -0
  8. package/dist/components/form.mjs +192 -0
  9. package/dist/components/input.d.mts +2 -0
  10. package/dist/components/input.mjs +19 -10
  11. package/dist/components/select-trigger-variants.mjs +1 -0
  12. package/dist/components/select.d.mts +1 -9
  13. package/dist/components/select.mjs +8 -28
  14. package/dist/components/sidebar.d.mts +1 -1
  15. package/dist/components/sidebar.mjs +6 -6
  16. package/dist/components/spinner.d.mts +1 -1
  17. package/dist/components/status-dot.d.mts +1 -1
  18. package/dist/components/tabs.d.mts +1 -1
  19. package/dist/components/textarea.d.mts +2 -0
  20. package/dist/components/textarea.mjs +19 -10
  21. package/dist/components/toggle-group.d.mts +1 -1
  22. package/package.json +31 -40
  23. package/src/components/combobox.tsx +1 -1
  24. package/src/components/dialog.tsx +2 -2
  25. package/src/components/form.tsx +343 -0
  26. package/src/components/input.tsx +12 -0
  27. package/src/components/select-trigger-variants.ts +1 -0
  28. package/src/components/select.tsx +2 -35
  29. package/src/components/sidebar.tsx +8 -10
  30. package/src/components/textarea.tsx +12 -1
  31. package/src/stories/accordion-card.stories.tsx +53 -0
  32. package/src/stories/accordion.stories.tsx +65 -0
  33. package/src/stories/alert.stories.tsx +58 -0
  34. package/src/stories/attachment-tile.stories.tsx +37 -0
  35. package/src/stories/avatar.stories.tsx +54 -0
  36. package/src/stories/badge.stories.tsx +50 -0
  37. package/src/stories/breadcrumb.stories.tsx +107 -0
  38. package/src/stories/button.stories.tsx +342 -0
  39. package/src/stories/card.stories.tsx +89 -0
  40. package/src/stories/checkbox.stories.tsx +56 -0
  41. package/src/stories/collapsible.stories.tsx +69 -0
  42. package/src/stories/combobox.stories.tsx +214 -0
  43. package/src/stories/command.stories.tsx +95 -0
  44. package/src/stories/copyable.stories.tsx +29 -0
  45. package/src/stories/description-list.stories.tsx +71 -0
  46. package/src/stories/dialog.stories.tsx +135 -0
  47. package/src/stories/dropdown-menu.stories.tsx +191 -0
  48. package/src/stories/form.stories.tsx +91 -0
  49. package/src/stories/input-group.stories.tsx +63 -0
  50. package/src/stories/input.stories.tsx +72 -0
  51. package/src/stories/label.stories.tsx +26 -0
  52. package/src/stories/ladle.css +3 -0
  53. package/src/stories/pagination.stories.tsx +35 -0
  54. package/src/stories/popover.stories.tsx +34 -0
  55. package/src/stories/radio-group.stories.tsx +59 -0
  56. package/src/stories/scroll-area.stories.tsx +43 -0
  57. package/src/stories/select.stories.tsx +95 -0
  58. package/src/stories/separator.stories.tsx +36 -0
  59. package/src/stories/sheet.stories.tsx +76 -0
  60. package/src/stories/sidebar.stories.tsx +255 -0
  61. package/src/stories/skeleton.stories.tsx +47 -0
  62. package/src/stories/sonner.stories.tsx +91 -0
  63. package/src/stories/spinner.stories.tsx +66 -0
  64. package/src/stories/status-dot.stories.tsx +27 -0
  65. package/src/stories/switch.stories.tsx +46 -0
  66. package/src/stories/table.stories.tsx +242 -0
  67. package/src/stories/tabs.stories.tsx +169 -0
  68. package/src/stories/tag.stories.tsx +82 -0
  69. package/src/stories/textarea.stories.tsx +60 -0
  70. package/src/stories/toggle-group.stories.tsx +142 -0
  71. package/src/stories/tooltip-icon-button.stories.tsx +59 -0
  72. package/src/stories/tooltip.stories.tsx +54 -0
@@ -316,21 +316,19 @@ function SidebarHeader({ className, icon, title, children, ...props }: SidebarHe
316
316
  <div
317
317
  data-slot="sidebar-header"
318
318
  data-sidebar="header"
319
- className={cn("flex flex-col gap-2 p-2 group-data-[collapsible=icon]:items-center", className)}
319
+ className={cn("flex flex-col gap-2 p-2", className)}
320
320
  {...props}
321
321
  >
322
- <div className="flex h-8 items-center gap-2 px-2 group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:gap-0 group-data-[collapsible=icon]:px-0">
322
+ <div className="flex h-8 items-center gap-2 px-3">
323
323
  <div className="relative shrink-0">
324
324
  <span className="transition-opacity group-data-[collapsible=icon]:group-hover:opacity-0">{icon}</span>
325
325
  <div className="absolute inset-0 flex items-center justify-center opacity-0 transition-opacity group-data-[collapsible=icon]:group-hover:opacity-100">
326
- <SidebarTrigger className="!size-4 !p-0" />
326
+ <SidebarTrigger />
327
327
  </div>
328
328
  </div>
329
- {title && (
330
- <span className="text-foreground text-md min-w-0 truncate font-medium group-data-[collapsible=icon]:hidden">
331
- {title}
332
- </span>
333
- )}
329
+ <span className="text-foreground text-md min-w-0 truncate font-medium group-data-[collapsible=icon]:hidden">
330
+ {title}
331
+ </span>
334
332
  <SidebarTrigger className="ml-auto shrink-0 group-data-[collapsible=icon]:hidden" />
335
333
  </div>
336
334
  {children}
@@ -376,7 +374,7 @@ function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
376
374
  <div
377
375
  data-slot="sidebar-group"
378
376
  data-sidebar="group"
379
- className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
377
+ className={cn("relative flex w-full min-w-0 flex-col", className)}
380
378
  {...props}
381
379
  />
382
380
  );
@@ -453,7 +451,7 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
453
451
  <li
454
452
  data-slot="sidebar-menu-item"
455
453
  data-sidebar="menu-item"
456
- className={cn("group/menu-item relative", className)}
454
+ className={cn("group/menu-item relative px-3", className)}
457
455
  {...props}
458
456
  />
459
457
  );
@@ -1,18 +1,21 @@
1
1
  "use client";
2
2
 
3
+ import { Info } from "lucide-react";
3
4
  import * as React from "react";
4
5
 
5
6
  import { cn } from "../lib/cn";
6
7
  import { Label } from "./label";
8
+ import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip";
7
9
 
8
10
  interface TextareaProps extends React.ComponentProps<"textarea"> {
9
11
  label?: string;
10
12
  required?: boolean;
11
13
  hint?: string;
12
14
  error?: string;
15
+ tooltip?: string;
13
16
  }
14
17
 
15
- function Textarea({ className, id, label, required, hint, error, ...props }: TextareaProps) {
18
+ function Textarea({ className, id, label, required, hint, error, tooltip, ...props }: TextareaProps) {
16
19
  const generatedId = React.useId();
17
20
  const fieldId = id ?? generatedId;
18
21
 
@@ -51,6 +54,14 @@ function Textarea({ className, id, label, required, hint, error, ...props }: Tex
51
54
  *
52
55
  </span>
53
56
  )}
57
+ {tooltip && (
58
+ <Tooltip>
59
+ <TooltipTrigger asChild>
60
+ <Info className="size-4 text-muted-foreground" />
61
+ </TooltipTrigger>
62
+ <TooltipContent>{tooltip}</TooltipContent>
63
+ </Tooltip>
64
+ )}
54
65
  </div>
55
66
  )}
56
67
  {textarea}
@@ -0,0 +1,53 @@
1
+ import type { Story } from "@ladle/react";
2
+ import {
3
+ AccordionCard,
4
+ AccordionCardContent,
5
+ AccordionCardHeader,
6
+ AccordionCardTitle,
7
+ } from "../components/accordion-card";
8
+
9
+ export const AllVariants: Story = () => (
10
+ <div className="flex flex-col gap-6 max-w-md">
11
+ <div className="flex flex-col gap-1.5">
12
+ <p className="type-text-xs font-medium text-subtle-foreground">Default (collapsed)</p>
13
+ <AccordionCard>
14
+ <AccordionCardHeader>
15
+ <AccordionCardTitle>Logs</AccordionCardTitle>
16
+ </AccordionCardHeader>
17
+ <AccordionCardContent>Content inside the accordion card.</AccordionCardContent>
18
+ </AccordionCard>
19
+ </div>
20
+
21
+ <div className="flex flex-col gap-1.5">
22
+ <p className="type-text-xs font-medium text-subtle-foreground">Default open</p>
23
+ <AccordionCard defaultOpen>
24
+ <AccordionCardHeader>
25
+ <AccordionCardTitle>Build settings</AccordionCardTitle>
26
+ </AccordionCardHeader>
27
+ <AccordionCardContent>
28
+ This card starts open. The chevron rotates to indicate the open state.
29
+ </AccordionCardContent>
30
+ </AccordionCard>
31
+ </div>
32
+
33
+ <div className="flex flex-col gap-1.5">
34
+ <p className="type-text-xs font-medium text-subtle-foreground">Without title component</p>
35
+ <AccordionCard>
36
+ <AccordionCardHeader>
37
+ <span className="type-text-sm font-medium">Environment Variables</span>
38
+ </AccordionCardHeader>
39
+ <AccordionCardContent>Header text can be any element, not just AccordionCardTitle.</AccordionCardContent>
40
+ </AccordionCard>
41
+ </div>
42
+
43
+ <div className="flex flex-col gap-1.5">
44
+ <p className="type-text-xs font-medium text-subtle-foreground">Disabled</p>
45
+ <AccordionCard disabled>
46
+ <AccordionCardHeader>
47
+ <AccordionCardTitle>Disabled card</AccordionCardTitle>
48
+ </AccordionCardHeader>
49
+ <AccordionCardContent>Cannot be opened.</AccordionCardContent>
50
+ </AccordionCard>
51
+ </div>
52
+ </div>
53
+ );
@@ -0,0 +1,65 @@
1
+ import type { Story } from "@ladle/react";
2
+ import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "../components/accordion";
3
+
4
+ export const AllVariants: Story = () => (
5
+ <div className="flex flex-col gap-10 max-w-md">
6
+ <div className="flex flex-col gap-1.5">
7
+ <p className="type-text-xs font-medium text-subtle-foreground">Single (collapsible)</p>
8
+ <Accordion type="single" collapsible>
9
+ <AccordionItem value="item-1">
10
+ <AccordionTrigger>What is Alpic?</AccordionTrigger>
11
+ <AccordionContent>
12
+ Alpic is a multi-tenant SaaS platform for deploying MCP servers. It handles infrastructure, scaling, and
13
+ distribution so you can focus on building.
14
+ </AccordionContent>
15
+ </AccordionItem>
16
+ <AccordionItem value="item-2">
17
+ <AccordionTrigger>How do I deploy?</AccordionTrigger>
18
+ <AccordionContent>
19
+ Use the Alpic CLI to deploy your MCP server with a single command. The platform handles containerization,
20
+ routing, and scaling automatically.
21
+ </AccordionContent>
22
+ </AccordionItem>
23
+ <AccordionItem value="item-3">
24
+ <AccordionTrigger>What languages are supported?</AccordionTrigger>
25
+ <AccordionContent>
26
+ Any language that can run in a container. TypeScript, Python, Go, Rust, and more are all supported out of
27
+ the box with pre-built runtime images.
28
+ </AccordionContent>
29
+ </AccordionItem>
30
+ </Accordion>
31
+ </div>
32
+
33
+ <div className="flex flex-col gap-1.5">
34
+ <p className="type-text-xs font-medium text-subtle-foreground">Multiple</p>
35
+ <Accordion type="multiple" defaultValue={["item-1", "item-3"]}>
36
+ <AccordionItem value="item-1">
37
+ <AccordionTrigger>First item (open by default)</AccordionTrigger>
38
+ <AccordionContent>Content for the first item. Multiple items can be open at the same time.</AccordionContent>
39
+ </AccordionItem>
40
+ <AccordionItem value="item-2">
41
+ <AccordionTrigger>Second item</AccordionTrigger>
42
+ <AccordionContent>Content for the second item.</AccordionContent>
43
+ </AccordionItem>
44
+ <AccordionItem value="item-3">
45
+ <AccordionTrigger>Third item (open by default)</AccordionTrigger>
46
+ <AccordionContent>Content for the third item.</AccordionContent>
47
+ </AccordionItem>
48
+ </Accordion>
49
+ </div>
50
+
51
+ <div className="flex flex-col gap-1.5">
52
+ <p className="type-text-xs font-medium text-subtle-foreground">Disabled</p>
53
+ <Accordion type="single" collapsible>
54
+ <AccordionItem value="item-1">
55
+ <AccordionTrigger>Enabled item</AccordionTrigger>
56
+ <AccordionContent>This item can be toggled.</AccordionContent>
57
+ </AccordionItem>
58
+ <AccordionItem value="item-2" disabled>
59
+ <AccordionTrigger>Disabled item</AccordionTrigger>
60
+ <AccordionContent>This content cannot be reached.</AccordionContent>
61
+ </AccordionItem>
62
+ </Accordion>
63
+ </div>
64
+ </div>
65
+ );
@@ -0,0 +1,58 @@
1
+ import type { Story } from "@ladle/react";
2
+ import { AlertCircle, AlertTriangle, CheckCircle2, Info } from "lucide-react";
3
+
4
+ import { Alert, AlertDescription, AlertTitle, ErrorAlert, WarningAlert } from "../components/alert";
5
+
6
+ export const AllVariants: Story = () => (
7
+ <div className="flex flex-col gap-4 max-w-xl p-6">
8
+ <p className="type-text-xs font-medium text-subtle-foreground">Default</p>
9
+ <Alert>
10
+ <Info />
11
+ <AlertTitle>Best on desktop</AlertTitle>
12
+ <AlertDescription>
13
+ This site isn't optimised for small screens. For the best experience we recommend using a desktop or tablet.
14
+ </AlertDescription>
15
+ </Alert>
16
+
17
+ <p className="type-text-xs font-medium text-subtle-foreground">Destructive</p>
18
+ <Alert variant="destructive">
19
+ <AlertCircle />
20
+ <AlertTitle>Clone failed</AlertTitle>
21
+ <AlertDescription>Unable to clone the repository. Check your credentials and try again.</AlertDescription>
22
+ </Alert>
23
+
24
+ <p className="type-text-xs font-medium text-subtle-foreground">Warning</p>
25
+ <Alert variant="warning">
26
+ <AlertTriangle />
27
+ <AlertTitle>API key expiring soon</AlertTitle>
28
+ <AlertDescription>Your API key expires in 3 days. Rotate it to avoid service interruption.</AlertDescription>
29
+ </Alert>
30
+
31
+ <p className="type-text-xs font-medium text-subtle-foreground">Success</p>
32
+ <Alert variant="success">
33
+ <CheckCircle2 />
34
+ <AlertTitle>Deployment complete</AlertTitle>
35
+ <AlertDescription>Your project was deployed successfully and is now live.</AlertDescription>
36
+ </Alert>
37
+
38
+ <p className="type-text-xs font-medium text-subtle-foreground">Description only (no title)</p>
39
+ <Alert variant="destructive">
40
+ <AlertCircle />
41
+ <AlertDescription>
42
+ This is the only time you'll be able to see this API key. Make sure to copy it and store it securely.
43
+ </AlertDescription>
44
+ </Alert>
45
+
46
+ <p className="type-text-xs font-medium text-subtle-foreground">ErrorAlert (pre-composed)</p>
47
+ <ErrorAlert />
48
+
49
+ <p className="type-text-xs font-medium text-subtle-foreground">ErrorAlert (custom)</p>
50
+ <ErrorAlert title="Error loading projects" description="Failed to fetch your projects. Please refresh the page." />
51
+
52
+ <p className="type-text-xs font-medium text-subtle-foreground">WarningAlert</p>
53
+ <WarningAlert
54
+ title="Save your API key"
55
+ description="This is the only time you'll be able to see this API key. Make sure to copy it and store it securely."
56
+ />
57
+ </div>
58
+ );
@@ -0,0 +1,37 @@
1
+ import type { Story } from "@ladle/react";
2
+ import { AttachmentTile } from "../components/attachment-tile";
3
+
4
+ export const AllVariants: Story = () => (
5
+ <div className="flex flex-col gap-10 p-10">
6
+ <div className="flex flex-col gap-1.5">
7
+ <p className="type-text-xs font-medium text-subtle-foreground">Image attachment (with remove)</p>
8
+ <div className="flex gap-3">
9
+ <AttachmentTile
10
+ src="https://picsum.photos/seed/a/200/200"
11
+ isImage
12
+ onRemove={() => alert("Remove clicked")}
13
+ onClick={() => alert("Preview clicked")}
14
+ />
15
+ <AttachmentTile
16
+ src="https://picsum.photos/seed/b/200/200"
17
+ isImage
18
+ onRemove={() => alert("Remove clicked")}
19
+ onClick={() => alert("Preview clicked")}
20
+ />
21
+ </div>
22
+ </div>
23
+
24
+ <div className="flex flex-col gap-1.5">
25
+ <p className="type-text-xs font-medium text-subtle-foreground">File attachment (fallback icon)</p>
26
+ <AttachmentTile onRemove={() => alert("Remove clicked")} onClick={() => alert("Preview clicked")} />
27
+ </div>
28
+
29
+ <div className="flex flex-col gap-1.5">
30
+ <p className="type-text-xs font-medium text-subtle-foreground">Read-only (no remove button)</p>
31
+ <div className="flex gap-3">
32
+ <AttachmentTile src="https://picsum.photos/seed/c/200/200" isImage />
33
+ <AttachmentTile />
34
+ </div>
35
+ </div>
36
+ </div>
37
+ );
@@ -0,0 +1,54 @@
1
+ import type { Story } from "@ladle/react";
2
+ import { Avatar, AvatarFallback, AvatarImage, AvatarLabelGroup } from "../components/avatar";
3
+
4
+ const DEMO_SRC = "https://i.pravatar.cc/150";
5
+ const SIZES = ["xs", "sm", "md", "lg", "xl"] as const;
6
+
7
+ export const AllVariants: Story = () => (
8
+ <div className="flex flex-col gap-8 p-6 bg-background">
9
+ <div className="flex flex-col gap-3">
10
+ <p className="type-text-xs font-medium text-subtle-foreground">Sizes — with image</p>
11
+ <div className="flex items-center gap-4">
12
+ {SIZES.map((size) => (
13
+ <Avatar key={size} size={size}>
14
+ <AvatarImage src={DEMO_SRC} alt="User" />
15
+ <AvatarFallback>JD</AvatarFallback>
16
+ </Avatar>
17
+ ))}
18
+ </div>
19
+ </div>
20
+
21
+ <div className="flex flex-col gap-3">
22
+ <p className="type-text-xs font-medium text-subtle-foreground">Sizes — fallback (no image)</p>
23
+ <div className="flex items-center gap-4">
24
+ {SIZES.map((size) => (
25
+ <Avatar key={size} size={size}>
26
+ <AvatarFallback>OR</AvatarFallback>
27
+ </Avatar>
28
+ ))}
29
+ </div>
30
+ </div>
31
+
32
+ <div className="flex flex-col gap-3">
33
+ <p className="type-text-xs font-medium text-subtle-foreground">Online status indicator</p>
34
+ <div className="flex items-center gap-4">
35
+ {SIZES.map((size) => (
36
+ <Avatar key={size} size={size} status="online">
37
+ <AvatarImage src={DEMO_SRC} alt="User" />
38
+ <AvatarFallback>JD</AvatarFallback>
39
+ </Avatar>
40
+ ))}
41
+ </div>
42
+ </div>
43
+
44
+ <div className="flex flex-col gap-3">
45
+ <p className="type-text-xs font-medium text-subtle-foreground">Label group</p>
46
+ <div className="flex flex-col gap-4">
47
+ <AvatarLabelGroup size="sm" src={DEMO_SRC} name="Alice Martin" label="alice@example.com" />
48
+ <AvatarLabelGroup size="md" src={DEMO_SRC} name="Bob Dupont" label="bob@example.com" />
49
+ <AvatarLabelGroup size="md" src={DEMO_SRC} name="Charly Bernard" label="charly@example.com" status="online" />
50
+ <AvatarLabelGroup size="md" name="Daniel Wilson" label="daniel@example.com" />
51
+ </div>
52
+ </div>
53
+ </div>
54
+ );
@@ -0,0 +1,50 @@
1
+ import type { Story } from "@ladle/react";
2
+ import { AlertTriangle, CheckCircle2, XCircle } from "lucide-react";
3
+
4
+ import { Badge } from "../components/badge";
5
+
6
+ export const AllVariants: Story = () => (
7
+ <div className="flex flex-col gap-6">
8
+ <div className="flex flex-col gap-1.5">
9
+ <p className="type-text-xs font-medium text-subtle-foreground">Medium (default)</p>
10
+ <div className="flex items-center gap-2">
11
+ <Badge variant="primary">Primary</Badge>
12
+ <Badge>Secondary</Badge>
13
+ <Badge variant="success">
14
+ <CheckCircle2 />
15
+ Success
16
+ </Badge>
17
+ <Badge variant="warning">
18
+ <AlertTriangle />
19
+ Warning
20
+ </Badge>
21
+ <Badge variant="error">
22
+ <XCircle />
23
+ Error
24
+ </Badge>
25
+ </div>
26
+ </div>
27
+
28
+ <div className="flex flex-col gap-1.5">
29
+ <p className="type-text-xs font-medium text-subtle-foreground">Small</p>
30
+ <div className="flex items-center gap-2">
31
+ <Badge size="sm" variant="primary">
32
+ Primary
33
+ </Badge>
34
+ <Badge size="sm">Secondary</Badge>
35
+ <Badge size="sm" variant="success">
36
+ <CheckCircle2 />
37
+ Success
38
+ </Badge>
39
+ <Badge size="sm" variant="warning">
40
+ <AlertTriangle />
41
+ Warning
42
+ </Badge>
43
+ <Badge size="sm" variant="error">
44
+ <XCircle />
45
+ Error
46
+ </Badge>
47
+ </div>
48
+ </div>
49
+ </div>
50
+ );
@@ -0,0 +1,107 @@
1
+ import type { Story } from "@ladle/react";
2
+ import { Home } from "lucide-react";
3
+ import { useState } from "react";
4
+ import {
5
+ Breadcrumb,
6
+ BreadcrumbChevron,
7
+ BreadcrumbItem,
8
+ BreadcrumbLink,
9
+ BreadcrumbList,
10
+ BreadcrumbPage,
11
+ BreadcrumbSeparator,
12
+ } from "../components/breadcrumb";
13
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../components/dropdown-menu";
14
+
15
+ const TREE: Record<string, string[]> = {
16
+ Home: ["Projects", "Settings", "Billing"],
17
+ Projects: ["my-mcp-server", "weather-api", "slack-bot"],
18
+ "my-mcp-server": ["Deployments", "Logs", "Environment"],
19
+ };
20
+
21
+ export const AllVariants: Story = () => {
22
+ const [trail, setTrail] = useState(["Home", "Projects", "my-mcp-server"]);
23
+
24
+ function handleSelect(index: number, option: string) {
25
+ setTrail([...trail.slice(0, index + 1), option]);
26
+ }
27
+
28
+ return (
29
+ <div className="flex flex-col gap-10">
30
+ <div className="flex flex-col gap-1.5">
31
+ <p className="type-text-xs font-medium text-subtle-foreground">
32
+ Interactive — click items to navigate, click chevrons for dropdown
33
+ </p>
34
+ <Breadcrumb>
35
+ <BreadcrumbList>
36
+ {trail.map((page, index) => {
37
+ const isLast = index === trail.length - 1;
38
+ const children = TREE[page];
39
+ return (
40
+ <li key={page} className="contents">
41
+ {index > 0 && <BreadcrumbSeparator />}
42
+ <DropdownMenu>
43
+ <DropdownMenuTrigger asChild>
44
+ <BreadcrumbItem className="cursor-pointer focus-visible:ring-2 focus-visible:ring-ring outline-none">
45
+ {isLast ? (
46
+ <BreadcrumbPage>{page}</BreadcrumbPage>
47
+ ) : (
48
+ <span className="type-text-sm font-medium text-subtle-foreground">{page}</span>
49
+ )}
50
+ {children && <BreadcrumbChevron />}
51
+ </BreadcrumbItem>
52
+ </DropdownMenuTrigger>
53
+ {children && (
54
+ <DropdownMenuContent align="start">
55
+ {children.map((child) => (
56
+ <DropdownMenuItem key={child} onSelect={() => handleSelect(index, child)}>
57
+ {child}
58
+ </DropdownMenuItem>
59
+ ))}
60
+ </DropdownMenuContent>
61
+ )}
62
+ </DropdownMenu>
63
+ </li>
64
+ );
65
+ })}
66
+ </BreadcrumbList>
67
+ </Breadcrumb>
68
+ </div>
69
+
70
+ <div className="flex flex-col gap-1.5">
71
+ <p className="type-text-xs font-medium text-subtle-foreground">With home icon (static)</p>
72
+ <Breadcrumb>
73
+ <BreadcrumbList>
74
+ <BreadcrumbItem>
75
+ <BreadcrumbLink href="#">
76
+ <Home className="size-5" />
77
+ </BreadcrumbLink>
78
+ </BreadcrumbItem>
79
+ <BreadcrumbSeparator />
80
+ <BreadcrumbItem>
81
+ <BreadcrumbLink href="#">Projects</BreadcrumbLink>
82
+ </BreadcrumbItem>
83
+ <BreadcrumbSeparator />
84
+ <BreadcrumbItem>
85
+ <BreadcrumbPage>my-mcp-server</BreadcrumbPage>
86
+ </BreadcrumbItem>
87
+ </BreadcrumbList>
88
+ </Breadcrumb>
89
+ </div>
90
+
91
+ <div className="flex flex-col gap-1.5">
92
+ <p className="type-text-xs font-medium text-subtle-foreground">Simple — two levels</p>
93
+ <Breadcrumb>
94
+ <BreadcrumbList>
95
+ <BreadcrumbItem>
96
+ <BreadcrumbLink href="#">Projects</BreadcrumbLink>
97
+ </BreadcrumbItem>
98
+ <BreadcrumbSeparator />
99
+ <BreadcrumbItem>
100
+ <BreadcrumbPage>my-mcp-server</BreadcrumbPage>
101
+ </BreadcrumbItem>
102
+ </BreadcrumbList>
103
+ </Breadcrumb>
104
+ </div>
105
+ </div>
106
+ );
107
+ };