@stampui/blocks 1.0.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 (107) hide show
  1. package/dist/components/ai-chat-shell.d.ts +1 -0
  2. package/dist/components/ai-chat-shell.js +23 -0
  3. package/dist/components/prompt-input.d.ts +5 -0
  4. package/dist/components/prompt-input.js +47 -0
  5. package/dist/components/registry-card.d.ts +6 -0
  6. package/dist/components/registry-card.js +15 -0
  7. package/dist/components/registry-explorer.d.ts +8 -0
  8. package/dist/components/registry-explorer.js +38 -0
  9. package/dist/components/token-stream.d.ts +7 -0
  10. package/dist/components/token-stream.js +21 -0
  11. package/dist/index.d.ts +2 -0
  12. package/dist/index.js +23 -0
  13. package/dist/manifests.d.ts +3 -0
  14. package/dist/manifests.js +1666 -0
  15. package/dist/types.d.ts +44 -0
  16. package/dist/types.js +2 -0
  17. package/package.json +28 -0
  18. package/src/components/blocks/ai-chat-shell.tsx +97 -0
  19. package/src/components/blocks/auth-panel.tsx +203 -0
  20. package/src/components/blocks/feature-grid.tsx +122 -0
  21. package/src/components/blocks/hero-section.tsx +73 -0
  22. package/src/components/blocks/notification-center.tsx +185 -0
  23. package/src/components/blocks/onboarding-flow.tsx +230 -0
  24. package/src/components/blocks/pricing-section.tsx +135 -0
  25. package/src/components/blocks/project-command-center.tsx +188 -0
  26. package/src/components/blocks/prompt-input.tsx +81 -0
  27. package/src/components/blocks/registry-card.tsx +104 -0
  28. package/src/components/blocks/registry-explorer.tsx +78 -0
  29. package/src/components/blocks/settings-layout.tsx +178 -0
  30. package/src/components/blocks/stats-strip.tsx +100 -0
  31. package/src/components/blocks/token-stream.tsx +42 -0
  32. package/src/components/blocks/usage-card.tsx +116 -0
  33. package/src/components/core/accordion.tsx +58 -0
  34. package/src/components/core/alert-dialog.tsx +113 -0
  35. package/src/components/core/alert.tsx +48 -0
  36. package/src/components/core/animated-number.tsx +77 -0
  37. package/src/components/core/aspect-ratio.tsx +20 -0
  38. package/src/components/core/avatar-stack.tsx +61 -0
  39. package/src/components/core/avatar.tsx +90 -0
  40. package/src/components/core/badge.tsx +39 -0
  41. package/src/components/core/breadcrumb.tsx +63 -0
  42. package/src/components/core/button-group.tsx +37 -0
  43. package/src/components/core/button.tsx +110 -0
  44. package/src/components/core/calendar.tsx +143 -0
  45. package/src/components/core/card.tsx +60 -0
  46. package/src/components/core/carousel.tsx +170 -0
  47. package/src/components/core/chart.tsx +377 -0
  48. package/src/components/core/checkbox.tsx +64 -0
  49. package/src/components/core/collapsible.tsx +30 -0
  50. package/src/components/core/combobox.tsx +114 -0
  51. package/src/components/core/command-box.tsx +22 -0
  52. package/src/components/core/command.tsx +165 -0
  53. package/src/components/core/confirm-action.tsx +94 -0
  54. package/src/components/core/context-menu.tsx +139 -0
  55. package/src/components/core/copy-button.tsx +41 -0
  56. package/src/components/core/data-table.tsx +173 -0
  57. package/src/components/core/date-picker.tsx +73 -0
  58. package/src/components/core/dialog.tsx +83 -0
  59. package/src/components/core/drawer.tsx +87 -0
  60. package/src/components/core/dropdown-menu.tsx +147 -0
  61. package/src/components/core/empty.tsx +34 -0
  62. package/src/components/core/field.tsx +39 -0
  63. package/src/components/core/file-upload.tsx +143 -0
  64. package/src/components/core/hover-card.tsx +31 -0
  65. package/src/components/core/inline-edit.tsx +104 -0
  66. package/src/components/core/input-group.tsx +47 -0
  67. package/src/components/core/input-otp.tsx +108 -0
  68. package/src/components/core/input.tsx +37 -0
  69. package/src/components/core/kbd.tsx +47 -0
  70. package/src/components/core/label.tsx +28 -0
  71. package/src/components/core/marquee.tsx +61 -0
  72. package/src/components/core/menubar.tsx +120 -0
  73. package/src/components/core/multi-select.tsx +145 -0
  74. package/src/components/core/native-select.tsx +27 -0
  75. package/src/components/core/navigation-menu.tsx +130 -0
  76. package/src/components/core/number-stepper.tsx +80 -0
  77. package/src/components/core/pagination.tsx +80 -0
  78. package/src/components/core/password-input.tsx +90 -0
  79. package/src/components/core/popover.tsx +34 -0
  80. package/src/components/core/progress.tsx +63 -0
  81. package/src/components/core/radio-group.tsx +77 -0
  82. package/src/components/core/resizable.tsx +250 -0
  83. package/src/components/core/scroll-area.tsx +38 -0
  84. package/src/components/core/select.tsx +128 -0
  85. package/src/components/core/separator.tsx +47 -0
  86. package/src/components/core/sheet.tsx +118 -0
  87. package/src/components/core/sidebar.tsx +129 -0
  88. package/src/components/core/skeleton.tsx +32 -0
  89. package/src/components/core/slider.tsx +97 -0
  90. package/src/components/core/sonner.tsx +29 -0
  91. package/src/components/core/spinner.tsx +60 -0
  92. package/src/components/core/status-pulse.tsx +67 -0
  93. package/src/components/core/stepper.tsx +111 -0
  94. package/src/components/core/switch.tsx +72 -0
  95. package/src/components/core/table.tsx +104 -0
  96. package/src/components/core/tabs.tsx +55 -0
  97. package/src/components/core/tag-input.tsx +93 -0
  98. package/src/components/core/textarea.tsx +44 -0
  99. package/src/components/core/timeline.tsx +81 -0
  100. package/src/components/core/toggle-group.tsx +56 -0
  101. package/src/components/core/toggle.tsx +66 -0
  102. package/src/components/core/tooltip.tsx +31 -0
  103. package/src/components/core/typing-indicator.tsx +51 -0
  104. package/src/index.ts +8 -0
  105. package/src/manifests.ts +1682 -0
  106. package/src/types.ts +58 -0
  107. package/src/ui.ts +13 -0
