@cntyclub/ui-react 0.10.3 → 0.10.5

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.3",
3
+ "version": "0.10.5",
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": [
@@ -0,0 +1,73 @@
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
+ animationDuration: `${10 / speed}s`,
51
+ };
52
+
53
+ return (
54
+ <span className={cn("relative inline-block", className)}>
55
+ {/* Accessible, copyable text; the colored layers below are decorative. */}
56
+ <span className="sr-only">{children}</span>
57
+ {glow ? (
58
+ <span
59
+ aria-hidden="true"
60
+ className="animate-aurora pointer-events-none absolute inset-0 select-none blur-[6px] opacity-60"
61
+ style={gradientStyle}
62
+ >
63
+ {children}
64
+ </span>
65
+ ) : null}
66
+ <span aria-hidden="true" className="animate-aurora relative" style={gradientStyle}>
67
+ {children}
68
+ </span>
69
+ </span>
70
+ );
71
+ });
72
+
73
+ AuroraText.displayName = "AuroraText";
@@ -19,7 +19,16 @@ export interface SmoothWheelOptions {
19
19
  * Eased, momentum-style wheel scrolling for any scrollable element. Instead of
20
20
  * jumping `scrollTop`/`scrollLeft` by the raw wheel delta (which feels fast and
21
21
  * abrupt), it animates toward an accumulated target with `requestAnimationFrame`.
22
- * At the start/end edge it lets the wheel bubble so the page can still scroll.
22
+ *
23
+ * It only smooths genuine mouse wheels. Trackpads already scroll smoothly with
24
+ * OS-level inertia, so layering our own easing on top makes it fight the native
25
+ * momentum — the container sticks, or snaps back to where the animation was
26
+ * still heading. In the default vertical mode a trackpad therefore keeps native
27
+ * behaviour; in horizontal mode we still translate a vertical mouse wheel into
28
+ * horizontal scrolling (the whole point of the mode) but let a real horizontal
29
+ * trackpad swipe scroll natively. Every time control returns to native
30
+ * scrolling — trackpad, or an edge — the in-flight animation is cancelled and
31
+ * the target resynced so it can never pull the position back.
23
32
  */
24
33
  export function useSmoothWheel(
25
34
  ref: React.RefObject<HTMLElement | null>,
@@ -47,6 +56,17 @@ export function useSmoothWheel(
47
56
  let target = el[axisProp];
48
57
  let raf = 0;
49
58
  let animating = false;
59
+ let kind: "mouse" | "trackpad" | null = null;
60
+ let lastTs = 0;
61
+
62
+ // Cancel the animation and resync the target to the real position, so any
63
+ // native scrolling that follows is never yanked back to a stale target.
64
+ const releaseToNative = () => {
65
+ if (raf) cancelAnimationFrame(raf);
66
+ raf = 0;
67
+ animating = false;
68
+ target = el[axisProp];
69
+ };
50
70
 
51
71
  const tick = () => {
52
72
  const max = el[sizeProp] - el[clientProp];
@@ -55,6 +75,7 @@ export function useSmoothWheel(
55
75
  const diff = target - current;
56
76
  if (Math.abs(diff) < 0.5) {
57
77
  el[axisProp] = target;
78
+ raf = 0;
58
79
  animating = false;
59
80
  return;
60
81
  }
@@ -62,27 +83,69 @@ export function useSmoothWheel(
62
83
  raf = requestAnimationFrame(tick);
63
84
  };
64
85
 
65
- const normalize = (event: WheelEvent) => {
66
- let delta =
67
- horizontal && event.deltaX !== 0 ? event.deltaX : event.deltaY;
68
- // deltaMode: 0 = pixel, 1 = line, 2 = page.
69
- if (event.deltaMode === 1) delta *= 16;
70
- else if (event.deltaMode === 2) delta *= el[clientProp];
71
- return delta;
86
+ // Strong per-event trackpad signals: fractional deltas, a horizontal
87
+ // component (mice almost never emit deltaX), or small pixel steps.
88
+ const looksLikeTrackpad = (e: WheelEvent) =>
89
+ e.deltaMode === 0 &&
90
+ (!Number.isInteger(e.deltaY) ||
91
+ e.deltaX !== 0 ||
92
+ Math.abs(e.deltaY) < 50);
93
+
94
+ // Sticky, gesture-scoped classification: decide once per burst of events
95
+ // (< 200ms apart) so a hard trackpad flick whose mid-gesture deltas look
96
+ // mouse-like stays classified as a trackpad the whole way through.
97
+ const classify = (e: WheelEvent, now: number): "mouse" | "trackpad" => {
98
+ if (kind === null || now - lastTs > 200) {
99
+ kind = looksLikeTrackpad(e) ? "trackpad" : "mouse";
100
+ } else if (
101
+ kind === "mouse" &&
102
+ (e.deltaX !== 0 || !Number.isInteger(e.deltaY))
103
+ ) {
104
+ kind = "trackpad";
105
+ }
106
+ return kind;
72
107
  };
73
108
 
74
109
  const onWheel = (event: WheelEvent) => {
110
+ if (event.ctrlKey) return; // pinch-zoom
75
111
  const max = el[sizeProp] - el[clientProp];
76
112
  if (max <= 0) return;
77
- if (!animating) target = el[axisProp];
78
113
 
79
- const delta = normalize(event) * multiplier;
114
+ const now = performance.now();
115
+ const source = classify(event, now);
116
+ lastTs = now;
117
+
118
+ if (horizontal) {
119
+ // Native handles a real horizontal swipe; we only translate the
120
+ // vertical wheel (mouse, or a trackpad's vertical swipe) into
121
+ // horizontal motion — that translation is the reason the mode exists.
122
+ if (event.deltaX !== 0 && Math.abs(event.deltaX) >= Math.abs(event.deltaY)) {
123
+ releaseToNative();
124
+ return;
125
+ }
126
+ } else if (source === "trackpad") {
127
+ // Vertical mode: let the OS scroll the trackpad natively.
128
+ releaseToNative();
129
+ return;
130
+ }
131
+
132
+ let delta =
133
+ horizontal && event.deltaX !== 0 ? event.deltaX : event.deltaY;
134
+ // deltaMode: 0 = pixel, 1 = line, 2 = page.
135
+ if (event.deltaMode === 1) delta *= 16;
136
+ else if (event.deltaMode === 2) delta *= el[clientProp];
137
+ delta *= multiplier;
80
138
  if (delta === 0) return;
81
139
 
140
+ if (!animating) target = el[axisProp];
141
+
82
142
  const atStart = target <= 0;
83
143
  const atEnd = target >= max;
84
144
  // Let the wheel bubble at the edges so the page keeps scrolling.
85
- if ((delta < 0 && atStart) || (delta > 0 && atEnd)) return;
145
+ if ((delta < 0 && atStart) || (delta > 0 && atEnd)) {
146
+ releaseToNative();
147
+ return;
148
+ }
86
149
 
87
150
  event.preventDefault();
88
151
  target = Math.max(0, Math.min(target + delta, max));
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-text";
11
12
  export * from "./components/ui/autocomplete";
12
13
  export * from "./components/ui/avatar";
13
14
  export * from "./components/ui/avatar-group";
package/src/styles.css CHANGED
@@ -17,6 +17,7 @@
17
17
  @theme inline {
18
18
  --animate-skeleton: skeleton 2s -1s infinite linear;
19
19
  --animate-qr-scan: qr-scan 2.6s ease-in-out infinite alternate;
20
+ --animate-aurora: aurora 10s ease infinite;
20
21
  --color-warning-foreground: var(--warning-foreground);
21
22
  --color-warning: var(--warning);
22
23
  --color-success-foreground: var(--success-foreground);
@@ -81,6 +82,16 @@
81
82
  transform: translateY(150%);
82
83
  }
83
84
  }
85
+ /* AuroraText: sweep the oversized gradient back and forth across the glyphs. */
86
+ @keyframes aurora {
87
+ 0%,
88
+ 100% {
89
+ background-position: 0% 50%;
90
+ }
91
+ 50% {
92
+ background-position: 100% 50%;
93
+ }
94
+ }
84
95
  --font-sans: "DM Sans", sans-serif;
85
96
  --font-serif: "Fraunces", sans-serif;
86
97
  }