@momentumworld/ui-react 0.17.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 (138) hide show
  1. package/dist/chunk-2HQPCV2L.js +895 -0
  2. package/dist/chunk-2HQPCV2L.js.map +1 -0
  3. package/dist/form.d.ts +175 -0
  4. package/dist/form.js +356 -0
  5. package/dist/form.js.map +1 -0
  6. package/dist/index.d.ts +2162 -0
  7. package/dist/index.js +11640 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/input-JCZs61MO.d.ts +24 -0
  10. package/package.json +90 -0
  11. package/src/components/form/checkbox-group-field.tsx +101 -0
  12. package/src/components/form/date-field.tsx +79 -0
  13. package/src/components/form/date-range-field.tsx +106 -0
  14. package/src/components/form/form-context.ts +10 -0
  15. package/src/components/form/form.tsx +54 -0
  16. package/src/components/form/number-field.tsx +69 -0
  17. package/src/components/form/select-field.tsx +76 -0
  18. package/src/components/form/submit-button.tsx +28 -0
  19. package/src/components/form/text-field.tsx +107 -0
  20. package/src/components/layout/dashboard-header.tsx +54 -0
  21. package/src/components/layout/dashboard-panel.tsx +34 -0
  22. package/src/components/layout/dashboard-topbar.tsx +221 -0
  23. package/src/components/theme-provider.tsx +403 -0
  24. package/src/components/ui/accordion.tsx +69 -0
  25. package/src/components/ui/alert-dialog.tsx +169 -0
  26. package/src/components/ui/alert.tsx +80 -0
  27. package/src/components/ui/animated-counter.tsx +109 -0
  28. package/src/components/ui/animated-theme-toggler.tsx +265 -0
  29. package/src/components/ui/app-store-buttons.tsx +182 -0
  30. package/src/components/ui/aspect-ratio.tsx +23 -0
  31. package/src/components/ui/aurora-orb.tsx +163 -0
  32. package/src/components/ui/aurora-text.tsx +76 -0
  33. package/src/components/ui/autocomplete.tsx +296 -0
  34. package/src/components/ui/avatar-group.tsx +150 -0
  35. package/src/components/ui/avatar.tsx +285 -0
  36. package/src/components/ui/badge-group.tsx +160 -0
  37. package/src/components/ui/badge.tsx +172 -0
  38. package/src/components/ui/breadcrumb.tsx +112 -0
  39. package/src/components/ui/button.tsx +77 -0
  40. package/src/components/ui/calendar.tsx +137 -0
  41. package/src/components/ui/card.tsx +244 -0
  42. package/src/components/ui/carousel.tsx +258 -0
  43. package/src/components/ui/chart.tsx +379 -0
  44. package/src/components/ui/chat-divider.tsx +67 -0
  45. package/src/components/ui/chat.tsx +494 -0
  46. package/src/components/ui/checkbox-group.tsx +16 -0
  47. package/src/components/ui/checkbox.tsx +82 -0
  48. package/src/components/ui/collapsible.tsx +45 -0
  49. package/src/components/ui/color-picker.tsx +389 -0
  50. package/src/components/ui/combobox.tsx +411 -0
  51. package/src/components/ui/command.tsx +264 -0
  52. package/src/components/ui/confetti.tsx +196 -0
  53. package/src/components/ui/context-menu.tsx +271 -0
  54. package/src/components/ui/context-meter.tsx +134 -0
  55. package/src/components/ui/credit-card.tsx +214 -0
  56. package/src/components/ui/data-table-paged.tsx +163 -0
  57. package/src/components/ui/date-picker.tsx +154 -0
  58. package/src/components/ui/dialog.tsx +196 -0
  59. package/src/components/ui/drawer.tsx +135 -0
  60. package/src/components/ui/empty.tsx +127 -0
  61. package/src/components/ui/fab.tsx +45 -0
  62. package/src/components/ui/featured-icon.tsx +149 -0
  63. package/src/components/ui/field.tsx +88 -0
  64. package/src/components/ui/fieldset.tsx +29 -0
  65. package/src/components/ui/form.tsx +17 -0
  66. package/src/components/ui/frame.tsx +82 -0
  67. package/src/components/ui/generic-empty.tsx +142 -0
  68. package/src/components/ui/group.tsx +97 -0
  69. package/src/components/ui/help-tip.tsx +94 -0
  70. package/src/components/ui/horizontal-scroll-fader.tsx +228 -0
  71. package/src/components/ui/input-group.tsx +102 -0
  72. package/src/components/ui/input-otp.tsx +96 -0
  73. package/src/components/ui/input.tsx +66 -0
  74. package/src/components/ui/item.tsx +198 -0
  75. package/src/components/ui/kbd.tsx +30 -0
  76. package/src/components/ui/label.tsx +28 -0
  77. package/src/components/ui/markdown.tsx +188 -0
  78. package/src/components/ui/menu.tsx +312 -0
  79. package/src/components/ui/menubar.tsx +93 -0
  80. package/src/components/ui/meter.tsx +67 -0
  81. package/src/components/ui/multi-select.tsx +308 -0
  82. package/src/components/ui/navigation-menu.tsx +143 -0
  83. package/src/components/ui/number-field.tsx +160 -0
  84. package/src/components/ui/pagination-controls.tsx +74 -0
  85. package/src/components/ui/pagination.tsx +149 -0
  86. package/src/components/ui/pipeline.tsx +102 -0
  87. package/src/components/ui/popover.tsx +119 -0
  88. package/src/components/ui/preview-card.tsx +55 -0
  89. package/src/components/ui/progress.tsx +289 -0
  90. package/src/components/ui/qr-code.tsx +150 -0
  91. package/src/components/ui/radio-group.tsx +103 -0
  92. package/src/components/ui/resizable.tsx +56 -0
  93. package/src/components/ui/scroll-area.tsx +90 -0
  94. package/src/components/ui/scroller.tsx +38 -0
  95. package/src/components/ui/section-header.tsx +118 -0
  96. package/src/components/ui/segmented-control.tsx +83 -0
  97. package/src/components/ui/select.tsx +181 -0
  98. package/src/components/ui/separator.tsx +23 -0
  99. package/src/components/ui/sheet.tsx +224 -0
  100. package/src/components/ui/sidebar.tsx +774 -0
  101. package/src/components/ui/skeleton.tsx +16 -0
  102. package/src/components/ui/slider.tsx +108 -0
  103. package/src/components/ui/smooth-scroll.tsx +206 -0
  104. package/src/components/ui/social-button.tsx +247 -0
  105. package/src/components/ui/spinner-on-demand.tsx +32 -0
  106. package/src/components/ui/spinner.tsx +18 -0
  107. package/src/components/ui/stat.tsx +187 -0
  108. package/src/components/ui/stepper.tsx +167 -0
  109. package/src/components/ui/switch.tsx +56 -0
  110. package/src/components/ui/table.tsx +126 -0
  111. package/src/components/ui/tabs.tsx +90 -0
  112. package/src/components/ui/tag.tsx +242 -0
  113. package/src/components/ui/target-countdown.tsx +46 -0
  114. package/src/components/ui/text-editor.tsx +313 -0
  115. package/src/components/ui/textarea.tsx +51 -0
  116. package/src/components/ui/thinking-orb.tsx +6 -0
  117. package/src/components/ui/time-picker.tsx +308 -0
  118. package/src/components/ui/timeline.tsx +116 -0
  119. package/src/components/ui/toast.tsx +268 -0
  120. package/src/components/ui/toggle-group.tsx +101 -0
  121. package/src/components/ui/toggle.tsx +45 -0
  122. package/src/components/ui/toolbar.tsx +89 -0
  123. package/src/components/ui/tooltip.tsx +102 -0
  124. package/src/components/ui/vertical-scroll-fader.tsx +250 -0
  125. package/src/components/ui/video-player.tsx +275 -0
  126. package/src/components/upload/avatar-upload-base.tsx +131 -0
  127. package/src/components/upload/image-upload-base.tsx +112 -0
  128. package/src/form.ts +17 -0
  129. package/src/index.ts +143 -0
  130. package/src/lib/hooks/use-callback-ref.ts +15 -0
  131. package/src/lib/hooks/use-first-render.ts +11 -0
  132. package/src/lib/hooks/use-hover.ts +53 -0
  133. package/src/lib/hooks/use-is-tab-active.ts +17 -0
  134. package/src/lib/hooks/use-media-query.ts +164 -0
  135. package/src/lib/utils/css.ts +6 -0
  136. package/src/styles.css +329 -0
  137. package/src/types/helpers.ts +24 -0
  138. package/src/types/react.d.ts +7 -0
