@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.
- package/dist/chunk-2HQPCV2L.js +895 -0
- package/dist/chunk-2HQPCV2L.js.map +1 -0
- package/dist/form.d.ts +175 -0
- package/dist/form.js +356 -0
- package/dist/form.js.map +1 -0
- package/dist/index.d.ts +2162 -0
- package/dist/index.js +11640 -0
- package/dist/index.js.map +1 -0
- package/dist/input-JCZs61MO.d.ts +24 -0
- package/package.json +90 -0
- package/src/components/form/checkbox-group-field.tsx +101 -0
- package/src/components/form/date-field.tsx +79 -0
- package/src/components/form/date-range-field.tsx +106 -0
- package/src/components/form/form-context.ts +10 -0
- package/src/components/form/form.tsx +54 -0
- package/src/components/form/number-field.tsx +69 -0
- package/src/components/form/select-field.tsx +76 -0
- package/src/components/form/submit-button.tsx +28 -0
- package/src/components/form/text-field.tsx +107 -0
- package/src/components/layout/dashboard-header.tsx +54 -0
- package/src/components/layout/dashboard-panel.tsx +34 -0
- package/src/components/layout/dashboard-topbar.tsx +221 -0
- package/src/components/theme-provider.tsx +403 -0
- package/src/components/ui/accordion.tsx +69 -0
- package/src/components/ui/alert-dialog.tsx +169 -0
- package/src/components/ui/alert.tsx +80 -0
- package/src/components/ui/animated-counter.tsx +109 -0
- package/src/components/ui/animated-theme-toggler.tsx +265 -0
- package/src/components/ui/app-store-buttons.tsx +182 -0
- package/src/components/ui/aspect-ratio.tsx +23 -0
- package/src/components/ui/aurora-orb.tsx +163 -0
- package/src/components/ui/aurora-text.tsx +76 -0
- package/src/components/ui/autocomplete.tsx +296 -0
- package/src/components/ui/avatar-group.tsx +150 -0
- package/src/components/ui/avatar.tsx +285 -0
- package/src/components/ui/badge-group.tsx +160 -0
- package/src/components/ui/badge.tsx +172 -0
- package/src/components/ui/breadcrumb.tsx +112 -0
- package/src/components/ui/button.tsx +77 -0
- package/src/components/ui/calendar.tsx +137 -0
- package/src/components/ui/card.tsx +244 -0
- package/src/components/ui/carousel.tsx +258 -0
- package/src/components/ui/chart.tsx +379 -0
- package/src/components/ui/chat-divider.tsx +67 -0
- package/src/components/ui/chat.tsx +494 -0
- package/src/components/ui/checkbox-group.tsx +16 -0
- package/src/components/ui/checkbox.tsx +82 -0
- package/src/components/ui/collapsible.tsx +45 -0
- package/src/components/ui/color-picker.tsx +389 -0
- package/src/components/ui/combobox.tsx +411 -0
- package/src/components/ui/command.tsx +264 -0
- package/src/components/ui/confetti.tsx +196 -0
- package/src/components/ui/context-menu.tsx +271 -0
- package/src/components/ui/context-meter.tsx +134 -0
- package/src/components/ui/credit-card.tsx +214 -0
- package/src/components/ui/data-table-paged.tsx +163 -0
- package/src/components/ui/date-picker.tsx +154 -0
- package/src/components/ui/dialog.tsx +196 -0
- package/src/components/ui/drawer.tsx +135 -0
- package/src/components/ui/empty.tsx +127 -0
- package/src/components/ui/fab.tsx +45 -0
- package/src/components/ui/featured-icon.tsx +149 -0
- package/src/components/ui/field.tsx +88 -0
- package/src/components/ui/fieldset.tsx +29 -0
- package/src/components/ui/form.tsx +17 -0
- package/src/components/ui/frame.tsx +82 -0
- package/src/components/ui/generic-empty.tsx +142 -0
- package/src/components/ui/group.tsx +97 -0
- package/src/components/ui/help-tip.tsx +94 -0
- package/src/components/ui/horizontal-scroll-fader.tsx +228 -0
- package/src/components/ui/input-group.tsx +102 -0
- package/src/components/ui/input-otp.tsx +96 -0
- package/src/components/ui/input.tsx +66 -0
- package/src/components/ui/item.tsx +198 -0
- package/src/components/ui/kbd.tsx +30 -0
- package/src/components/ui/label.tsx +28 -0
- package/src/components/ui/markdown.tsx +188 -0
- package/src/components/ui/menu.tsx +312 -0
- package/src/components/ui/menubar.tsx +93 -0
- package/src/components/ui/meter.tsx +67 -0
- package/src/components/ui/multi-select.tsx +308 -0
- package/src/components/ui/navigation-menu.tsx +143 -0
- package/src/components/ui/number-field.tsx +160 -0
- package/src/components/ui/pagination-controls.tsx +74 -0
- package/src/components/ui/pagination.tsx +149 -0
- package/src/components/ui/pipeline.tsx +102 -0
- package/src/components/ui/popover.tsx +119 -0
- package/src/components/ui/preview-card.tsx +55 -0
- package/src/components/ui/progress.tsx +289 -0
- package/src/components/ui/qr-code.tsx +150 -0
- package/src/components/ui/radio-group.tsx +103 -0
- package/src/components/ui/resizable.tsx +56 -0
- package/src/components/ui/scroll-area.tsx +90 -0
- package/src/components/ui/scroller.tsx +38 -0
- package/src/components/ui/section-header.tsx +118 -0
- package/src/components/ui/segmented-control.tsx +83 -0
- package/src/components/ui/select.tsx +181 -0
- package/src/components/ui/separator.tsx +23 -0
- package/src/components/ui/sheet.tsx +224 -0
- package/src/components/ui/sidebar.tsx +774 -0
- package/src/components/ui/skeleton.tsx +16 -0
- package/src/components/ui/slider.tsx +108 -0
- package/src/components/ui/smooth-scroll.tsx +206 -0
- package/src/components/ui/social-button.tsx +247 -0
- package/src/components/ui/spinner-on-demand.tsx +32 -0
- package/src/components/ui/spinner.tsx +18 -0
- package/src/components/ui/stat.tsx +187 -0
- package/src/components/ui/stepper.tsx +167 -0
- package/src/components/ui/switch.tsx +56 -0
- package/src/components/ui/table.tsx +126 -0
- package/src/components/ui/tabs.tsx +90 -0
- package/src/components/ui/tag.tsx +242 -0
- package/src/components/ui/target-countdown.tsx +46 -0
- package/src/components/ui/text-editor.tsx +313 -0
- package/src/components/ui/textarea.tsx +51 -0
- package/src/components/ui/thinking-orb.tsx +6 -0
- package/src/components/ui/time-picker.tsx +308 -0
- package/src/components/ui/timeline.tsx +116 -0
- package/src/components/ui/toast.tsx +268 -0
- package/src/components/ui/toggle-group.tsx +101 -0
- package/src/components/ui/toggle.tsx +45 -0
- package/src/components/ui/toolbar.tsx +89 -0
- package/src/components/ui/tooltip.tsx +102 -0
- package/src/components/ui/vertical-scroll-fader.tsx +250 -0
- package/src/components/ui/video-player.tsx +275 -0
- package/src/components/upload/avatar-upload-base.tsx +131 -0
- package/src/components/upload/image-upload-base.tsx +112 -0
- package/src/form.ts +17 -0
- package/src/index.ts +143 -0
- package/src/lib/hooks/use-callback-ref.ts +15 -0
- package/src/lib/hooks/use-first-render.ts +11 -0
- package/src/lib/hooks/use-hover.ts +53 -0
- package/src/lib/hooks/use-is-tab-active.ts +17 -0
- package/src/lib/hooks/use-media-query.ts +164 -0
- package/src/lib/utils/css.ts +6 -0
- package/src/styles.css +329 -0
- package/src/types/helpers.ts +24 -0
- 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 };
|