@cntyclub/ui-react 0.10.6 → 0.11.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cntyclub/ui-react",
3
- "version": "0.10.6",
3
+ "version": "0.11.0",
4
4
  "description": "React component library for the Country Club UI Kit — Base UI primitives styled with the Country Club design system (Tailwind CSS v4)",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -55,6 +55,7 @@
55
55
  "remark-gfm": "^4.0.1",
56
56
  "spin-delay": "^2.0.1",
57
57
  "tailwind-merge": "^3.4.0",
58
+ "thinking-orbs": "0.1.1",
58
59
  "tw-animate-css": "^1.4.0",
59
60
  "use-resize-observer": "^9.1.0",
60
61
  "vaul": "^1.1.2",
@@ -0,0 +1,159 @@
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) => {
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 slowly
84
+ // so the colors drift the way AuroraText's gradient sweep does.
85
+ const angle = t * 0.5;
86
+ const r = size / 2;
87
+ const dx = Math.cos(angle) * r;
88
+ const dy = Math.sin(angle) * r;
89
+ const gradient = ctx.createLinearGradient(r - dx, r - dy, r + dx, r + dy);
90
+ stops.forEach((color, i) => {
91
+ gradient.addColorStop(stops.length === 1 ? 0 : i / (stops.length - 1), color);
92
+ });
93
+ ctx.globalCompositeOperation = "source-in";
94
+ ctx.fillStyle = gradient;
95
+ ctx.fillRect(0, 0, size, size);
96
+ ctx.globalCompositeOperation = "source-over";
97
+ };
98
+
99
+ // Reduced motion: a single still frame, matching the stock component.
100
+ if (typeof matchMedia !== "undefined" && matchMedia("(prefers-reduced-motion: reduce)").matches) {
101
+ paint(0.6);
102
+ return;
103
+ }
104
+
105
+ // Animation loop, paused while the orb is off-screen or the tab is
106
+ // hidden — mirroring the stock ThinkingOrb's behavior.
107
+ let frame = 0;
108
+ let running = false;
109
+ const loop = () => {
110
+ paint((performance.now() / 1000) * rate);
111
+ if (running) frame = requestAnimationFrame(loop);
112
+ };
113
+ const start = () => {
114
+ if (running) return;
115
+ running = true;
116
+ frame = requestAnimationFrame(loop);
117
+ };
118
+ const stop = () => {
119
+ running = false;
120
+ cancelAnimationFrame(frame);
121
+ };
122
+
123
+ paint((performance.now() / 1000) * rate);
124
+ let visible = true;
125
+ const observer =
126
+ typeof IntersectionObserver !== "undefined"
127
+ ? new IntersectionObserver((entries) => {
128
+ const entry = entries[0];
129
+ if (!entry) return;
130
+ visible = entry.isIntersecting;
131
+ if (visible && document.visibilityState !== "hidden") start();
132
+ else stop();
133
+ })
134
+ : null;
135
+ observer?.observe(canvas);
136
+ const onVisibilityChange = () => {
137
+ if (document.visibilityState === "hidden") stop();
138
+ else if (visible) start();
139
+ };
140
+ document.addEventListener("visibilitychange", onVisibilityChange);
141
+ if (!observer) start();
142
+
143
+ return () => {
144
+ stop();
145
+ observer?.disconnect();
146
+ document.removeEventListener("visibilitychange", onVisibilityChange);
147
+ };
148
+ }, [colorsKey, size, speed, state]);
149
+
150
+ return (
151
+ <canvas
152
+ aria-hidden="true"
153
+ className={cn("shrink-0", className)}
154
+ data-slot="aurora-orb"
155
+ ref={canvasRef}
156
+ style={{ width: size, height: size, display: "block" }}
157
+ />
158
+ );
159
+ }
@@ -58,7 +58,7 @@ export function ImageUploadBase({
58
58
  )}
59
59
  {...props}
60
60
  >
61
- <div className="flex flex-col relative rounded-md">
61
+ <div className="grow flex flex-col relative rounded-md">
62
62
  <input {...getInputProps()} />
63
63
  <Avatar
64
64
  key={imageUrl ?? "none"}
@@ -71,7 +71,7 @@ export function ImageUploadBase({
71
71
  </Avatar>
72
72
  <div
73
73
  data-active={loading || isDragActive}
74
- className="flex justify-center items-center [--opacity:0] p-8 data-[active=true]:[--opacity:1] peer-has-data-[slot=avatar-fallback]/avatar:[--opacity:1] group-hover/image-upload-root:[--opacity:1] relative z-10 rounded-[inherit] bg-background/[calc(55%*var(--opacity))] backdrop-blur-[calc(var(--blur-xs)*var(--opacity))] gap-3 transition"
74
+ className="grow flex justify-center items-center [--opacity:0] p-8 data-[active=true]:[--opacity:1] peer-has-data-[slot=avatar-fallback]/avatar:[--opacity:1] group-hover/image-upload-root:[--opacity:1] relative z-10 rounded-[inherit] bg-background/[calc(55%*var(--opacity))] backdrop-blur-[calc(var(--blur-xs)*var(--opacity))] gap-3 transition"
75
75
  >
76
76
  <Button
77
77
  size="sm"
package/src/index.ts CHANGED
@@ -8,6 +8,7 @@ export * from "./components/ui/alert-dialog";
8
8
  export * from "./components/ui/animated-theme-toggler";
9
9
  export * from "./components/ui/app-store-buttons";
10
10
  export * from "./components/ui/aspect-ratio";
11
+ export * from "./components/ui/aurora-orb";
11
12
  export * from "./components/ui/aurora-text";
12
13
  export * from "./components/ui/autocomplete";
13
14
  export * from "./components/ui/avatar";