@@ -0,0 +1,163 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef } from "react";
4
+ import { MODE_DRAWS, resolvePreset, type OrbSize, type OrbState } from "thinking-orbs";
5
+
6
+ import { cn } from "../../lib/utils/css";
7
+
8
+ // Re-exported so consumers can type their own props without depending on
9
+ // thinking-orbs directly.
10
+ export type { OrbSize, OrbState } from "thinking-orbs";
11
+
12
+ /** AuroraText's default RGB stops, so orb + text read as one piece. */
13
+ const DEFAULT_COLORS = ["#FF0080", "#7928CA", "#0070F3", "#38bdf8"];
14
+
15
+ export interface AuroraOrbProps {
16
+ /**
17
+ * Which thinking-orbs animation to show: "working" | "searching" |
18
+ * "solving" | "listening" | "composing" | "shaping". Default "working".
19
+ */
20
+ state?: OrbState;
21
+ /** Tuned size preset from thinking-orbs — 20 (inline) or 64 (avatar). Default 20. */
22
+ size?: OrbSize;
23
+ /** Animation speed multiplier on top of the preset's baked speed. Default 1. */
24
+ speed?: number;
25
+ /**
26
+ * Gradient stops swept across the dots, matching AuroraText's default RGB
27
+ * set (pink → purple → blue → sky). Vivid stops stay legible on both light
28
+ * and dark backgrounds — the dots are painted with these colors directly.
29
+ */
30
+ colors?: string[];
31
+ className?: string;
32
+ }
33
+
34
+ /**
35
+ * A `thinking-orbs` orb recolored with the aurora gradient — the dotted-orb
36
+ * counterpart to `AuroraText`.
37
+ *
38
+ * The stock `<ThinkingOrb>` has no color API (its canvas paints monochrome
39
+ * theme ink only), but the package exports its frame painters (`MODE_DRAWS` +
40
+ * `resolvePreset`) for custom rendering. So we run the same animation loop
41
+ * and, after each frame, composite a slowly rotating aurora gradient onto the
42
+ * dots via `source-in`: the dots' alpha (their depth shading) survives while
43
+ * their ink color is replaced by the gradient. Theme detection becomes
44
+ * unnecessary — the gradient works on light and dark backgrounds alike.
45
+ * Respects prefers-reduced-motion (renders a single still frame).
46
+ *
47
+ * @example
48
+ * <AuroraOrb state="solving" speed={0.7} />
49
+ * <AuroraOrb size={64} colors={["#22d3ee", "#a855f7", "#f43f5e"]} />
50
+ */
51
+ export function AuroraOrb({
52
+ className,
53
+ colors = DEFAULT_COLORS,
54
+ size = 20,
55
+ speed = 1,
56
+ state = "working",
57
+ }: AuroraOrbProps) {
58
+ const canvasRef = useRef<HTMLCanvasElement>(null);
59
+ // A joined key keeps the effect's deps primitive (an inline `colors` array
60
+ // would otherwise retrigger every render). Joined on a control character so
61
+ // comma-bearing stops like rgb(255, 0, 128) survive the round-trip.
62
+ const colorsKey = colors.join("\u001f");
63
+
64
+ useEffect(() => {
65
+ const canvas = canvasRef.current;
66
+ if (!canvas) return;
67
+ const dpr = Math.min(2, (typeof devicePixelRatio !== "undefined" && devicePixelRatio) || 1);
68
+ canvas.width = Math.round(size * dpr);
69
+ canvas.height = Math.round(size * dpr);
70
+ const ctx = canvas.getContext("2d");
71
+ if (!ctx) return;
72
+
73
+ // Guard colors={[]}: an empty array bypasses the default parameter.
74
+ const stops = colorsKey ? colorsKey.split("\u001f") : DEFAULT_COLORS;
75
+ const { mode, speed: baseSpeed, opts } = resolvePreset(state, size);
76
+ const draw = MODE_DRAWS[mode];
77
+ const rate = baseSpeed * speed;
78
+
79
+ const paint = (t: number, wall: number) => {
80
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
81
+ ctx.clearRect(0, 0, size, size);
82
+ draw(ctx, size, t, false, opts);
83
+ // Recolor the frame: a linear gradient across the orb, rotating so the
84
+ // colors drift the way AuroraText's sweep does. Driven by wall-clock
85
+ // (~5s per turn) rather than the preset-scaled t, so the drift stays
86
+ // visible even for slow presets/speeds.
87
+ const angle = wall * 1.2;
88
+ const r = size / 2;
89
+ const dx = Math.cos(angle) * r;
90
+ const dy = Math.sin(angle) * r;
91
+ const gradient = ctx.createLinearGradient(r - dx, r - dy, r + dx, r + dy);
92
+ stops.forEach((color, i) => {
93
+ gradient.addColorStop(stops.length === 1 ? 0 : i / (stops.length - 1), color);
94
+ });
95
+ ctx.globalCompositeOperation = "source-in";
96
+ ctx.fillStyle = gradient;
97
+ ctx.fillRect(0, 0, size, size);
98
+ ctx.globalCompositeOperation = "source-over";
99
+ };
100
+
101
+ // Reduced motion: a single still frame, matching the stock component.
102
+ if (typeof matchMedia !== "undefined" && matchMedia("(prefers-reduced-motion: reduce)").matches) {
103
+ paint(0.6, 0.6);
104
+ return;
105
+ }
106
+
107
+ // Animation loop, paused while the orb is off-screen or the tab is
108
+ // hidden — mirroring the stock ThinkingOrb's behavior.
109
+ let frame = 0;
110
+ let running = false;
111
+ const loop = () => {
112
+ const now = performance.now() / 1000;
113
+ paint(now * rate, now);
114
+ if (running) frame = requestAnimationFrame(loop);
115
+ };
116
+ const start = () => {
117
+ if (running) return;
118
+ running = true;
119
+ frame = requestAnimationFrame(loop);
120
+ };
121
+ const stop = () => {
122
+ running = false;
123
+ cancelAnimationFrame(frame);
124
+ };
125
+
126
+ const first = performance.now() / 1000;
127
+ paint(first * rate, first);
128
+ let visible = true;
129
+ const observer =
130
+ typeof IntersectionObserver !== "undefined"
131
+ ? new IntersectionObserver((entries) => {
132
+ const entry = entries[0];
133
+ if (!entry) return;
134
+ visible = entry.isIntersecting;
135
+ if (visible && document.visibilityState !== "hidden") start();
136
+ else stop();
137
+ })
138
+ : null;
139
+ observer?.observe(canvas);
140
+ const onVisibilityChange = () => {
141
+ if (document.visibilityState === "hidden") stop();
142
+ else if (visible) start();
143
+ };
144
+ document.addEventListener("visibilitychange", onVisibilityChange);
145
+ if (!observer) start();
146
+
147
+ return () => {
148
+ stop();
149
+ observer?.disconnect();
150
+ document.removeEventListener("visibilitychange", onVisibilityChange);
151
+ };
152
+ }, [colorsKey, size, speed, state]);
153
+
154
+ return (
155
+ <canvas
156
+ aria-hidden="true"
157
+ className={cn("shrink-0", className)}
158
+ data-slot="aurora-orb"
159
+ ref={canvasRef}
160
+ style={{ width: size, height: size, display: "block" }}
161
+ />
162
+ );
163
+ }
@@ -0,0 +1,76 @@
1
+ "use client";
2
+
3
+ import { memo, type CSSProperties, type ReactNode } from "react";
4
+
5
+ import { cn } from "../../lib/utils/css";
6
+
7
+ export interface AuroraTextProps {
8
+ children: ReactNode;
9
+ className?: string;
10
+ /**
11
+ * Gradient stops, cycled left→right and looped back to the first. The default
12
+ * is a vivid RGB set (pink → purple → blue → sky) chosen to stay legible on
13
+ * BOTH light and dark backgrounds — the text is painted with these colors
14
+ * (transparent fill + clipped gradient), so it never depends on the page bg.
15
+ */
16
+ colors?: string[];
17
+ /** Animation speed multiplier. 1 ≈ a 10s loop; 2 is twice as fast. */
18
+ speed?: number;
19
+ /** Add a soft, blurred RGB halo behind the text (a subtle aura). */
20
+ glow?: boolean;
21
+ }
22
+
23
+ const DEFAULT_COLORS = ["#FF0080", "#7928CA", "#0070F3", "#38bdf8"];
24
+
25
+ /**
26
+ * Animated RGB gradient text: the gradient slowly sweeps across the glyphs on a
27
+ * loop (via `background-position`, so it's GPU-composited and cheap — safe on
28
+ * mobile). Purely CSS; no JS ticking. The real text is kept in an `sr-only`
29
+ * span so it stays selectable/announced while the visible layer is decorative.
30
+ *
31
+ * @example
32
+ * <AuroraText>Thinking</AuroraText>
33
+ * <AuroraText glow speed={1.4} colors={["#22d3ee", "#a855f7", "#f43f5e"]}>Live</AuroraText>
34
+ */
35
+ export const AuroraText = memo(function AuroraText({
36
+ children,
37
+ className,
38
+ colors = DEFAULT_COLORS,
39
+ speed = 1,
40
+ glow = false,
41
+ }: AuroraTextProps) {
42
+ const gradientStyle: CSSProperties = {
43
+ backgroundImage: `linear-gradient(135deg, ${colors.join(", ")}, ${colors[0]})`,
44
+ // Oversized so the sweep has room to travel before repeating.
45
+ backgroundSize: "200% auto",
46
+ WebkitBackgroundClip: "text",
47
+ backgroundClip: "text",
48
+ WebkitTextFillColor: "transparent",
49
+ color: "transparent",
50
+ // Full `animation` inline (not the `animate-aurora` utility): the keyframe
51
+ // ships in styles.css unconditionally, so the sweep runs even if the
52
+ // consumer's Tailwind never generates that utility class.
53
+ animation: `aurora ${10 / speed}s ease infinite`,
54
+ };
55
+
56
+ return (
57
+ <span className={cn("relative inline-block", className)}>
58
+ {/* Accessible, copyable text; the colored layers below are decorative. */}
59
+ <span className="sr-only">{children}</span>
60
+ {glow ? (
61
+ <span
62
+ aria-hidden="true"
63
+ className="pointer-events-none absolute inset-0 select-none blur-[6px] opacity-60"
64
+ style={gradientStyle}
65
+ >
66
+ {children}
67
+ </span>
68
+ ) : null}
69
+ <span aria-hidden="true" className="relative" style={gradientStyle}>
70
+ {children}
71
+ </span>
72
+ </span>
73
+ );
74
+ });
75
+
76
+ AuroraText.displayName = "AuroraText";
@@ -0,0 +1,296 @@
1
+ "use client";
2
+
3
+ import { Autocomplete as AutocompletePrimitive } from "@base-ui/react/autocomplete";
4
+ import { ChevronsUpDownIcon, XIcon } from "lucide-react";
5
+ import { Input } from "./input";
6
+ import { ScrollArea } from "./scroll-area";
7
+ import { cn } from "../../lib/utils/css";
8
+
9
+ const Autocomplete = AutocompletePrimitive.Root;
10
+
11
+ function AutocompleteInput({
12
+ className,
13
+ showTrigger = false,
14
+ showClear = false,
15
+ startAddon,
16
+ size,
17
+ ...props
18
+ }: Omit<AutocompletePrimitive.Input.Props, "size"> & {
19
+ showTrigger?: boolean;
20
+ showClear?: boolean;
21
+ startAddon?: React.ReactNode;
22
+ size?: "sm" | "default" | "lg" | number;
23
+ ref?: React.Ref<HTMLInputElement>;
24
+ }) {
25
+ const sizeValue = (size ?? "default") as "sm" | "default" | "lg" | number;
26
+
27
+ return (
28
+ <div className="relative not-has-[>*.w-full]:w-fit w-full text-foreground has-disabled:opacity-64">
29
+ {startAddon && (
30
+ <div
31
+ aria-hidden="true"
32
+ className="[&_svg]:-mx-0.5 pointer-events-none absolute inset-y-0 start-px z-10 flex items-center ps-[calc(--spacing(3)-1px)] opacity-80 has-[+[data-size=sm]]:ps-[calc(--spacing(2.5)-1px)] [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4"
33
+ data-slot="autocomplete-start-addon"
34
+ >
35
+ {startAddon}
36
+ </div>
37
+ )}
38
+ <AutocompletePrimitive.Input
39
+ className={cn(
40
+ startAddon &&
41
+ "data-[size=sm]:*:data-[slot=autocomplete-input]:ps-[calc(--spacing(7.5)-1px)] *:data-[slot=autocomplete-input]:ps-[calc(--spacing(8.5)-1px)] sm:data-[size=sm]:*:data-[slot=autocomplete-input]:ps-[calc(--spacing(7)-1px)] sm:*:data-[slot=autocomplete-input]:ps-[calc(--spacing(8)-1px)]",
42
+ sizeValue === "sm"
43
+ ? "has-[+[data-slot=autocomplete-trigger],+[data-slot=autocomplete-clear]]:*:data-[slot=autocomplete-input]:pe-6.5"
44
+ : "has-[+[data-slot=autocomplete-trigger],+[data-slot=autocomplete-clear]]:*:data-[slot=autocomplete-input]:pe-7",
45
+ className,
46
+ )}
47
+ data-slot="autocomplete-input"
48
+ render={<Input nativeInput size={sizeValue} />}
49
+ {...props}
50
+ />
51
+ {showTrigger && (
52
+ <AutocompleteTrigger
53
+ className={cn(
54
+ "-translate-y-1/2 absolute top-1/2 inline-flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-md border border-transparent opacity-80 outline-none transition-colors pointer-coarse:after:absolute pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 hover:opacity-100 has-[+[data-slot=autocomplete-clear]]:hidden sm:size-7 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
55
+ sizeValue === "sm" ? "end-0" : "end-0.5",
56
+ )}
57
+ >
58
+ <ChevronsUpDownIcon />
59
+ </AutocompleteTrigger>
60
+ )}
61
+ {showClear && (
62
+ <AutocompleteClear
63
+ className={cn(
64
+ "-translate-y-1/2 absolute top-1/2 inline-flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-md border border-transparent opacity-80 outline-none transition-colors pointer-coarse:after:absolute pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 hover:opacity-100 has-[+[data-slot=autocomplete-clear]]:hidden sm:size-7 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
65
+ sizeValue === "sm" ? "end-0" : "end-0.5",
66
+ )}
67
+ >
68
+ <XIcon />
69
+ </AutocompleteClear>
70
+ )}
71
+ </div>
72
+ );
73
+ }
74
+
75
+ function AutocompletePopup({
76
+ className,
77
+ children,
78
+ sideOffset = 4,
79
+ ...props
80
+ }: AutocompletePrimitive.Popup.Props & {
81
+ sideOffset?: number;
82
+ }) {
83
+ return (
84
+ <AutocompletePrimitive.Portal>
85
+ <AutocompletePrimitive.Positioner
86
+ className="z-50 select-none"
87
+ data-slot="autocomplete-positioner"
88
+ sideOffset={sideOffset}
89
+ >
90
+ <AutocompletePrimitive.Popup
91
+ className={cn(
92
+ "relative flex max-h-[min(var(--available-height),23rem)] w-(--anchor-width) max-w-(--available-width) origin-(--transform-origin) flex-col rounded-lg border bg-popover not-dark:bg-clip-padding text-foreground shadow-lg/5 transition-opacity duration-150 ease-out before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/6%)] data-ending-style:opacity-0 data-starting-style:opacity-0 dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",
93
+ className,
94
+ )}
95
+ data-slot="autocomplete-popup"
96
+ {...props}
97
+ >
98
+ {children}
99
+ </AutocompletePrimitive.Popup>
100
+ </AutocompletePrimitive.Positioner>
101
+ </AutocompletePrimitive.Portal>
102
+ );
103
+ }
104
+
105
+ function AutocompleteItem({
106
+ className,
107
+ children,
108
+ ...props
109
+ }: AutocompletePrimitive.Item.Props) {
110
+ return (
111
+ <AutocompletePrimitive.Item
112
+ className={cn(
113
+ "flex min-h-8 cursor-default select-none items-center rounded-sm px-2 py-1 text-base outline-none data-disabled:pointer-events-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm",
114
+ className,
115
+ )}
116
+ data-slot="autocomplete-item"
117
+ {...props}
118
+ >
119
+ {children}
120
+ </AutocompletePrimitive.Item>
121
+ );
122
+ }
123
+
124
+ function AutocompleteSeparator({
125
+ className,
126
+ ...props
127
+ }: AutocompletePrimitive.Separator.Props) {
128
+ return (
129
+ <AutocompletePrimitive.Separator
130
+ className={cn("mx-2 my-1 h-px bg-border last:hidden", className)}
131
+ data-slot="autocomplete-separator"
132
+ {...props}
133
+ />
134
+ );
135
+ }
136
+
137
+ function AutocompleteGroup({
138
+ className,
139
+ ...props
140
+ }: AutocompletePrimitive.Group.Props) {
141
+ return (
142
+ <AutocompletePrimitive.Group
143
+ className={cn("[[role=group]+&]:mt-1.5", className)}
144
+ data-slot="autocomplete-group"
145
+ {...props}
146
+ />
147
+ );
148
+ }
149
+
150
+ function AutocompleteGroupLabel({
151
+ className,
152
+ ...props
153
+ }: AutocompletePrimitive.GroupLabel.Props) {
154
+ return (
155
+ <AutocompletePrimitive.GroupLabel
156
+ className={cn(
157
+ "px-2 py-1.5 font-medium text-muted-foreground text-xs",
158
+ className,
159
+ )}
160
+ data-slot="autocomplete-group-label"
161
+ {...props}
162
+ />
163
+ );
164
+ }
165
+
166
+ function AutocompleteEmpty({
167
+ className,
168
+ ...props
169
+ }: AutocompletePrimitive.Empty.Props) {
170
+ return (
171
+ <AutocompletePrimitive.Empty
172
+ className={cn(
173
+ "not-empty:p-2 text-center text-base text-muted-foreground sm:text-sm",
174
+ className,
175
+ )}
176
+ data-slot="autocomplete-empty"
177
+ {...props}
178
+ />
179
+ );
180
+ }
181
+
182
+ function AutocompleteRow({
183
+ className,
184
+ ...props
185
+ }: AutocompletePrimitive.Row.Props) {
186
+ return (
187
+ <AutocompletePrimitive.Row
188
+ className={className}
189
+ data-slot="autocomplete-row"
190
+ {...props}
191
+ />
192
+ );
193
+ }
194
+
195
+ function AutocompleteValue({ ...props }: AutocompletePrimitive.Value.Props) {
196
+ return (
197
+ <AutocompletePrimitive.Value data-slot="autocomplete-value" {...props} />
198
+ );
199
+ }
200
+
201
+ function AutocompleteList({
202
+ className,
203
+ ...props
204
+ }: AutocompletePrimitive.List.Props) {
205
+ return (
206
+ <ScrollArea scrollbarGutter scrollFade className="grow">
207
+ <AutocompletePrimitive.List
208
+ className={cn(
209
+ "not-empty:scroll-py-1 not-empty:p-1 in-data-has-overflow-y:pe-3",
210
+ className,
211
+ )}
212
+ data-slot="autocomplete-list"
213
+ {...props}
214
+ />
215
+ </ScrollArea>
216
+ );
217
+ }
218
+
219
+ function AutocompleteClear({
220
+ className,
221
+ ...props
222
+ }: AutocompletePrimitive.Clear.Props) {
223
+ return (
224
+ <AutocompletePrimitive.Clear
225
+ className={cn(
226
+ "-translate-y-1/2 absolute end-0.5 top-1/2 inline-flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-md border border-transparent opacity-80 outline-none transition-[color,background-color,box-shadow,opacity] pointer-coarse:after:absolute pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 hover:opacity-100 sm:size-7 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
227
+ className,
228
+ )}
229
+ data-slot="autocomplete-clear"
230
+ {...props}
231
+ >
232
+ <XIcon />
233
+ </AutocompletePrimitive.Clear>
234
+ );
235
+ }
236
+
237
+ function AutocompleteStatus({
238
+ className,
239
+ ...props
240
+ }: AutocompletePrimitive.Status.Props) {
241
+ return (
242
+ <AutocompletePrimitive.Status
243
+ className={cn(
244
+ "px-3 py-2 font-medium text-muted-foreground text-xs empty:m-0 empty:p-0",
245
+ className,
246
+ )}
247
+ data-slot="autocomplete-status"
248
+ {...props}
249
+ />
250
+ );
251
+ }
252
+
253
+ function AutocompleteCollection({
254
+ ...props
255
+ }: AutocompletePrimitive.Collection.Props) {
256
+ return (
257
+ <AutocompletePrimitive.Collection
258
+ data-slot="autocomplete-collection"
259
+ {...props}
260
+ />
261
+ );
262
+ }
263
+
264
+ function AutocompleteTrigger({
265
+ className,
266
+ ...props
267
+ }: AutocompletePrimitive.Trigger.Props) {
268
+ return (
269
+ <AutocompletePrimitive.Trigger
270
+ className={className}
271
+ data-slot="autocomplete-trigger"
272
+ {...props}
273
+ />
274
+ );
275
+ }
276
+
277
+ const useAutocompleteFilter = AutocompletePrimitive.useFilter;
278
+
279
+ export {
280
+ Autocomplete,
281
+ AutocompleteInput,
282
+ AutocompleteTrigger,
283
+ AutocompletePopup,
284
+ AutocompleteItem,
285
+ AutocompleteSeparator,
286
+ AutocompleteGroup,
287
+ AutocompleteGroupLabel,
288
+ AutocompleteEmpty,
289
+ AutocompleteValue,
290
+ AutocompleteList,
291
+ AutocompleteClear,
292
+ AutocompleteStatus,
293
+ AutocompleteRow,
294
+ AutocompleteCollection,
295
+ useAutocompleteFilter,
296
+ };
@@ -0,0 +1,150 @@
1
+ "use client";
2
+
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+ import type * as React from "react";
5
+
6
+ import { cn } from "../../lib/utils/css";
7
+ import { Avatar, avatarVariants } from "./avatar";
8
+ import {
9
+ Tooltip,
10
+ TooltipPopup,
11
+ TooltipProvider,
12
+ TooltipTrigger,
13
+ } from "./tooltip";
14
+
15
+ type AvatarSize = NonNullable<VariantProps<typeof avatarVariants>["size"]>;
16
+
17
+ const avatarGroupVariants = cva(
18
+ // Each child gets a ring in the page color so overlapping avatars stay
19
+ // visually separated; hover lifts one to the front.
20
+ "isolate flex items-center *:relative *:rounded-full *:ring-2 *:ring-background *:transition-transform hover:*:z-10",
21
+ {
22
+ defaultVariants: { overlap: "md" },
23
+ variants: {
24
+ overlap: {
25
+ sm: "*:not-first:-ml-1.5",
26
+ md: "*:not-first:-ml-2.5",
27
+ lg: "*:not-first:-ml-3.5",
28
+ },
29
+ },
30
+ },
31
+ );
32
+
33
+ interface AvatarGroupItem {
34
+ src?: string;
35
+ /** Initials shown when there is no image. */
36
+ initials?: React.ReactNode;
37
+ alt?: string;
38
+ /**
39
+ * Label revealed on hover when `showNameOnHover` is set (e.g. the person's
40
+ * name). Falls back to `alt` when omitted.
41
+ */
42
+ name?: React.ReactNode;
43
+ }
44
+
45
+ interface AvatarGroupProps
46
+ extends React.ComponentProps<"div">,
47
+ VariantProps<typeof avatarGroupVariants> {
48
+ /** Data-driven avatars. Omit to compose `<Avatar>` children yourself. */
49
+ items?: AvatarGroupItem[];
50
+ /** Cap the visible avatars; the rest collapse into a "+N" chip. */
51
+ max?: number;
52
+ size?: AvatarSize;
53
+ /**
54
+ * Reveal each avatar's `name` (or `alt`) in a tooltip above it on hover /
55
+ * focus. Only applies to the data-driven `items` path.
56
+ */
57
+ showNameOnHover?: boolean;
58
+ }
59
+
60
+ /**
61
+ * A row of overlapping avatars with an overflow "+N" chip. Pass `items` for the
62
+ * data-driven path, or compose `<Avatar>` children directly for full control.
63
+ *
64
+ * With `showNameOnHover`, hovering (or focusing) an avatar reveals its `name`
65
+ * in a tooltip — e.g. hovering Amir's photo shows "Amir".
66
+ */
67
+ function AvatarGroup({
68
+ className,
69
+ items,
70
+ max,
71
+ size = "sm",
72
+ overlap,
73
+ showNameOnHover = false,
74
+ children,
75
+ ...props
76
+ }: AvatarGroupProps) {
77
+ const shown = items && max ? items.slice(0, max) : items;
78
+ const overflow = items && max ? items.length - max : 0;
79
+
80
+ const group = (
81
+ <div
82
+ className={cn(avatarGroupVariants({ overlap }), className)}
83
+ data-slot="avatar-group"
84
+ role="group"
85
+ {...props}
86
+ >
87
+ {items
88
+ ? shown?.map((item, i) => {
89
+ const label = item.name ?? item.alt;
90
+ const avatar = (
91
+ <Avatar
92
+ alt={item.alt}
93
+ initials={item.initials}
94
+ size={size}
95
+ src={item.src}
96
+ />
97
+ );
98
+ if (!(showNameOnHover && label)) {
99
+ // biome-ignore lint/suspicious/noArrayIndexKey: order is stable
100
+ return <Avatar alt={item.alt} initials={item.initials} key={i} size={size} src={item.src} />;
101
+ }
102
+ return (
103
+ // The span wrapper is the group's direct child, so it carries the
104
+ // ring/overlap styling; the tooltip anchors to it (Base UI needs a
105
+ // ref-able host, which the Avatar fn component doesn't forward).
106
+ // biome-ignore lint/suspicious/noArrayIndexKey: order is stable
107
+ <Tooltip key={i}>
108
+ <TooltipTrigger
109
+ className="inline-flex cursor-default"
110
+ render={<span />}
111
+ >
112
+ {avatar}
113
+ </TooltipTrigger>
114
+ <TooltipPopup>{label}</TooltipPopup>
115
+ </Tooltip>
116
+ );
117
+ })
118
+ : children}
119
+ {overflow > 0 ? (
120
+ <span
121
+ aria-label={`${overflow} more`}
122
+ className={cn(
123
+ avatarVariants({ size }),
124
+ // Solid neutral fill so the chip reads as a distinct surface in both
125
+ // themes — the `bg-muted` token is only 4% opacity (near-transparent).
126
+ "bg-stone-200 font-medium text-stone-600 dark:bg-stone-700 dark:text-stone-200",
127
+ )}
128
+ data-slot="avatar-group-overflow"
129
+ >
130
+ +{overflow}
131
+ </span>
132
+ ) : null}
133
+ </div>
134
+ );
135
+
136
+ // Self-contained: provide a tooltip context so the hover mode works without
137
+ // the consumer having to mount a TooltipProvider. A short delay keeps names
138
+ // from flickering as the pointer sweeps across the row.
139
+ if (showNameOnHover) {
140
+ return (
141
+ <TooltipProvider delay={150} closeDelay={0}>
142
+ {group}
143
+ </TooltipProvider>
144
+ );
145
+ }
146
+
147
+ return group;
148
+ }
149
+
150
+ export { AvatarGroup, avatarGroupVariants };