@olympusoss/canvas 2.6.19

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 (128) hide show
  1. package/package.json +179 -0
  2. package/src/components/atoms/README.md +11 -0
  3. package/src/components/atoms/aspect-ratio.tsx +32 -0
  4. package/src/components/atoms/avatar.tsx +98 -0
  5. package/src/components/atoms/badge.tsx +44 -0
  6. package/src/components/atoms/brand-mark.tsx +74 -0
  7. package/src/components/atoms/button.tsx +104 -0
  8. package/src/components/atoms/checkbox.tsx +63 -0
  9. package/src/components/atoms/flex-box.tsx +105 -0
  10. package/src/components/atoms/icon.tsx +34 -0
  11. package/src/components/atoms/input.tsx +91 -0
  12. package/src/components/atoms/label.tsx +41 -0
  13. package/src/components/atoms/logo.tsx +89 -0
  14. package/src/components/atoms/progress.tsx +55 -0
  15. package/src/components/atoms/radio-group.tsx +122 -0
  16. package/src/components/atoms/scroll-area.tsx +106 -0
  17. package/src/components/atoms/section.tsx +48 -0
  18. package/src/components/atoms/separator.tsx +45 -0
  19. package/src/components/atoms/skeleton.tsx +17 -0
  20. package/src/components/atoms/slider.tsx +93 -0
  21. package/src/components/atoms/switch.tsx +60 -0
  22. package/src/components/atoms/textarea.tsx +78 -0
  23. package/src/components/atoms/toggle.tsx +80 -0
  24. package/src/components/charts/activity-heatmap.tsx +96 -0
  25. package/src/components/charts/axes.tsx +21 -0
  26. package/src/components/charts/chart-container.tsx +195 -0
  27. package/src/components/charts/chart-legend.tsx +67 -0
  28. package/src/components/charts/chart-tooltip.tsx +161 -0
  29. package/src/components/charts/chart-types.tsx +49 -0
  30. package/src/components/charts/containers.tsx +11 -0
  31. package/src/components/charts/data.tsx +16 -0
  32. package/src/components/charts/details.tsx +25 -0
  33. package/src/components/charts/gauge.tsx +106 -0
  34. package/src/components/charts/grids.tsx +8 -0
  35. package/src/components/charts/index.ts +62 -0
  36. package/src/components/charts/labeled-bar-list.tsx +85 -0
  37. package/src/components/charts/references.tsx +8 -0
  38. package/src/components/charts/service-health-list.tsx +73 -0
  39. package/src/components/charts/sparkline.tsx +52 -0
  40. package/src/components/charts/stacked-bar.tsx +104 -0
  41. package/src/components/charts/text.tsx +10 -0
  42. package/src/components/charts/world-heat-map-inner.tsx +317 -0
  43. package/src/components/charts/world-heat-map.tsx +184 -0
  44. package/src/components/molecules/README.md +12 -0
  45. package/src/components/molecules/action-bar.tsx +73 -0
  46. package/src/components/molecules/activity-item.tsx +74 -0
  47. package/src/components/molecules/alert.tsx +80 -0
  48. package/src/components/molecules/animated-background.tsx +92 -0
  49. package/src/components/molecules/brand-lockup.tsx +48 -0
  50. package/src/components/molecules/breadcrumb.tsx +161 -0
  51. package/src/components/molecules/button-group.tsx +104 -0
  52. package/src/components/molecules/calendar.tsx +216 -0
  53. package/src/components/molecules/card.tsx +101 -0
  54. package/src/components/molecules/code-block.tsx +48 -0
  55. package/src/components/molecules/empty-state.tsx +55 -0
  56. package/src/components/molecules/error-state.tsx +42 -0
  57. package/src/components/molecules/field-display.tsx +35 -0
  58. package/src/components/molecules/input-otp.tsx +74 -0
  59. package/src/components/molecules/loading-state.tsx +36 -0
  60. package/src/components/molecules/notification-item.tsx +67 -0
  61. package/src/components/molecules/notification-list.tsx +45 -0
  62. package/src/components/molecules/number-badge.tsx +53 -0
  63. package/src/components/molecules/page-header.tsx +88 -0
  64. package/src/components/molecules/page-tabs.tsx +94 -0
  65. package/src/components/molecules/pagination.tsx +150 -0
  66. package/src/components/molecules/phone-input.tsx +200 -0
  67. package/src/components/molecules/search-bar.tsx +64 -0
  68. package/src/components/molecules/secret-field.tsx +158 -0
  69. package/src/components/molecules/section-card.tsx +91 -0
  70. package/src/components/molecules/stat-card.tsx +96 -0
  71. package/src/components/molecules/status-badge.tsx +42 -0
  72. package/src/components/molecules/stepper.tsx +96 -0
  73. package/src/components/molecules/table.tsx +157 -0
  74. package/src/components/molecules/toggle-group.tsx +145 -0
  75. package/src/components/molecules/tooltip.tsx +150 -0
  76. package/src/components/molecules/user-avatar-chip.tsx +71 -0
  77. package/src/components/organisms/README.md +14 -0
  78. package/src/components/organisms/accordion.tsx +149 -0
  79. package/src/components/organisms/alert-dialog.tsx +269 -0
  80. package/src/components/organisms/carousel.tsx +244 -0
  81. package/src/components/organisms/collapsible.tsx +69 -0
  82. package/src/components/organisms/command.tsx +143 -0
  83. package/src/components/organisms/context-menu.tsx +333 -0
  84. package/src/components/organisms/dashboard-grid.tsx +360 -0
  85. package/src/components/organisms/data-table.tsx +330 -0
  86. package/src/components/organisms/dialog.tsx +304 -0
  87. package/src/components/organisms/drawer.tsx +100 -0
  88. package/src/components/organisms/dropdown-menu.tsx +434 -0
  89. package/src/components/organisms/editors/code-editor.tsx +144 -0
  90. package/src/components/organisms/editors/index.ts +4 -0
  91. package/src/components/organisms/editors/markdown-editor.tsx +153 -0
  92. package/src/components/organisms/editors/markdown-renderer.ts +27 -0
  93. package/src/components/organisms/editors/prose-canvas-classes.ts +45 -0
  94. package/src/components/organisms/editors/rich-text-editor.tsx +126 -0
  95. package/src/components/organisms/editors/toolbar/md-toolbar.tsx +129 -0
  96. package/src/components/organisms/editors/toolbar/rte-toolbar.tsx +211 -0
  97. package/src/components/organisms/editors/toolbar/toolbar-shell.tsx +45 -0
  98. package/src/components/organisms/editors/use-codemirror-theme.ts +61 -0
  99. package/src/components/organisms/error-boundary.tsx +61 -0
  100. package/src/components/organisms/form.tsx +174 -0
  101. package/src/components/organisms/hover-card.tsx +114 -0
  102. package/src/components/organisms/menubar.tsx +491 -0
  103. package/src/components/organisms/navbar.tsx +101 -0
  104. package/src/components/organisms/navigation-menu.tsx +234 -0
  105. package/src/components/organisms/popover.tsx +144 -0
  106. package/src/components/organisms/resizable.tsx +39 -0
  107. package/src/components/organisms/schema-form.tsx +232 -0
  108. package/src/components/organisms/select.tsx +303 -0
  109. package/src/components/organisms/sheet.tsx +256 -0
  110. package/src/components/organisms/sidebar.tsx +1037 -0
  111. package/src/components/organisms/sonner.tsx +96 -0
  112. package/src/components/organisms/tabs.tsx +132 -0
  113. package/src/components/organisms/theme-provider.tsx +101 -0
  114. package/src/hooks/use-mobile.tsx +19 -0
  115. package/src/index.ts +547 -0
  116. package/src/lib/portal-container.tsx +35 -0
  117. package/src/lib/utils.ts +6 -0
  118. package/src/native.ts +23 -0
  119. package/src/tokens/colors.ts +91 -0
  120. package/src/tokens/index.ts +3 -0
  121. package/src/tokens/spacing.ts +55 -0
  122. package/src/tokens/typography.ts +27 -0
  123. package/styles/canvas.css +55 -0
  124. package/styles/dashboard-grid.css +47 -0
  125. package/styles/leaflet.css +13 -0
  126. package/styles/tokens.css +234 -0
  127. package/tailwind.config.ts +70 -0
  128. package/tsconfig.json +23 -0
