@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/dist/index.d.ts +40 -1
- package/dist/index.js +105 -13
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/components/ui/aurora-orb.tsx +159 -0
- package/src/components/upload/image-upload-base.tsx +2 -2
- package/src/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cntyclub/ui-react",
|
|
3
|
-
"version": "0.
|
|
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";
|