@cntyclub/ui-react 0.10.7 → 0.12.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.7",
3
+ "version": "0.12.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,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,6 @@
1
+ // Stock monochrome thinking orb — six hand-tuned dotted animations with
2
+ // automatic light/dark ink (data-theme / .dark class / prefers-color-scheme).
3
+ // Re-exported from the thinking-orbs package so kit consumers get it alongside
4
+ // AuroraOrb without depending on thinking-orbs directly. For the
5
+ // aurora-gradient recolored variant see ./aurora-orb.
6
+ export { ThinkingOrb, type OrbTheme, type ThinkingOrbProps } from "thinking-orbs";
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";
@@ -88,6 +89,7 @@ export * from "./components/ui/table";
88
89
  export * from "./components/ui/tabs";
89
90
  export * from "./components/ui/tag";
90
91
  export { default as TargetCountdown } from "./components/ui/target-countdown";
92
+ export * from "./components/ui/thinking-orb";
91
93
  export * from "./components/ui/timeline";
92
94
  export * from "./components/ui/text-editor";
93
95
  export * from "./components/ui/textarea";