@@ -0,0 +1,73 @@
1
+ "use client";
2
+
3
+ import type * as React from "react";
4
+
5
+ import { cn } from "../../lib/utils";
6
+ import { Button } from "../atoms/button";
7
+ import { Icon } from "../atoms/icon";
8
+
9
+ export interface ActionBarAction {
10
+ label: string;
11
+ onClick: () => void;
12
+ icon?: React.ReactNode;
13
+ loading?: boolean;
14
+ disabled?: boolean;
15
+ }
16
+
17
+ export interface ActionBarSecondary extends ActionBarAction {
18
+ variant?: "outline" | "ghost";
19
+ }
20
+
21
+ export interface ActionBarProps {
22
+ primaryAction?: ActionBarAction;
23
+ secondaryActions?: ActionBarSecondary[];
24
+ align?: "left" | "right" | "center" | "space-between";
25
+ className?: string;
26
+ }
27
+
28
+ const ALIGN: Record<NonNullable<ActionBarProps["align"]>, string> = {
29
+ left: "justify-start",
30
+ right: "justify-end",
31
+ center: "justify-center",
32
+ "space-between": "justify-between",
33
+ };
34
+
35
+ export function ActionBar({
36
+ primaryAction,
37
+ secondaryActions = [],
38
+ align = "right",
39
+ className,
40
+ }: ActionBarProps) {
41
+ return (
42
+ <div className={cn("flex items-center gap-2", ALIGN[align], className)}>
43
+ {secondaryActions.map((a) => (
44
+ <Button
45
+ key={a.label}
46
+ onClick={a.onClick}
47
+ disabled={a.disabled || a.loading}
48
+ variant={a.variant ?? "outline"}
49
+ size="sm"
50
+ >
51
+ {a.icon && <span className="mr-1">{a.icon}</span>}
52
+ {a.label}
53
+ </Button>
54
+ ))}
55
+ {primaryAction && (
56
+ <Button
57
+ onClick={primaryAction.onClick}
58
+ disabled={primaryAction.disabled || primaryAction.loading}
59
+ size="sm"
60
+ >
61
+ {primaryAction.loading ? (
62
+ <Icon name="LoaderCircle" className="mr-1 h-4 w-4 animate-spin" />
63
+ ) : (
64
+ primaryAction.icon && <span className="mr-1">{primaryAction.icon}</span>
65
+ )}
66
+ {primaryAction.label}
67
+ </Button>
68
+ )}
69
+ </div>
70
+ );
71
+ }
72
+
73
+ ActionBar.displayName = "ActionBar";
@@ -0,0 +1,74 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "../../lib/utils";
4
+
5
+ export interface ActivityItemProps extends React.HTMLAttributes<HTMLLIElement> {
6
+ /** Bold subject (the actor). */
7
+ subject: React.ReactNode;
8
+ /** Muted action verb + object. */
9
+ action: React.ReactNode;
10
+ /** Right-aligned timestamp. */
11
+ timestamp?: React.ReactNode;
12
+ /** Leading icon or avatar slot. */
13
+ leading?: React.ReactNode;
14
+ /** When provided, the row becomes clickable. */
15
+ onClick?: () => void;
16
+ /**
17
+ * Index in a list — when 0, the top border is suppressed so consecutive
18
+ * items only render dividers between rows. Defaults to 1 (border on).
19
+ */
20
+ index?: number;
21
+ }
22
+
23
+ export const ActivityItem = React.forwardRef<HTMLLIElement, ActivityItemProps>(
24
+ ({ subject, action, timestamp, leading, onClick, index = 1, className, ...props }, ref) => {
25
+ const Row = onClick ? "button" : "div";
26
+ return (
27
+ <li
28
+ ref={ref}
29
+ className={cn(
30
+ "flex items-start gap-3 py-3",
31
+ index > 0 && "border-t border-border",
32
+ className,
33
+ )}
34
+ {...props}
35
+ >
36
+ {leading && <div className="shrink-0">{leading}</div>}
37
+ <Row
38
+ type={onClick ? "button" : undefined}
39
+ onClick={onClick}
40
+ className={cn(
41
+ "flex w-full items-start justify-between gap-4 text-left",
42
+ onClick && "transition-colors hover:text-foreground",
43
+ )}
44
+ >
45
+ <p className="m-0 min-w-0 text-[13.5px]">
46
+ <span className="font-medium text-foreground">{subject}</span>{" "}
47
+ <span className="text-muted-foreground">{action}</span>
48
+ </p>
49
+ {timestamp != null && (
50
+ <span className="shrink-0 font-mono text-[11px] text-muted-foreground">
51
+ {timestamp}
52
+ </span>
53
+ )}
54
+ </Row>
55
+ </li>
56
+ );
57
+ },
58
+ );
59
+ ActivityItem.displayName = "ActivityItem";
60
+
61
+ export interface ActivityFeedProps extends React.HTMLAttributes<HTMLUListElement> {
62
+ children?: React.ReactNode;
63
+ }
64
+
65
+ export const ActivityFeed = React.forwardRef<HTMLUListElement, ActivityFeedProps>(
66
+ ({ children, className, ...props }, ref) => {
67
+ return (
68
+ <ul ref={ref} className={cn("m-0 list-none p-0", className)} {...props}>
69
+ {children}
70
+ </ul>
71
+ );
72
+ },
73
+ );
74
+ ActivityFeed.displayName = "ActivityFeed";
@@ -0,0 +1,80 @@
1
+ import { cva, type VariantProps } from "class-variance-authority";
2
+ import * as React from "react";
3
+
4
+ import { cn } from "../../lib/utils";
5
+
6
+ const alertVariants = cva(
7
+ "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "bg-background text-foreground",
12
+ destructive:
13
+ "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
14
+ },
15
+ },
16
+ defaultVariants: {
17
+ variant: "default",
18
+ },
19
+ },
20
+ );
21
+
22
+ export interface AlertProps
23
+ extends React.HTMLAttributes<HTMLDivElement>,
24
+ VariantProps<typeof alertVariants> {
25
+ /**
26
+ * Visual emphasis preset. `default` is informational, `destructive`
27
+ * uses the danger palette for errors and warnings.
28
+ * @default "default"
29
+ */
30
+ variant?: "default" | "destructive";
31
+ /**
32
+ * Optional leading icon (lucide-react), `<AlertTitle>`, and
33
+ * `<AlertDescription>`. The icon — when present as a direct child —
34
+ * is auto-positioned in the top-left.
35
+ */
36
+ children?: React.ReactNode;
37
+ /** Tailwind / CSS classes merged onto the alert via `cn()`. */
38
+ className?: string;
39
+ }
40
+
41
+ const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
42
+ ({ className, variant, ...props }, ref) => (
43
+ <div ref={ref} role="alert" className={cn(alertVariants({ variant }), className)} {...props} />
44
+ ),
45
+ );
46
+ Alert.displayName = "Alert";
47
+
48
+ export interface AlertTitleProps extends React.HTMLAttributes<HTMLHeadingElement> {
49
+ /** The alert headline. Renders as an `<h5>`. */
50
+ children?: React.ReactNode;
51
+ /** Tailwind / CSS classes merged onto the title via `cn()`. */
52
+ className?: string;
53
+ }
54
+
55
+ const AlertTitle = React.forwardRef<HTMLParagraphElement, AlertTitleProps>(
56
+ ({ className, ...props }, ref) => (
57
+ <h5
58
+ ref={ref}
59
+ className={cn("mb-1 font-medium leading-none tracking-tight", className)}
60
+ {...props}
61
+ />
62
+ ),
63
+ );
64
+ AlertTitle.displayName = "AlertTitle";
65
+
66
+ export interface AlertDescriptionProps extends React.HTMLAttributes<HTMLParagraphElement> {
67
+ /** Body copy of the alert. Renders as a `<div>` so it can hold paragraphs and lists. */
68
+ children?: React.ReactNode;
69
+ /** Tailwind / CSS classes merged onto the description via `cn()`. */
70
+ className?: string;
71
+ }
72
+
73
+ const AlertDescription = React.forwardRef<HTMLParagraphElement, AlertDescriptionProps>(
74
+ ({ className, ...props }, ref) => (
75
+ <div ref={ref} className={cn("text-sm [&_p]:leading-relaxed", className)} {...props} />
76
+ ),
77
+ );
78
+ AlertDescription.displayName = "AlertDescription";
79
+
80
+ export { Alert, AlertDescription, AlertTitle };
@@ -0,0 +1,92 @@
1
+ import { cn } from "../../lib/utils";
2
+
3
+ export interface AnimatedBackgroundOrb {
4
+ /** CSS color for the radial gradient (hex / rgb / hsl / token). */
5
+ color: string;
6
+ /**
7
+ * Diameter in pixels.
8
+ * @default 500
9
+ */
10
+ size?: number;
11
+ /**
12
+ * Opacity 0–1. Lower values let more of the page bg bleed through.
13
+ * @default 0.2
14
+ */
15
+ opacity?: number;
16
+ /**
17
+ * Gaussian blur radius in pixels. Larger values produce softer orbs.
18
+ * @default 100
19
+ */
20
+ blur?: number;
21
+ /** Tailwind position classes, e.g. `"-top-32 -right-32"` or `"left-1/2 top-1/3"`. */
22
+ position?: string;
23
+ /**
24
+ * CSS `animation` shorthand. Pair with the `orb-float-1` / `orb-float-2`
25
+ * keyframes shipped in canvas's tokens, or your own.
26
+ */
27
+ animation?: string;
28
+ }
29
+
30
+ export interface AnimatedBackgroundProps {
31
+ /**
32
+ * Orbs to render. Defaults to a 3-orb indigo/purple/cyan composition
33
+ * that mirrors the auth-screen backdrop. Pass an empty array `[]` to
34
+ * render no orbs (useful for testing).
35
+ */
36
+ orbs?: AnimatedBackgroundOrb[];
37
+ /**
38
+ * Tailwind / CSS classes merged onto the root container via `cn()`.
39
+ * Defaults to `pointer-events-none fixed inset-0 overflow-hidden`.
40
+ */
41
+ className?: string;
42
+ }
43
+
44
+ const DEFAULT_ORBS: AnimatedBackgroundOrb[] = [
45
+ {
46
+ color: "#6366f1",
47
+ size: 500,
48
+ opacity: 0.2,
49
+ blur: 120,
50
+ position: "-top-32 -right-32",
51
+ animation: "orb-float-1 8s ease-in-out infinite",
52
+ },
53
+ {
54
+ color: "#8b5cf6",
55
+ size: 400,
56
+ opacity: 0.15,
57
+ blur: 100,
58
+ position: "-bottom-32 -left-32",
59
+ animation: "orb-float-2 10s ease-in-out infinite",
60
+ },
61
+ {
62
+ color: "#06b6d4",
63
+ size: 300,
64
+ opacity: 0.1,
65
+ blur: 80,
66
+ position: "left-1/2 top-1/3 -translate-x-1/2",
67
+ animation: "orb-float-1 12s ease-in-out 2s infinite",
68
+ },
69
+ ];
70
+
71
+ export function AnimatedBackground({ orbs = DEFAULT_ORBS, className }: AnimatedBackgroundProps) {
72
+ return (
73
+ <div className={cn("pointer-events-none fixed inset-0 overflow-hidden", className)}>
74
+ {orbs.map((orb, i) => (
75
+ <div
76
+ key={i}
77
+ className={cn("absolute rounded-full", orb.position)}
78
+ style={{
79
+ width: orb.size ?? 500,
80
+ height: orb.size ?? 500,
81
+ opacity: orb.opacity ?? 0.2,
82
+ filter: `blur(${orb.blur ?? 100}px)`,
83
+ background: `radial-gradient(circle, ${orb.color} 0%, transparent 70%)`,
84
+ animation: orb.animation,
85
+ }}
86
+ />
87
+ ))}
88
+ </div>
89
+ );
90
+ }
91
+
92
+ AnimatedBackground.displayName = "AnimatedBackground";
@@ -0,0 +1,48 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "../../lib/utils";
4
+
5
+ export interface BrandLockupProps extends React.HTMLAttributes<HTMLDivElement> {
6
+ /**
7
+ * Logo node rendered to the left of the wordmark — typically `<Logo />`
8
+ * for the canvas Olympus ring, or any other brand mark of the consumer's
9
+ * choosing. Required so canvas stays brand-agnostic.
10
+ */
11
+ logo: React.ReactNode;
12
+ /** Wordmark next to the logo (e.g. "Athena", "Hera"). */
13
+ productName: string;
14
+ /** Optional secondary line under the wordmark. */
15
+ subtitle?: string;
16
+ /** Visual size — sm = sidebar collapsed, md = sidebar expanded, lg = hero. */
17
+ size?: "sm" | "md" | "lg";
18
+ /** When true, renders only the logo without the wordmark or subtitle. */
19
+ collapsed?: boolean;
20
+ }
21
+
22
+ const SIZE: Record<NonNullable<BrandLockupProps["size"]>, { name: string; sub: string }> = {
23
+ sm: { name: "text-[13px]", sub: "text-[10px]" },
24
+ md: { name: "text-sm", sub: "text-[11px]" },
25
+ lg: { name: "text-2xl", sub: "text-xs" },
26
+ };
27
+
28
+ export const BrandLockup = React.forwardRef<HTMLDivElement, BrandLockupProps>(
29
+ ({ logo, productName, subtitle, size = "md", collapsed = false, className, ...props }, ref) => {
30
+ const sz = SIZE[size];
31
+ return (
32
+ <div ref={ref} className={cn("flex items-center gap-2.5", className)} {...props}>
33
+ <span className="shrink-0">{logo}</span>
34
+ {!collapsed && (
35
+ <div className="flex flex-col leading-tight">
36
+ <span className={cn(sz.name, "font-semibold tracking-tight text-foreground")}>
37
+ {productName}
38
+ </span>
39
+ {subtitle && (
40
+ <span className={cn(sz.sub, "font-mono text-muted-foreground")}>{subtitle}</span>
41
+ )}
42
+ </div>
43
+ )}
44
+ </div>
45
+ );
46
+ },
47
+ );
48
+ BrandLockup.displayName = "BrandLockup";
@@ -0,0 +1,161 @@
1
+ import { Slot } from "@radix-ui/react-slot";
2
+ import { ChevronRight, MoreHorizontal } from "lucide-react";
3
+ import * as React from "react";
4
+
5
+ import { cn } from "../../lib/utils";
6
+
7
+ export interface BreadcrumbProps extends React.ComponentPropsWithoutRef<"nav"> {
8
+ /**
9
+ * Custom separator element used between items. Defaults to a chevron
10
+ * via `<BreadcrumbSeparator>`.
11
+ */
12
+ separator?: React.ReactNode;
13
+ /** A `<BreadcrumbList>` containing items. */
14
+ children?: React.ReactNode;
15
+ /** Tailwind / CSS classes merged onto the `<nav>` via `cn()`. */
16
+ className?: string;
17
+ }
18
+
19
+ const Breadcrumb = React.forwardRef<HTMLElement, BreadcrumbProps>(({ ...props }, ref) => (
20
+ <nav ref={ref} aria-label="breadcrumb" {...props} />
21
+ ));
22
+ Breadcrumb.displayName = "Breadcrumb";
23
+
24
+ export interface BreadcrumbListProps extends React.ComponentPropsWithoutRef<"ol"> {
25
+ /** A flat list of `<BreadcrumbItem>`s, separated by `<BreadcrumbSeparator>`. */
26
+ children?: React.ReactNode;
27
+ /** Tailwind / CSS classes merged onto the `<ol>` via `cn()`. */
28
+ className?: string;
29
+ }
30
+
31
+ const BreadcrumbList = React.forwardRef<HTMLOListElement, BreadcrumbListProps>(
32
+ ({ className, ...props }, ref) => (
33
+ <ol
34
+ ref={ref}
35
+ className={cn(
36
+ "flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
37
+ className,
38
+ )}
39
+ {...props}
40
+ />
41
+ ),
42
+ );
43
+ BreadcrumbList.displayName = "BreadcrumbList";
44
+
45
+ export interface BreadcrumbItemProps extends React.ComponentPropsWithoutRef<"li"> {
46
+ /** Typically a `<BreadcrumbLink>` or `<BreadcrumbPage>`. */
47
+ children?: React.ReactNode;
48
+ /** Tailwind / CSS classes merged onto the `<li>` via `cn()`. */
49
+ className?: string;
50
+ }
51
+
52
+ const BreadcrumbItem = React.forwardRef<HTMLLIElement, BreadcrumbItemProps>(
53
+ ({ className, ...props }, ref) => (
54
+ <li ref={ref} className={cn("inline-flex items-center gap-1.5", className)} {...props} />
55
+ ),
56
+ );
57
+ BreadcrumbItem.displayName = "BreadcrumbItem";
58
+
59
+ export interface BreadcrumbLinkProps extends React.ComponentPropsWithoutRef<"a"> {
60
+ /**
61
+ * Render as a Radix Slot — useful for wrapping a router `<Link>` so
62
+ * it inherits breadcrumb styling.
63
+ * @default false
64
+ */
65
+ asChild?: boolean;
66
+ /** Anchor target. Used when not in `asChild` mode. */
67
+ href?: string;
68
+ /** Link label. */
69
+ children?: React.ReactNode;
70
+ /** Tailwind / CSS classes merged onto the link via `cn()`. */
71
+ className?: string;
72
+ }
73
+
74
+ const BreadcrumbLink = React.forwardRef<HTMLAnchorElement, BreadcrumbLinkProps>(
75
+ ({ asChild, className, ...props }, ref) => {
76
+ const Comp = asChild ? Slot : "a";
77
+
78
+ return (
79
+ <Comp
80
+ ref={ref}
81
+ className={cn("transition-colors hover:text-foreground", className)}
82
+ {...props}
83
+ />
84
+ );
85
+ },
86
+ );
87
+ BreadcrumbLink.displayName = "BreadcrumbLink";
88
+
89
+ export interface BreadcrumbPageProps extends React.ComponentPropsWithoutRef<"span"> {
90
+ /**
91
+ * Label of the current page. This is the last breadcrumb item and is
92
+ * non-clickable; sets `aria-current="page"` for assistive tech.
93
+ */
94
+ children?: React.ReactNode;
95
+ /** Tailwind / CSS classes merged onto the page span via `cn()`. */
96
+ className?: string;
97
+ }
98
+
99
+ const BreadcrumbPage = React.forwardRef<HTMLSpanElement, BreadcrumbPageProps>(
100
+ ({ className, ...props }, ref) => (
101
+ <span
102
+ ref={ref}
103
+ role="link"
104
+ aria-disabled="true"
105
+ aria-current="page"
106
+ className={cn("font-normal text-foreground", className)}
107
+ {...props}
108
+ />
109
+ ),
110
+ );
111
+ BreadcrumbPage.displayName = "BreadcrumbPage";
112
+
113
+ export interface BreadcrumbSeparatorProps extends React.ComponentProps<"li"> {
114
+ /**
115
+ * Override the default chevron with a custom node (e.g. a slash, an
116
+ * arrow icon).
117
+ */
118
+ children?: React.ReactNode;
119
+ /** Tailwind / CSS classes merged onto the separator via `cn()`. */
120
+ className?: string;
121
+ }
122
+
123
+ const BreadcrumbSeparator = ({ children, className, ...props }: BreadcrumbSeparatorProps) => (
124
+ <li
125
+ role="presentation"
126
+ aria-hidden="true"
127
+ className={cn("[&>svg]:w-3.5 [&>svg]:h-3.5", className)}
128
+ {...props}
129
+ >
130
+ {children ?? <ChevronRight />}
131
+ </li>
132
+ );
133
+ BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
134
+
135
+ export interface BreadcrumbEllipsisProps extends React.ComponentProps<"span"> {
136
+ /** Tailwind / CSS classes merged onto the ellipsis via `cn()`. */
137
+ className?: string;
138
+ }
139
+
140
+ const BreadcrumbEllipsis = ({ className, ...props }: BreadcrumbEllipsisProps) => (
141
+ <span
142
+ role="presentation"
143
+ aria-hidden="true"
144
+ className={cn("flex h-9 w-9 items-center justify-center", className)}
145
+ {...props}
146
+ >
147
+ <MoreHorizontal className="h-4 w-4" />
148
+ <span className="sr-only">More</span>
149
+ </span>
150
+ );
151
+ BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
152
+
153
+ export {
154
+ Breadcrumb,
155
+ BreadcrumbEllipsis,
156
+ BreadcrumbItem,
157
+ BreadcrumbLink,
158
+ BreadcrumbList,
159
+ BreadcrumbPage,
160
+ BreadcrumbSeparator,
161
+ };
@@ -0,0 +1,104 @@
1
+ import { Slot } from "@radix-ui/react-slot";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+ import type * as React from "react";
4
+
5
+ import { cn } from "../../lib/utils";
6
+ import { Separator } from "../atoms/separator";
7
+
8
+ const buttonGroupVariants = cva(
9
+ "flex w-fit items-stretch has-[>[data-slot=button-group]]:gap-2 [&>*]:focus-visible:relative [&>*]:focus-visible:z-10 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
10
+ {
11
+ variants: {
12
+ orientation: {
13
+ horizontal:
14
+ "[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none",
15
+ vertical:
16
+ "flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none",
17
+ },
18
+ },
19
+ defaultVariants: {
20
+ orientation: "horizontal",
21
+ },
22
+ },
23
+ );
24
+
25
+ export interface ButtonGroupProps
26
+ extends React.ComponentProps<"div">,
27
+ VariantProps<typeof buttonGroupVariants> {
28
+ /**
29
+ * `horizontal` (default) lays buttons left-to-right with shared borders;
30
+ * `vertical` stacks them with shared horizontal borders.
31
+ * @default "horizontal"
32
+ */
33
+ orientation?: "horizontal" | "vertical";
34
+ /** A row/column of `<Button>`s, `<ButtonGroupText>`s, or separators. */
35
+ children?: React.ReactNode;
36
+ className?: string;
37
+ }
38
+
39
+ function ButtonGroup({ className, orientation, ...props }: ButtonGroupProps) {
40
+ return (
41
+ <div
42
+ role="group"
43
+ data-slot="button-group"
44
+ data-orientation={orientation}
45
+ className={cn(buttonGroupVariants({ orientation }), className)}
46
+ {...props}
47
+ />
48
+ );
49
+ }
50
+
51
+ export interface ButtonGroupTextProps extends React.ComponentProps<"div"> {
52
+ /**
53
+ * Render as a Radix Slot — useful for wrapping a label or icon as the
54
+ * group's text element.
55
+ * @default false
56
+ */
57
+ asChild?: boolean;
58
+ /** Text or icon content. */
59
+ children?: React.ReactNode;
60
+ className?: string;
61
+ }
62
+
63
+ function ButtonGroupText({ className, asChild = false, ...props }: ButtonGroupTextProps) {
64
+ const Comp = asChild ? Slot : "div";
65
+
66
+ return (
67
+ <Comp
68
+ className={cn(
69
+ "bg-muted shadow-xs flex items-center gap-2 rounded-md border px-4 text-sm font-medium [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none",
70
+ className,
71
+ )}
72
+ {...props}
73
+ />
74
+ );
75
+ }
76
+
77
+ export interface ButtonGroupSeparatorProps extends React.ComponentProps<typeof Separator> {
78
+ /**
79
+ * Layout direction.
80
+ * @default "vertical"
81
+ */
82
+ orientation?: "horizontal" | "vertical";
83
+ className?: string;
84
+ }
85
+
86
+ function ButtonGroupSeparator({
87
+ className,
88
+ orientation = "vertical",
89
+ ...props
90
+ }: ButtonGroupSeparatorProps) {
91
+ return (
92
+ <Separator
93
+ data-slot="button-group-separator"
94
+ orientation={orientation}
95
+ className={cn(
96
+ "bg-input relative !m-0 self-stretch data-[orientation=vertical]:h-auto",
97
+ className,
98
+ )}
99
+ {...props}
100
+ />
101
+ );
102
+ }
103
+
104
+ export { ButtonGroup, ButtonGroupSeparator, ButtonGroupText, buttonGroupVariants };