@@ -0,0 +1,118 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as RadixDialog from "@radix-ui/react-dialog"
5
+ import { X } from "lucide-react"
6
+ import { cva, type VariantProps } from "class-variance-authority"
7
+ import { cx } from "@/lib/cx"
8
+
9
+ export const Sheet = RadixDialog.Root
10
+ export const SheetTrigger = RadixDialog.Trigger
11
+ export const SheetClose = RadixDialog.Close
12
+ export const SheetPortal = RadixDialog.Portal
13
+
14
+ export const SheetOverlay = React.forwardRef<
15
+ React.ElementRef<typeof RadixDialog.Overlay>,
16
+ React.ComponentPropsWithoutRef<typeof RadixDialog.Overlay>
17
+ >(({ className, ...props }, ref) => (
18
+ <RadixDialog.Overlay
19
+ ref={ref}
20
+ className={cx(
21
+ "fixed inset-0 z-50 bg-black/40 backdrop-blur-sm",
22
+ "data-[state=open]:animate-in data-[state=open]:fade-in-0",
23
+ "data-[state=closed]:animate-out data-[state=closed]:fade-out-0",
24
+ className
25
+ )}
26
+ {...props}
27
+ />
28
+ ))
29
+ SheetOverlay.displayName = "SheetOverlay"
30
+
31
+ const sheetContentStyles = cva(
32
+ [
33
+ "fixed z-50 bg-card border-border shadow-xl outline-none",
34
+ "data-[state=open]:animate-in data-[state=closed]:animate-out",
35
+ "data-[state=closed]:duration-300 data-[state=open]:duration-300",
36
+ ],
37
+ {
38
+ variants: {
39
+ side: {
40
+ right: [
41
+ "inset-y-0 right-0 h-full w-3/4 max-w-sm border-l",
42
+ "data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right",
43
+ ],
44
+ left: [
45
+ "inset-y-0 left-0 h-full w-3/4 max-w-sm border-r",
46
+ "data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left",
47
+ ],
48
+ top: [
49
+ "inset-x-0 top-0 w-full border-b",
50
+ "data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
51
+ ],
52
+ bottom: [
53
+ "inset-x-0 bottom-0 w-full rounded-t-2xl border-t",
54
+ "data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
55
+ ],
56
+ },
57
+ },
58
+ defaultVariants: {
59
+ side: "right",
60
+ },
61
+ }
62
+ )
63
+
64
+ export interface SheetContentProps
65
+ extends React.ComponentPropsWithoutRef<typeof RadixDialog.Content>,
66
+ VariantProps<typeof sheetContentStyles> {}
67
+
68
+ export const SheetContent = React.forwardRef<
69
+ React.ElementRef<typeof RadixDialog.Content>,
70
+ SheetContentProps
71
+ >(({ className, side, children, ...props }, ref) => (
72
+ <RadixDialog.Portal>
73
+ <SheetOverlay />
74
+ <RadixDialog.Content
75
+ ref={ref}
76
+ className={cx(sheetContentStyles({ side }), className)}
77
+ {...props}
78
+ >
79
+ {children}
80
+ <RadixDialog.Close className="absolute right-4 top-4 rounded-md p-1 text-muted-foreground transition-colors hover:bg-surface-2 hover:text-foreground outline-none focus-visible:ring-1 focus-visible:ring-border-strong focus-visible:ring-offset-1 focus-visible:ring-offset-background">
81
+ <X className="h-4 w-4" />
82
+ <span className="sr-only">Close</span>
83
+ </RadixDialog.Close>
84
+ </RadixDialog.Content>
85
+ </RadixDialog.Portal>
86
+ ))
87
+ SheetContent.displayName = "SheetContent"
88
+
89
+ export const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
90
+ <div className={cx("flex flex-col gap-1.5 p-6 pb-0", className)} {...props} />
91
+ )
92
+ SheetHeader.displayName = "SheetHeader"
93
+
94
+ export const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
95
+ <div className={cx("flex items-center justify-end gap-2 p-6 pt-0 mt-auto", className)} {...props} />
96
+ )
97
+ SheetFooter.displayName = "SheetFooter"
98
+
99
+ export const SheetTitle = React.forwardRef<
100
+ React.ElementRef<typeof RadixDialog.Title>,
101
+ React.ComponentPropsWithoutRef<typeof RadixDialog.Title>
102
+ >(({ className, ...props }, ref) => (
103
+ <RadixDialog.Title ref={ref} className={cx("text-base font-semibold leading-tight text-foreground", className)} {...props} />
104
+ ))
105
+ SheetTitle.displayName = "SheetTitle"
106
+
107
+ export const SheetDescription = React.forwardRef<
108
+ React.ElementRef<typeof RadixDialog.Description>,
109
+ React.ComponentPropsWithoutRef<typeof RadixDialog.Description>
110
+ >(({ className, ...props }, ref) => (
111
+ <RadixDialog.Description ref={ref} className={cx("text-sm text-muted-foreground leading-relaxed", className)} {...props} />
112
+ ))
113
+ SheetDescription.displayName = "SheetDescription"
114
+
115
+ export const SheetBody = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
116
+ <div className={cx("flex-1 overflow-y-auto px-6 py-4", className)} {...props} />
117
+ )
118
+ SheetBody.displayName = "SheetBody"
@@ -0,0 +1,129 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { ChevronRight } from "lucide-react"
5
+ import { cx } from "@/lib/cx"
6
+
7
+ // ── Root ──────────────────────────────────────────────────────────────────
8
+
9
+ export interface SidebarProps extends React.HTMLAttributes<HTMLElement> {
10
+ collapsed?: boolean
11
+ }
12
+
13
+ export function Sidebar({ className, collapsed = false, children, ...props }: SidebarProps) {
14
+ return (
15
+ <aside
16
+ data-collapsed={collapsed || undefined}
17
+ className={cx(
18
+ "flex flex-col gap-1 py-4 transition-all duration-200",
19
+ collapsed ? "w-14" : "w-60",
20
+ className
21
+ )}
22
+ {...props}
23
+ >
24
+ {children}
25
+ </aside>
26
+ )
27
+ }
28
+
29
+ // ── Section ───────────────────────────────────────────────────────────────
30
+
31
+ export interface SidebarSectionProps extends React.HTMLAttributes<HTMLDivElement> {
32
+ title?: string
33
+ collapsed?: boolean
34
+ }
35
+
36
+ export function SidebarSection({ title, collapsed, className, children, ...props }: SidebarSectionProps) {
37
+ return (
38
+ <div className={cx("flex flex-col gap-0.5", className)} {...props}>
39
+ {title && !collapsed && (
40
+ <p className="px-3 py-1 text-[11px] font-semibold text-muted-foreground uppercase tracking-widest">
41
+ {title}
42
+ </p>
43
+ )}
44
+ {children}
45
+ </div>
46
+ )
47
+ }
48
+
49
+ // ── Item ──────────────────────────────────────────────────────────────────
50
+
51
+ export interface SidebarItemProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
52
+ icon?: React.ReactNode
53
+ label: string
54
+ active?: boolean
55
+ badge?: React.ReactNode
56
+ collapsed?: boolean
57
+ href?: string
58
+ }
59
+
60
+ export function SidebarItem({ icon, label, active, badge, collapsed, href, className, ...props }: SidebarItemProps) {
61
+ const Tag = href ? "a" : "button"
62
+ return (
63
+ <Tag
64
+ {...(href ? { href } : { type: "button" })}
65
+ aria-current={active ? "page" : undefined}
66
+ title={collapsed ? label : undefined}
67
+ className={cx(
68
+ "flex items-center gap-2.5 rounded-lg px-3 py-2 text-sm font-medium transition-colors w-full",
69
+ active
70
+ ? "bg-surface-2 text-foreground"
71
+ : "text-muted-foreground hover:bg-surface-2/60 hover:text-foreground",
72
+ collapsed && "justify-center px-2",
73
+ className
74
+ )}
75
+ {...(props as any)}
76
+ >
77
+ {icon && <span className="shrink-0 text-[1em]">{icon}</span>}
78
+ {!collapsed && <span className="flex-1 truncate">{label}</span>}
79
+ {!collapsed && badge && <span className="shrink-0">{badge}</span>}
80
+ </Tag>
81
+ )
82
+ }
83
+
84
+ // ── Collapsible group ─────────────────────────────────────────────────────
85
+
86
+ export interface SidebarGroupProps {
87
+ icon?: React.ReactNode
88
+ label: string
89
+ defaultOpen?: boolean
90
+ collapsed?: boolean
91
+ className?: string
92
+ children: React.ReactNode
93
+ }
94
+
95
+ export function SidebarGroup({ icon, label, defaultOpen = false, collapsed, className, children }: SidebarGroupProps) {
96
+ const [open, setOpen] = React.useState(defaultOpen)
97
+
98
+ return (
99
+ <div className={cx("flex flex-col gap-0.5", className)}>
100
+ <button
101
+ type="button"
102
+ onClick={() => setOpen((v) => !v)}
103
+ className={cx(
104
+ "flex items-center gap-2.5 rounded-lg px-3 py-2 text-sm font-medium text-muted-foreground hover:bg-surface-2/60 hover:text-foreground transition-colors w-full",
105
+ collapsed && "justify-center px-2"
106
+ )}
107
+ >
108
+ {icon && <span className="shrink-0 text-[1em]">{icon}</span>}
109
+ {!collapsed && (
110
+ <>
111
+ <span className="flex-1 truncate text-left">{label}</span>
112
+ <ChevronRight className={cx("h-3.5 w-3.5 shrink-0 transition-transform duration-200", open && "rotate-90")} />
113
+ </>
114
+ )}
115
+ </button>
116
+ {open && !collapsed && (
117
+ <div className="ml-3 border-l border-border pl-3 flex flex-col gap-0.5">
118
+ {children}
119
+ </div>
120
+ )}
121
+ </div>
122
+ )
123
+ }
124
+
125
+ // ── Separator ─────────────────────────────────────────────────────────────
126
+
127
+ export function SidebarSeparator({ className }: { className?: string }) {
128
+ return <div className={cx("mx-3 my-2 h-px bg-border", className)} />
129
+ }
@@ -0,0 +1,32 @@
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+ import { cx } from "@/lib/cx"
4
+
5
+ const skeletonStyles = cva(
6
+ "animate-pulse bg-surface-3",
7
+ {
8
+ variants: {
9
+ variant: {
10
+ default: "rounded-lg",
11
+ circle: "rounded-full",
12
+ text: "rounded",
13
+ pill: "rounded-full",
14
+ },
15
+ },
16
+ defaultVariants: { variant: "default" },
17
+ }
18
+ )
19
+
20
+ export interface SkeletonProps
21
+ extends React.HTMLAttributes<HTMLDivElement>,
22
+ VariantProps<typeof skeletonStyles> {}
23
+
24
+ export function Skeleton({ className, variant, ...props }: SkeletonProps) {
25
+ return (
26
+ <div
27
+ className={cx(skeletonStyles({ variant }), className)}
28
+ aria-hidden="true"
29
+ {...props}
30
+ />
31
+ )
32
+ }
@@ -0,0 +1,97 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as SliderPrimitive from "@radix-ui/react-slider"
5
+ import { cva, type VariantProps } from "class-variance-authority"
6
+ import { cx } from "@/lib/cx"
7
+
8
+ const sliderStyles = cva(
9
+ "relative flex touch-none select-none items-center",
10
+ {
11
+ variants: {
12
+ orientation: {
13
+ horizontal: "w-full",
14
+ vertical: "flex-col h-full min-h-[160px] w-auto",
15
+ },
16
+ },
17
+ defaultVariants: { orientation: "horizontal" },
18
+ }
19
+ )
20
+
21
+ const trackStyles = cva(
22
+ "relative grow overflow-hidden rounded-full bg-surface-3",
23
+ {
24
+ variants: {
25
+ orientation: {
26
+ horizontal: "h-1.5 w-full",
27
+ vertical: "h-full w-1.5",
28
+ },
29
+ size: {
30
+ sm: "",
31
+ md: "",
32
+ lg: "",
33
+ },
34
+ },
35
+ compoundVariants: [
36
+ { orientation: "horizontal", size: "sm", className: "h-1" },
37
+ { orientation: "horizontal", size: "lg", className: "h-2" },
38
+ { orientation: "vertical", size: "sm", className: "w-1" },
39
+ { orientation: "vertical", size: "lg", className: "w-2" },
40
+ ],
41
+ defaultVariants: { orientation: "horizontal", size: "md" },
42
+ }
43
+ )
44
+
45
+ const thumbStyles = cva(
46
+ [
47
+ "block rounded-full bg-foreground ring-offset-background",
48
+ "transition-all duration-100",
49
+ "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border-strong focus-visible:ring-offset-1 focus-visible:ring-offset-background",
50
+ "disabled:pointer-events-none disabled:opacity-50",
51
+ "hover:scale-110",
52
+ ],
53
+ {
54
+ variants: {
55
+ size: {
56
+ sm: "h-3.5 w-3.5",
57
+ md: "h-4 w-4",
58
+ lg: "h-5 w-5",
59
+ },
60
+ },
61
+ defaultVariants: { size: "md" },
62
+ }
63
+ )
64
+
65
+ export interface SliderProps
66
+ extends Omit<React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>, "orientation">,
67
+ VariantProps<typeof sliderStyles>,
68
+ VariantProps<typeof thumbStyles> {
69
+ orientation?: "horizontal" | "vertical"
70
+ }
71
+
72
+ const Slider = React.forwardRef<
73
+ React.ElementRef<typeof SliderPrimitive.Root>,
74
+ SliderProps
75
+ >(({ className, orientation = "horizontal", size, ...props }, ref) => (
76
+ <SliderPrimitive.Root
77
+ ref={ref}
78
+ orientation={orientation}
79
+ className={cx(sliderStyles({ orientation }), className)}
80
+ {...props}
81
+ >
82
+ <SliderPrimitive.Track className={trackStyles({ orientation, size })}>
83
+ <SliderPrimitive.Range
84
+ className={cx(
85
+ "absolute bg-foreground",
86
+ orientation === "horizontal" ? "h-full" : "w-full",
87
+ )}
88
+ />
89
+ </SliderPrimitive.Track>
90
+ {(props.defaultValue ?? props.value ?? [0]).map((_, i) => (
91
+ <SliderPrimitive.Thumb key={i} className={thumbStyles({ size })} />
92
+ ))}
93
+ </SliderPrimitive.Root>
94
+ ))
95
+ Slider.displayName = "Slider"
96
+
97
+ export { Slider }
@@ -0,0 +1,29 @@
1
+ "use client"
2
+
3
+ import { Toaster as SonnerToaster } from "sonner"
4
+ import { useTheme } from "next-themes"
5
+
6
+ export { toast } from "sonner"
7
+
8
+ export function Toaster() {
9
+ const { resolvedTheme } = useTheme()
10
+ return (
11
+ <SonnerToaster
12
+ theme={resolvedTheme as "light" | "dark" | undefined}
13
+ position="bottom-right"
14
+ toastOptions={{
15
+ classNames: {
16
+ toast:
17
+ "group border border-border bg-card text-foreground shadow-lg rounded-xl text-sm font-medium gap-3 px-4 py-3",
18
+ description: "text-muted-foreground text-xs",
19
+ actionButton: "bg-foreground text-background text-xs font-medium rounded-md px-2.5 py-1 hover:bg-foreground/90",
20
+ cancelButton: "bg-surface-2 text-muted-foreground text-xs font-medium rounded-md px-2.5 py-1 hover:bg-surface-3",
21
+ error: "border-danger/30",
22
+ success: "border-border",
23
+ warning: "border-border",
24
+ info: "border-border",
25
+ },
26
+ }}
27
+ />
28
+ )
29
+ }
@@ -0,0 +1,60 @@
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+ import { cx } from "@/lib/cx"
4
+
5
+ const spinnerStyles = cva(
6
+ "animate-spin text-muted-foreground",
7
+ {
8
+ variants: {
9
+ size: {
10
+ xs: "h-3 w-3",
11
+ sm: "h-4 w-4",
12
+ md: "h-5 w-5",
13
+ lg: "h-6 w-6",
14
+ xl: "h-8 w-8",
15
+ },
16
+ variant: {
17
+ default: "text-muted-foreground",
18
+ primary: "text-foreground",
19
+ white: "text-white",
20
+ },
21
+ },
22
+ defaultVariants: { size: "md", variant: "default" },
23
+ }
24
+ )
25
+
26
+ export interface SpinnerProps
27
+ extends Omit<React.SVGAttributes<SVGElement>, "color">,
28
+ VariantProps<typeof spinnerStyles> {
29
+ label?: string
30
+ }
31
+
32
+ export function Spinner({ className, size, variant, label = "Loading...", ...props }: SpinnerProps) {
33
+ return (
34
+ <svg
35
+ role="status"
36
+ aria-label={label}
37
+ viewBox="0 0 24 24"
38
+ fill="none"
39
+ xmlns="http://www.w3.org/2000/svg"
40
+ className={cx(spinnerStyles({ size, variant }), className)}
41
+ {...props}
42
+ >
43
+ <circle
44
+ cx="12" cy="12" r="10"
45
+ stroke="currentColor"
46
+ strokeWidth="3"
47
+ strokeLinecap="round"
48
+ strokeDasharray="40 60"
49
+ opacity="0.25"
50
+ />
51
+ <circle
52
+ cx="12" cy="12" r="10"
53
+ stroke="currentColor"
54
+ strokeWidth="3"
55
+ strokeLinecap="round"
56
+ strokeDasharray="40 60"
57
+ />
58
+ </svg>
59
+ )
60
+ }
@@ -0,0 +1,67 @@
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+ import { cx } from "@/lib/cx"
4
+
5
+ const dotStyles = cva("relative inline-flex rounded-full shrink-0", {
6
+ variants: {
7
+ status: {
8
+ online: "bg-green-500",
9
+ offline: "bg-muted-foreground/40",
10
+ processing: "bg-yellow-400",
11
+ error: "bg-red-500",
12
+ warning: "bg-orange-400",
13
+ },
14
+ size: {
15
+ sm: "h-1.5 w-1.5",
16
+ md: "h-2 w-2",
17
+ lg: "h-2.5 w-2.5",
18
+ },
19
+ },
20
+ defaultVariants: { status: "online", size: "md" },
21
+ })
22
+
23
+ const pingStyles = cva("absolute inline-flex h-full w-full rounded-full opacity-75 animate-ping", {
24
+ variants: {
25
+ status: {
26
+ online: "bg-green-500",
27
+ offline: "hidden",
28
+ processing: "bg-yellow-400",
29
+ error: "bg-red-500",
30
+ warning: "bg-orange-400",
31
+ },
32
+ },
33
+ defaultVariants: { status: "online" },
34
+ })
35
+
36
+ const labels: Record<string, string> = {
37
+ online: "Online",
38
+ offline: "Offline",
39
+ processing: "Processing",
40
+ error: "Error",
41
+ warning: "Warning",
42
+ }
43
+
44
+ export interface StatusPulseProps
45
+ extends React.HTMLAttributes<HTMLDivElement>,
46
+ VariantProps<typeof dotStyles> {
47
+ label?: string | boolean
48
+ pulse?: boolean
49
+ }
50
+
51
+ export function StatusPulse({ status, size, label, pulse = true, className, ...props }: StatusPulseProps) {
52
+ const showLabel = label === true ? labels[status ?? "online"] : label
53
+
54
+ return (
55
+ <div className={cx("inline-flex items-center gap-1.5", className)} {...props}>
56
+ <span className="relative inline-flex items-center justify-center">
57
+ {pulse && status !== "offline" && (
58
+ <span className={pingStyles({ status })} />
59
+ )}
60
+ <span className={dotStyles({ status, size })} />
61
+ </span>
62
+ {showLabel && (
63
+ <span className="text-xs text-muted-foreground">{showLabel}</span>
64
+ )}
65
+ </div>
66
+ )
67
+ }
@@ -0,0 +1,111 @@
1
+ import * as React from "react"
2
+ import { Check, X } from "lucide-react"
3
+ import { cx } from "@/lib/cx"
4
+
5
+ export type StepStatus = "pending" | "active" | "completed" | "error"
6
+ export type StepperOrientation = "horizontal" | "vertical"
7
+
8
+ export interface Step {
9
+ title: string
10
+ description?: string
11
+ icon?: React.ReactNode
12
+ }
13
+
14
+ export interface StepperProps {
15
+ steps: Step[]
16
+ currentStep: number
17
+ orientation?: StepperOrientation
18
+ className?: string
19
+ }
20
+
21
+ function StepIcon({ status, index, icon }: { status: StepStatus; index: number; icon?: React.ReactNode }) {
22
+ if (status === "completed") return <Check className="h-3.5 w-3.5" />
23
+ if (status === "error") return <X className="h-3.5 w-3.5" />
24
+ if (icon && status === "active") return <span className="text-xs">{icon}</span>
25
+ return <span className="text-xs font-medium">{index + 1}</span>
26
+ }
27
+
28
+ export function Stepper({ steps, currentStep, orientation = "horizontal", className }: StepperProps) {
29
+ const isHorizontal = orientation === "horizontal"
30
+
31
+ function getStatus(index: number): StepStatus {
32
+ if (index < currentStep) return "completed"
33
+ if (index === currentStep) return "active"
34
+ return "pending"
35
+ }
36
+
37
+ if (isHorizontal) {
38
+ return (
39
+ <div className={cx("flex items-start w-full", className)}>
40
+ {steps.map((step, i) => {
41
+ const status = getStatus(i)
42
+ const isLast = i === steps.length - 1
43
+ return (
44
+ <React.Fragment key={i}>
45
+ <div className="flex flex-col items-center flex-1 min-w-0">
46
+ <div className={cx(
47
+ "flex h-8 w-8 shrink-0 items-center justify-center rounded-full border-2 transition-colors",
48
+ status === "completed" && "border-foreground bg-foreground text-background",
49
+ status === "active" && "border-foreground bg-transparent text-foreground",
50
+ status === "pending" && "border-border bg-transparent text-muted-foreground",
51
+ status === "error" && "border-danger bg-danger/10 text-danger",
52
+ )}>
53
+ <StepIcon status={status} index={i} icon={step.icon} />
54
+ </div>
55
+ <div className="mt-2 text-center px-1">
56
+ <p className={cx("text-xs font-medium leading-tight", status === "pending" ? "text-muted-foreground" : "text-foreground")}>
57
+ {step.title}
58
+ </p>
59
+ {step.description && (
60
+ <p className="text-xs text-muted-foreground mt-0.5 leading-snug">{step.description}</p>
61
+ )}
62
+ </div>
63
+ </div>
64
+ {!isLast && (
65
+ <div className={cx(
66
+ "h-0.5 flex-1 mt-4 mx-2 rounded-full transition-colors",
67
+ i < currentStep ? "bg-foreground" : "bg-border"
68
+ )} />
69
+ )}
70
+ </React.Fragment>
71
+ )
72
+ })}
73
+ </div>
74
+ )
75
+ }
76
+
77
+ return (
78
+ <div className={cx("flex flex-col", className)}>
79
+ {steps.map((step, i) => {
80
+ const status = getStatus(i)
81
+ const isLast = i === steps.length - 1
82
+ return (
83
+ <div key={i} className="flex gap-4">
84
+ <div className="flex flex-col items-center">
85
+ <div className={cx(
86
+ "flex h-8 w-8 shrink-0 items-center justify-center rounded-full border-2 transition-colors",
87
+ status === "completed" && "border-foreground bg-foreground text-background",
88
+ status === "active" && "border-foreground bg-transparent text-foreground",
89
+ status === "pending" && "border-border bg-transparent text-muted-foreground",
90
+ status === "error" && "border-danger bg-danger/10 text-danger",
91
+ )}>
92
+ <StepIcon status={status} index={i} icon={step.icon} />
93
+ </div>
94
+ {!isLast && (
95
+ <div className={cx("w-0.5 flex-1 my-1 rounded-full transition-colors min-h-[24px]", i < currentStep ? "bg-foreground" : "bg-border")} />
96
+ )}
97
+ </div>
98
+ <div className={cx("pb-6 min-w-0", isLast && "pb-0")}>
99
+ <p className={cx("text-sm font-medium leading-tight mt-1.5", status === "pending" ? "text-muted-foreground" : "text-foreground")}>
100
+ {step.title}
101
+ </p>
102
+ {step.description && (
103
+ <p className="text-xs text-muted-foreground mt-0.5 leading-relaxed">{step.description}</p>
104
+ )}
105
+ </div>
106
+ </div>
107
+ )
108
+ })}
109
+ </div>
110
+ )
111
+ }