@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,100 @@
1
+ import * as React from "react"
2
+ import { Separator } from "@/components/core/separator"
3
+ import { cx } from "@/lib/cx"
4
+
5
+ export interface Stat {
6
+ value: number
7
+ suffix?: string
8
+ prefix?: string
9
+ label: string
10
+ description?: string
11
+ }
12
+
13
+ export interface StatsStripProps {
14
+ stats?: Stat[]
15
+ layout?: "strip" | "grid"
16
+ className?: string
17
+ }
18
+
19
+ const defaultStats: Stat[] = [
20
+ { value: 65, suffix: "+", label: "Components", description: "Accessible UI primitives" },
21
+ { value: 8, label: "Page Blocks", description: "Production-ready layouts" },
22
+ { value: 10, label: "Minute Setup", description: "From zero to shipped" },
23
+ { value: 0, label: "Runtime Deps", description: "Code you fully own" },
24
+ ]
25
+
26
+ function AnimatedValue({ value, prefix, suffix }: { value: number; prefix?: string; suffix?: string }) {
27
+ const [displayed, setDisplayed] = React.useState(0)
28
+ const ref = React.useRef<HTMLSpanElement>(null)
29
+ const started = React.useRef(false)
30
+
31
+ React.useEffect(() => {
32
+ const el = ref.current
33
+ if (!el) return
34
+ const observer = new IntersectionObserver(([entry]) => {
35
+ if (entry.isIntersecting && !started.current) {
36
+ started.current = true
37
+ const duration = 1200
38
+ const start = performance.now()
39
+ const tick = (now: number) => {
40
+ const progress = Math.min((now - start) / duration, 1)
41
+ const eased = 1 - Math.pow(1 - progress, 3)
42
+ setDisplayed(Math.round(eased * value))
43
+ if (progress < 1) requestAnimationFrame(tick)
44
+ }
45
+ requestAnimationFrame(tick)
46
+ }
47
+ }, { threshold: 0.5 })
48
+ observer.observe(el)
49
+ return () => observer.disconnect()
50
+ }, [value])
51
+
52
+ return (
53
+ <span ref={ref} className="tabular-nums">
54
+ {prefix}{displayed}{suffix}
55
+ </span>
56
+ )
57
+ }
58
+
59
+ export function StatsStrip({ stats = defaultStats, layout = "strip", className }: StatsStripProps) {
60
+ if (layout === "strip") {
61
+ return (
62
+ <section className={cx("w-full border-y border-border bg-surface-2/30 py-10 px-6", className)}>
63
+ <div className="mx-auto max-w-5xl">
64
+ <div className="flex flex-wrap justify-center gap-0 divide-x divide-border">
65
+ {stats.map((stat, i) => (
66
+ <div key={i} className="flex flex-col items-center px-10 py-2">
67
+ <span className="text-3xl font-bold tracking-tight text-foreground">
68
+ <AnimatedValue value={stat.value} prefix={stat.prefix} suffix={stat.suffix} />
69
+ </span>
70
+ <span className="mt-1 text-sm font-medium text-foreground">{stat.label}</span>
71
+ {stat.description && (
72
+ <span className="text-xs text-muted-foreground">{stat.description}</span>
73
+ )}
74
+ </div>
75
+ ))}
76
+ </div>
77
+ </div>
78
+ </section>
79
+ )
80
+ }
81
+
82
+ return (
83
+ <section className={cx("w-full py-16 px-6", className)}>
84
+ <div className="mx-auto max-w-5xl grid grid-cols-2 sm:grid-cols-4 gap-8">
85
+ {stats.map((stat, i) => (
86
+ <div key={i} className="flex flex-col">
87
+ <span className="text-4xl font-bold tracking-tight text-foreground">
88
+ <AnimatedValue value={stat.value} prefix={stat.prefix} suffix={stat.suffix} />
89
+ </span>
90
+ <Separator className="my-3 w-8" />
91
+ <span className="text-sm font-medium text-foreground">{stat.label}</span>
92
+ {stat.description && (
93
+ <span className="text-xs text-muted-foreground mt-0.5">{stat.description}</span>
94
+ )}
95
+ </div>
96
+ ))}
97
+ </div>
98
+ </section>
99
+ )
100
+ }
@@ -0,0 +1,42 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cx } from "@/lib/cx"
5
+
6
+ export interface TokenStreamProps extends React.HTMLAttributes<HTMLDivElement> {
7
+ content: string
8
+ speed?: number
9
+ onComplete?: () => void
10
+ }
11
+
12
+ export function TokenStream({
13
+ content,
14
+ speed = 15,
15
+ onComplete,
16
+ className,
17
+ ...props
18
+ }: TokenStreamProps) {
19
+ const [displayedContent, setDisplayedContent] = React.useState("")
20
+ const [currentIndex, setCurrentIndex] = React.useState(0)
21
+
22
+ React.useEffect(() => {
23
+ if (currentIndex < content.length) {
24
+ const timeout = setTimeout(() => {
25
+ setDisplayedContent((prev) => prev + content[currentIndex])
26
+ setCurrentIndex((prev) => prev + 1)
27
+ }, speed)
28
+ return () => clearTimeout(timeout)
29
+ } else if (onComplete) {
30
+ onComplete()
31
+ }
32
+ }, [content, currentIndex, speed, onComplete])
33
+
34
+ return (
35
+ <div className={cx("relative inline-block", className)} {...props}>
36
+ <span className="whitespace-pre-wrap">{displayedContent}</span>
37
+ {currentIndex < content.length && (
38
+ <span className="ml-1 inline-block h-4 w-1 animate-pulse bg-primary align-middle" />
39
+ )}
40
+ </div>
41
+ )
42
+ }
@@ -0,0 +1,116 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Zap, Database, Users, ArrowUpRight } from "lucide-react"
5
+ import { cx } from "@/lib/cx"
6
+
7
+ interface UsageMetric {
8
+ label: string
9
+ used: number
10
+ limit: number
11
+ unit?: string
12
+ icon?: React.ReactNode
13
+ }
14
+
15
+ const DEFAULT_METRICS: UsageMetric[] = [
16
+ {
17
+ label: "API Requests",
18
+ used: 48200,
19
+ limit: 100000,
20
+ unit: "req",
21
+ icon: <Zap className="h-3.5 w-3.5" />,
22
+ },
23
+ {
24
+ label: "Storage",
25
+ used: 2.4,
26
+ limit: 10,
27
+ unit: "GB",
28
+ icon: <Database className="h-3.5 w-3.5" />,
29
+ },
30
+ {
31
+ label: "Seats",
32
+ used: 3,
33
+ limit: 5,
34
+ unit: "users",
35
+ icon: <Users className="h-3.5 w-3.5" />,
36
+ },
37
+ ]
38
+
39
+ interface UsageCardProps {
40
+ plan?: string
41
+ renewsAt?: string
42
+ metrics?: UsageMetric[]
43
+ onUpgrade?: () => void
44
+ }
45
+
46
+ function formatNumber(n: number): string {
47
+ if (n >= 1000) return `${(n / 1000).toFixed(1)}k`
48
+ return String(n)
49
+ }
50
+
51
+ function UsageBar({ pct }: { pct: number }) {
52
+ const clamped = Math.min(pct, 100)
53
+ return (
54
+ <div className="h-1.5 w-full overflow-hidden rounded-full bg-surface-3">
55
+ <div
56
+ className={cx(
57
+ "h-full rounded-full transition-all",
58
+ clamped >= 90 ? "bg-danger" : clamped >= 70 ? "bg-warning" : "bg-primary"
59
+ )}
60
+ style={{ width: `${clamped}%` }}
61
+ />
62
+ </div>
63
+ )
64
+ }
65
+
66
+ export function UsageCard({
67
+ plan = "Pro",
68
+ renewsAt,
69
+ metrics = DEFAULT_METRICS,
70
+ onUpgrade,
71
+ }: UsageCardProps) {
72
+ return (
73
+ <div className="w-full max-w-sm rounded-2xl border border-border bg-card p-6">
74
+ <div className="flex items-start justify-between mb-6">
75
+ <div>
76
+ <p className="text-xs font-mono text-muted-foreground uppercase tracking-widest mb-1">Current Plan</p>
77
+ <p className="text-lg font-semibold">{plan}</p>
78
+ {renewsAt && (
79
+ <p className="text-xs text-muted-foreground mt-0.5">Renews {renewsAt}</p>
80
+ )}
81
+ </div>
82
+ {onUpgrade && (
83
+ <button
84
+ type="button"
85
+ onClick={onUpgrade}
86
+ className="inline-flex items-center gap-1 rounded-lg border border-border bg-surface-2 px-3 py-1.5 text-xs font-medium hover:bg-surface-3 transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border-strong"
87
+ >
88
+ Upgrade
89
+ <ArrowUpRight className="h-3 w-3" />
90
+ </button>
91
+ )}
92
+ </div>
93
+
94
+ <div className="space-y-4">
95
+ {metrics.map((m) => {
96
+ const pct = (m.used / m.limit) * 100
97
+ return (
98
+ <div key={m.label}>
99
+ <div className="flex items-center justify-between mb-1.5">
100
+ <div className="flex items-center gap-1.5 text-xs text-muted-foreground">
101
+ {m.icon}
102
+ <span>{m.label}</span>
103
+ </div>
104
+ <span className="text-xs font-mono text-foreground">
105
+ {formatNumber(m.used)}{m.unit && ` ${m.unit}`}
106
+ <span className="text-muted-foreground"> / {formatNumber(m.limit)}{m.unit && ` ${m.unit}`}</span>
107
+ </span>
108
+ </div>
109
+ <UsageBar pct={pct} />
110
+ </div>
111
+ )
112
+ })}
113
+ </div>
114
+ </div>
115
+ )
116
+ }
@@ -0,0 +1,58 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as RadixAccordion from "@radix-ui/react-accordion"
5
+ import { ChevronDown } from "lucide-react"
6
+ import { cx } from "@/lib/cx"
7
+
8
+ export const Accordion = RadixAccordion.Root
9
+
10
+ export const AccordionItem = React.forwardRef<
11
+ React.ElementRef<typeof RadixAccordion.Item>,
12
+ React.ComponentPropsWithoutRef<typeof RadixAccordion.Item>
13
+ >(({ className, ...props }, ref) => (
14
+ <RadixAccordion.Item
15
+ ref={ref}
16
+ className={cx(
17
+ "overflow-hidden rounded-xl border border-border/50 bg-card mb-3 last:mb-0 transition-colors data-[state=open]:border-primary/30",
18
+ className
19
+ )}
20
+ {...props}
21
+ />
22
+ ))
23
+ AccordionItem.displayName = "AccordionItem"
24
+
25
+ export const AccordionTrigger = React.forwardRef<
26
+ React.ElementRef<typeof RadixAccordion.Trigger>,
27
+ React.ComponentPropsWithoutRef<typeof RadixAccordion.Trigger>
28
+ >(({ className, children, ...props }, ref) => (
29
+ <RadixAccordion.Header className="flex">
30
+ <RadixAccordion.Trigger
31
+ ref={ref}
32
+ className={cx(
33
+ "flex flex-1 items-center justify-between px-5 py-4 text-left font-medium text-foreground outline-none transition-all hover:bg-surface-2 focus-visible:bg-surface-2 focus-visible:ring-1 focus-visible:ring-border-strong",
34
+ "[&[data-state=open]>svg]:rotate-180",
35
+ className
36
+ )}
37
+ {...props}
38
+ >
39
+ {children}
40
+ <ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-300 ease-in-out" />
41
+ </RadixAccordion.Trigger>
42
+ </RadixAccordion.Header>
43
+ ))
44
+ AccordionTrigger.displayName = "AccordionTrigger"
45
+
46
+ export const AccordionContent = React.forwardRef<
47
+ React.ElementRef<typeof RadixAccordion.Content>,
48
+ React.ComponentPropsWithoutRef<typeof RadixAccordion.Content>
49
+ >(({ className, children, ...props }, ref) => (
50
+ <RadixAccordion.Content
51
+ ref={ref}
52
+ className="overflow-hidden text-sm text-muted-foreground data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
53
+ {...props}
54
+ >
55
+ <div className={cx("px-5 pb-4 pt-1", className)}>{children}</div>
56
+ </RadixAccordion.Content>
57
+ ))
58
+ AccordionContent.displayName = "AccordionContent"
@@ -0,0 +1,113 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as RadixAlertDialog from "@radix-ui/react-alert-dialog"
5
+ import { cx } from "@/lib/cx"
6
+
7
+ export const AlertDialog = RadixAlertDialog.Root
8
+ export const AlertDialogTrigger = RadixAlertDialog.Trigger
9
+ export const AlertDialogPortal = RadixAlertDialog.Portal
10
+
11
+ export const AlertDialogOverlay = React.forwardRef<
12
+ React.ElementRef<typeof RadixAlertDialog.Overlay>,
13
+ React.ComponentPropsWithoutRef<typeof RadixAlertDialog.Overlay>
14
+ >(({ className, ...props }, ref) => (
15
+ <RadixAlertDialog.Overlay
16
+ ref={ref}
17
+ className={cx(
18
+ "fixed inset-0 z-50 bg-black/40 backdrop-blur-sm",
19
+ "data-[state=open]:animate-in data-[state=open]:fade-in-0",
20
+ "data-[state=closed]:animate-out data-[state=closed]:fade-out-0",
21
+ className
22
+ )}
23
+ {...props}
24
+ />
25
+ ))
26
+ AlertDialogOverlay.displayName = "AlertDialogOverlay"
27
+
28
+ export const AlertDialogContent = React.forwardRef<
29
+ React.ElementRef<typeof RadixAlertDialog.Content>,
30
+ React.ComponentPropsWithoutRef<typeof RadixAlertDialog.Content>
31
+ >(({ className, ...props }, ref) => (
32
+ <RadixAlertDialog.Portal>
33
+ <AlertDialogOverlay />
34
+ <RadixAlertDialog.Content
35
+ ref={ref}
36
+ className={cx(
37
+ "fixed left-1/2 top-1/2 z-50 w-full max-w-md -translate-x-1/2 -translate-y-1/2",
38
+ "rounded-2xl border border-border bg-card p-6 shadow-xl outline-none",
39
+ "data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]",
40
+ "data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
41
+ className
42
+ )}
43
+ {...props}
44
+ />
45
+ </RadixAlertDialog.Portal>
46
+ ))
47
+ AlertDialogContent.displayName = "AlertDialogContent"
48
+
49
+ export const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
50
+ <div className={cx("flex flex-col gap-1.5 mb-4", className)} {...props} />
51
+ )
52
+ AlertDialogHeader.displayName = "AlertDialogHeader"
53
+
54
+ export const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
55
+ <div className={cx("flex items-center justify-end gap-2 mt-6", className)} {...props} />
56
+ )
57
+ AlertDialogFooter.displayName = "AlertDialogFooter"
58
+
59
+ export const AlertDialogTitle = React.forwardRef<
60
+ React.ElementRef<typeof RadixAlertDialog.Title>,
61
+ React.ComponentPropsWithoutRef<typeof RadixAlertDialog.Title>
62
+ >(({ className, ...props }, ref) => (
63
+ <RadixAlertDialog.Title
64
+ ref={ref}
65
+ className={cx("text-base font-semibold leading-tight text-foreground", className)}
66
+ {...props}
67
+ />
68
+ ))
69
+ AlertDialogTitle.displayName = "AlertDialogTitle"
70
+
71
+ export const AlertDialogDescription = React.forwardRef<
72
+ React.ElementRef<typeof RadixAlertDialog.Description>,
73
+ React.ComponentPropsWithoutRef<typeof RadixAlertDialog.Description>
74
+ >(({ className, ...props }, ref) => (
75
+ <RadixAlertDialog.Description
76
+ ref={ref}
77
+ className={cx("text-sm text-muted-foreground leading-relaxed", className)}
78
+ {...props}
79
+ />
80
+ ))
81
+ AlertDialogDescription.displayName = "AlertDialogDescription"
82
+
83
+ export const AlertDialogAction = React.forwardRef<
84
+ React.ElementRef<typeof RadixAlertDialog.Action>,
85
+ React.ComponentPropsWithoutRef<typeof RadixAlertDialog.Action>
86
+ >(({ className, ...props }, ref) => (
87
+ <RadixAlertDialog.Action
88
+ ref={ref}
89
+ className={cx(
90
+ "inline-flex items-center justify-center rounded-lg px-4 py-2 text-sm font-medium transition-colors outline-none",
91
+ "bg-foreground text-background hover:bg-foreground/90 focus-visible:ring-1 focus-visible:ring-border-strong focus-visible:ring-offset-1 focus-visible:ring-offset-background",
92
+ className
93
+ )}
94
+ {...props}
95
+ />
96
+ ))
97
+ AlertDialogAction.displayName = "AlertDialogAction"
98
+
99
+ export const AlertDialogCancel = React.forwardRef<
100
+ React.ElementRef<typeof RadixAlertDialog.Cancel>,
101
+ React.ComponentPropsWithoutRef<typeof RadixAlertDialog.Cancel>
102
+ >(({ className, ...props }, ref) => (
103
+ <RadixAlertDialog.Cancel
104
+ ref={ref}
105
+ className={cx(
106
+ "inline-flex items-center justify-center rounded-lg border border-border px-4 py-2 text-sm font-medium transition-colors outline-none",
107
+ "bg-transparent text-foreground hover:bg-surface-2 focus-visible:ring-1 focus-visible:ring-border-strong focus-visible:ring-offset-1 focus-visible:ring-offset-background",
108
+ className
109
+ )}
110
+ {...props}
111
+ />
112
+ ))
113
+ AlertDialogCancel.displayName = "AlertDialogCancel"
@@ -0,0 +1,48 @@
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+ import { cx } from "@/lib/cx"
4
+
5
+ const alertVariants = cva(
6
+ "relative w-full rounded-xl border px-4 py-3.5 text-sm [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-3.5 [&>svg~*]:pl-7",
7
+ {
8
+ variants: {
9
+ variant: {
10
+ default: "bg-surface-2 border-border text-foreground",
11
+ info: "bg-surface-2 border-border text-foreground",
12
+ warning: "bg-surface-2 border-border text-foreground",
13
+ success: "bg-surface-2 border-border text-foreground",
14
+ danger: "bg-surface-2 border-danger/30 text-foreground",
15
+ },
16
+ },
17
+ defaultVariants: {
18
+ variant: "default",
19
+ },
20
+ }
21
+ )
22
+
23
+ export interface AlertProps
24
+ extends React.HTMLAttributes<HTMLDivElement>,
25
+ VariantProps<typeof alertVariants> {}
26
+
27
+ export const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
28
+ ({ className, variant, ...props }, ref) => (
29
+ <div ref={ref} role="alert" className={cx(alertVariants({ variant }), className)} {...props} />
30
+ )
31
+ )
32
+ Alert.displayName = "Alert"
33
+
34
+ export const AlertTitle = React.forwardRef<
35
+ HTMLHeadingElement,
36
+ React.HTMLAttributes<HTMLHeadingElement>
37
+ >(({ className, ...props }, ref) => (
38
+ <h5 ref={ref} className={cx("font-medium leading-none tracking-tight mb-1", className)} {...props} />
39
+ ))
40
+ AlertTitle.displayName = "AlertTitle"
41
+
42
+ export const AlertDescription = React.forwardRef<
43
+ HTMLParagraphElement,
44
+ React.HTMLAttributes<HTMLParagraphElement>
45
+ >(({ className, ...props }, ref) => (
46
+ <p ref={ref} className={cx("text-sm text-muted-foreground leading-relaxed", className)} {...props} />
47
+ ))
48
+ AlertDescription.displayName = "AlertDescription"
@@ -0,0 +1,77 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cx } from "@/lib/cx"
5
+
6
+ type Format = "default" | "compact" | "currency" | "percent"
7
+
8
+ export interface AnimatedNumberProps {
9
+ value: number
10
+ duration?: number
11
+ format?: Format
12
+ currency?: string
13
+ decimals?: number
14
+ prefix?: string
15
+ suffix?: string
16
+ className?: string
17
+ }
18
+
19
+ function formatValue(n: number, format: Format, currency: string, decimals: number, prefix: string, suffix: string): string {
20
+ let out: string
21
+ if (format === "compact") {
22
+ if (n >= 1_000_000) out = (n / 1_000_000).toFixed(1).replace(/\.0$/, "") + "M"
23
+ else if (n >= 1_000) out = (n / 1_000).toFixed(1).replace(/\.0$/, "") + "K"
24
+ else out = Math.round(n).toString()
25
+ } else if (format === "currency") {
26
+ out = new Intl.NumberFormat("en-US", { style: "currency", currency, maximumFractionDigits: decimals }).format(n)
27
+ } else if (format === "percent") {
28
+ out = n.toFixed(decimals) + "%"
29
+ } else {
30
+ out = n.toFixed(decimals)
31
+ }
32
+ return prefix + out + suffix
33
+ }
34
+
35
+ export function AnimatedNumber({
36
+ value,
37
+ duration = 1400,
38
+ format = "default",
39
+ currency = "USD",
40
+ decimals = 0,
41
+ prefix = "",
42
+ suffix = "",
43
+ className,
44
+ }: AnimatedNumberProps) {
45
+ const [current, setCurrent] = React.useState(0)
46
+ const ref = React.useRef<HTMLSpanElement>(null)
47
+ const triggered = React.useRef(false)
48
+
49
+ React.useEffect(() => {
50
+ const el = ref.current
51
+ if (!el) return
52
+ const observer = new IntersectionObserver(
53
+ ([entry]) => {
54
+ if (entry.isIntersecting && !triggered.current) {
55
+ triggered.current = true
56
+ const startTime = performance.now()
57
+ const tick = (now: number) => {
58
+ const t = Math.min((now - startTime) / duration, 1)
59
+ const eased = 1 - Math.pow(1 - t, 4)
60
+ setCurrent(eased * value)
61
+ if (t < 1) requestAnimationFrame(tick)
62
+ }
63
+ requestAnimationFrame(tick)
64
+ }
65
+ },
66
+ { threshold: 0.4 }
67
+ )
68
+ observer.observe(el)
69
+ return () => observer.disconnect()
70
+ }, [value, duration])
71
+
72
+ return (
73
+ <span ref={ref} className={cx("tabular-nums", className)}>
74
+ {formatValue(current, format, currency, decimals, prefix, suffix)}
75
+ </span>
76
+ )
77
+ }
@@ -0,0 +1,20 @@
1
+ import * as React from "react"
2
+ import { cx } from "@/lib/cx"
3
+
4
+ export interface AspectRatioProps extends React.HTMLAttributes<HTMLDivElement> {
5
+ ratio?: number
6
+ }
7
+
8
+ export const AspectRatio = React.forwardRef<HTMLDivElement, AspectRatioProps>(
9
+ ({ ratio = 16 / 9, className, children, style, ...props }, ref) => (
10
+ <div
11
+ ref={ref}
12
+ style={{ aspectRatio: String(ratio), ...style }}
13
+ className={cx("w-full overflow-hidden", className)}
14
+ {...props}
15
+ >
16
+ {children}
17
+ </div>
18
+ )
19
+ )
20
+ AspectRatio.displayName = "AspectRatio"
@@ -0,0 +1,61 @@
1
+ import * as React from "react"
2
+ import { cx } from "@/lib/cx"
3
+
4
+ export interface AvatarStackUser {
5
+ name: string
6
+ src?: string
7
+ }
8
+
9
+ export interface AvatarStackProps {
10
+ users: AvatarStackUser[]
11
+ max?: number
12
+ size?: "sm" | "md" | "lg"
13
+ className?: string
14
+ }
15
+
16
+ const sizes = {
17
+ sm: { ring: "h-6 w-6 text-[9px] border-[1.5px]", offset: "-ml-1.5" },
18
+ md: { ring: "h-8 w-8 text-[11px] border-2", offset: "-ml-2" },
19
+ lg: { ring: "h-10 w-10 text-xs border-2", offset: "-ml-2.5" },
20
+ }
21
+
22
+ export function AvatarStack({ users, max = 4, size = "md", className }: AvatarStackProps) {
23
+ const visible = users.slice(0, max)
24
+ const overflow = users.length - max
25
+ const { ring, offset } = sizes[size]
26
+
27
+ return (
28
+ <div className={cx("flex items-center", className)}>
29
+ {visible.map((user, i) => (
30
+ <div
31
+ key={i}
32
+ title={user.name}
33
+ className={cx(
34
+ "relative inline-flex shrink-0 items-center justify-center rounded-full border-background bg-surface-2 font-medium text-foreground overflow-hidden",
35
+ ring,
36
+ i > 0 && offset
37
+ )}
38
+ style={{ zIndex: visible.length - i }}
39
+ >
40
+ {user.src ? (
41
+ <img src={user.src} alt={user.name} className="h-full w-full object-cover" />
42
+ ) : (
43
+ <span className="select-none">{user.name.charAt(0).toUpperCase()}</span>
44
+ )}
45
+ </div>
46
+ ))}
47
+ {overflow > 0 && (
48
+ <div
49
+ className={cx(
50
+ "relative inline-flex shrink-0 items-center justify-center rounded-full border-background bg-surface-3 font-medium text-muted-foreground",
51
+ ring,
52
+ offset
53
+ )}
54
+ title={`${overflow} more`}
55
+ >
56
+ +{overflow}
57
+ </div>
58
+ )}
59
+ </div>
60
+ )
61
+ }