@playfast/reform-better-hub-ui-primitives 0.0.2 → 0.0.4

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.
Files changed (106) hide show
  1. package/package.json +2 -2
  2. package/src/components/agent-icon.tsx +21 -0
  3. package/src/components/badge.tsx +54 -0
  4. package/src/components/button.tsx +72 -0
  5. package/src/components/combobox.tsx +290 -0
  6. package/src/components/command.tsx +170 -0
  7. package/src/components/dialog.tsx +129 -0
  8. package/src/components/dropdown-menu.tsx +149 -0
  9. package/src/components/github-background.tsx +280 -0
  10. package/src/components/halftone-background.tsx +289 -0
  11. package/src/components/input-group.tsx +175 -0
  12. package/src/components/input.tsx +25 -0
  13. package/src/components/live-duration.tsx +70 -0
  14. package/src/components/logo.tsx +20 -0
  15. package/src/components/popover.tsx +82 -0
  16. package/src/components/resize-handle.tsx +86 -0
  17. package/src/components/sheet.tsx +147 -0
  18. package/src/components/textarea.tsx +21 -0
  19. package/src/components/time-ago.tsx +57 -0
  20. package/src/components/tooltip.tsx +64 -0
  21. package/src/foundation/cn.ts +8 -0
  22. package/src/foundation/styles.css +156 -0
  23. package/{dist/index.js → src/index.tsx} +2 -1
  24. package/dist/components/agent-icon.d.ts +0 -5
  25. package/dist/components/agent-icon.d.ts.map +0 -1
  26. package/dist/components/agent-icon.js +0 -5
  27. package/dist/components/agent-icon.js.map +0 -1
  28. package/dist/components/badge.d.ts +0 -11
  29. package/dist/components/badge.d.ts.map +0 -1
  30. package/dist/components/badge.js +0 -28
  31. package/dist/components/badge.js.map +0 -1
  32. package/dist/components/button.d.ts +0 -12
  33. package/dist/components/button.d.ts.map +0 -1
  34. package/dist/components/button.js +0 -34
  35. package/dist/components/button.js.map +0 -1
  36. package/dist/components/combobox.d.ts +0 -25
  37. package/dist/components/combobox.d.ts.map +0 -1
  38. package/dist/components/combobox.js +0 -60
  39. package/dist/components/combobox.js.map +0 -1
  40. package/dist/components/command.d.ts +0 -19
  41. package/dist/components/command.d.ts.map +0 -1
  42. package/dist/components/command.js +0 -36
  43. package/dist/components/command.js.map +0 -1
  44. package/dist/components/dialog.d.ts +0 -16
  45. package/dist/components/dialog.d.ts.map +0 -1
  46. package/dist/components/dialog.js +0 -38
  47. package/dist/components/dialog.js.map +0 -1
  48. package/dist/components/dropdown-menu.d.ts +0 -17
  49. package/dist/components/dropdown-menu.d.ts.map +0 -1
  50. package/dist/components/dropdown-menu.js +0 -35
  51. package/dist/components/dropdown-menu.js.map +0 -1
  52. package/dist/components/github-background.d.ts +0 -3
  53. package/dist/components/github-background.d.ts.map +0 -1
  54. package/dist/components/github-background.js +0 -51
  55. package/dist/components/github-background.js.map +0 -1
  56. package/dist/components/halftone-background.d.ts +0 -3
  57. package/dist/components/halftone-background.d.ts.map +0 -1
  58. package/dist/components/halftone-background.js +0 -248
  59. package/dist/components/halftone-background.js.map +0 -1
  60. package/dist/components/input-group.d.ts +0 -19
  61. package/dist/components/input-group.d.ts.map +0 -1
  62. package/dist/components/input-group.js +0 -63
  63. package/dist/components/input-group.js.map +0 -1
  64. package/dist/components/input.d.ts +0 -5
  65. package/dist/components/input.d.ts.map +0 -1
  66. package/dist/components/input.js +0 -7
  67. package/dist/components/input.js.map +0 -1
  68. package/dist/components/live-duration.d.ts +0 -9
  69. package/dist/components/live-duration.d.ts.map +0 -1
  70. package/dist/components/live-duration.js +0 -55
  71. package/dist/components/live-duration.js.map +0 -1
  72. package/dist/components/logo.d.ts +0 -8
  73. package/dist/components/logo.d.ts.map +0 -1
  74. package/dist/components/logo.js +0 -9
  75. package/dist/components/logo.js.map +0 -1
  76. package/dist/components/popover.d.ts +0 -12
  77. package/dist/components/popover.d.ts.map +0 -1
  78. package/dist/components/popover.js +0 -26
  79. package/dist/components/popover.js.map +0 -1
  80. package/dist/components/resize-handle.d.ts +0 -15
  81. package/dist/components/resize-handle.d.ts.map +0 -1
  82. package/dist/components/resize-handle.js +0 -36
  83. package/dist/components/resize-handle.js.map +0 -1
  84. package/dist/components/sheet.d.ts +0 -18
  85. package/dist/components/sheet.d.ts.map +0 -1
  86. package/dist/components/sheet.js +0 -44
  87. package/dist/components/sheet.js.map +0 -1
  88. package/dist/components/textarea.d.ts +0 -5
  89. package/dist/components/textarea.d.ts.map +0 -1
  90. package/dist/components/textarea.js +0 -7
  91. package/dist/components/textarea.js.map +0 -1
  92. package/dist/components/time-ago.d.ts +0 -8
  93. package/dist/components/time-ago.d.ts.map +0 -1
  94. package/dist/components/time-ago.js +0 -40
  95. package/dist/components/time-ago.js.map +0 -1
  96. package/dist/components/tooltip.d.ts +0 -10
  97. package/dist/components/tooltip.d.ts.map +0 -1
  98. package/dist/components/tooltip.js +0 -25
  99. package/dist/components/tooltip.js.map +0 -1
  100. package/dist/foundation/cn.d.ts +0 -7
  101. package/dist/foundation/cn.d.ts.map +0 -1
  102. package/dist/foundation/cn.js +0 -8
  103. package/dist/foundation/cn.js.map +0 -1
  104. package/dist/index.d.ts +0 -21
  105. package/dist/index.d.ts.map +0 -1
  106. package/dist/index.js.map +0 -1
@@ -0,0 +1,289 @@
1
+ // Adapted from better-hub by Better Auth — https://github.com/better-auth/better-hub
2
+ // (apps/web/src/components/ui/halftone-background.tsx). Full credit to the Better Auth team.
3
+
4
+ import type * as React from "react";
5
+ import { useEffect, useRef } from "react";
6
+
7
+ const VERTEX_SHADER = `
8
+ attribute vec2 a_position;
9
+ void main() {
10
+ gl_Position = vec4(a_position, 0.0, 1.0);
11
+ }`;
12
+
13
+ const FRAGMENT_SHADER = `
14
+ precision highp float;
15
+
16
+ uniform float u_time;
17
+ uniform vec2 u_resolution;
18
+ uniform vec2 u_mouse;
19
+ uniform float u_click;
20
+ uniform vec2 u_clickPos;
21
+
22
+ vec3 mod289(vec3 x) { return x - floor(x * (1.0/289.0)) * 289.0; }
23
+ vec2 mod289(vec2 x) { return x - floor(x * (1.0/289.0)) * 289.0; }
24
+ vec3 permute(vec3 x) { return mod289(((x*34.0)+1.0)*x); }
25
+
26
+ float snoise(vec2 v) {
27
+ const vec4 C = vec4(
28
+ 0.211324865405187,
29
+ 0.366025403784439,
30
+ -0.577350269189626,
31
+ 0.024390243902439
32
+ );
33
+ vec2 i = floor(v + dot(v, C.yy));
34
+ vec2 x0 = v - i + dot(i, C.xx);
35
+ vec2 i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
36
+ vec4 x12 = x0.xyxy + C.xxzz;
37
+ x12.xy -= i1;
38
+ i = mod289(i);
39
+ vec3 p = permute(permute(i.y + vec3(0.0, i1.y, 1.0)) + i.x + vec3(0.0, i1.x, 1.0));
40
+ vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy), dot(x12.zw,x12.zw)), 0.0);
41
+ m = m*m;
42
+ m = m*m;
43
+ vec3 x = 2.0 * fract(p * C.www) - 1.0;
44
+ vec3 h = abs(x) - 0.5;
45
+ vec3 ox = floor(x + 0.5);
46
+ vec3 a0 = x - ox;
47
+ m *= 1.79284291400159 - 0.85373472095314 * (a0*a0 + h*h);
48
+ vec3 g;
49
+ g.x = a0.x * x0.x + h.x * x0.y;
50
+ g.yz = a0.yz * x12.xz + h.yz * x12.yw;
51
+ return 130.0 * dot(m, g);
52
+ }
53
+
54
+ float fbm(vec2 p) {
55
+ float value = 0.0;
56
+ float amp = 0.5;
57
+ float freq = 1.0;
58
+ for (int i = 0; i < 6; i++) {
59
+ value += amp * snoise(p * freq);
60
+ freq *= 2.0;
61
+ amp *= 0.5;
62
+ }
63
+ return value;
64
+ }
65
+
66
+ float hash21(vec2 p) {
67
+ p = fract(p * vec2(233.34, 851.73));
68
+ p += dot(p, p + 23.45);
69
+ return fract(p.x * p.y);
70
+ }
71
+
72
+ void main() {
73
+ vec2 uv = gl_FragCoord.xy / u_resolution.xy;
74
+ float aspect = u_resolution.x / u_resolution.y;
75
+ float time = u_time;
76
+ float t = time * 0.04;
77
+
78
+ vec2 ap = vec2(uv.x * aspect, uv.y);
79
+
80
+ // ═══ SHARED MOUSE WARP ═══
81
+ vec2 scaledP = ap * 2.5;
82
+ vec2 mUV = u_mouse;
83
+ mUV.x *= aspect;
84
+ mUV *= 2.5;
85
+ vec2 mDiff = scaledP - mUV;
86
+ float mDist = length(mDiff);
87
+ vec2 warp = vec2(0.0);
88
+ if (u_mouse.x > 0.0 && mDist < 0.5) {
89
+ warp = normalize(mDiff) * smoothstep(0.5, 0.0, mDist) * 0.2;
90
+ }
91
+ if (u_click > 0.01) {
92
+ vec2 cUV = u_clickPos;
93
+ cUV.x *= aspect;
94
+ cUV *= 2.5;
95
+ vec2 cD = scaledP - cUV;
96
+ float cDist = length(cD);
97
+ warp += normalize(cD + 0.001) * sin(cDist * 12.0 - time * 8.0) * u_click * 0.4 * exp(-cDist * 2.0);
98
+ }
99
+
100
+ // ═══ NOISE LAYER ═══
101
+ vec2 np = scaledP + warp;
102
+ vec2 q = vec2(fbm(np + t * 0.3), fbm(np + vec2(5.2, 1.3) + t * 0.2));
103
+ vec2 r = vec2(
104
+ fbm(np + 4.0 * q + vec2(1.7, 9.2) + t * 0.15),
105
+ fbm(np + 4.0 * q + vec2(8.3, 2.8) + t * 0.12)
106
+ );
107
+ float f = fbm(np + 4.0 * r);
108
+ float noise = clamp(f*f*f + 0.6*f*f + 0.5*f, 0.0, 1.0) * 0.15;
109
+
110
+ // ═══ CONTRIBUTION GRID ═══
111
+ vec2 gp = ap;
112
+ gp += vec2(snoise(ap * 3.0 + t), snoise(ap * 3.0 + t + 50.0)) * 0.004;
113
+ gp += warp * 0.04;
114
+ gp += vec2(t * 0.2, t * 0.12);
115
+
116
+ float cellTotal = 0.025;
117
+ vec2 cellId = floor(gp / cellTotal);
118
+ vec2 cellUV = fract(gp / cellTotal);
119
+
120
+ float pad = 0.10;
121
+ float edge = 0.04;
122
+ float inX = smoothstep(pad - edge, pad + edge, cellUV.x)
123
+ * (1.0 - smoothstep(1.0 - pad - edge, 1.0 - pad + edge, cellUV.x));
124
+ float inY = smoothstep(pad - edge, pad + edge, cellUV.y)
125
+ * (1.0 - smoothstep(1.0 - pad - edge, 1.0 - pad + edge, cellUV.y));
126
+ float inSquare = inX * inY;
127
+
128
+ float hVal = hash21(cellId);
129
+ float level = hVal < 0.35 ? 0.0 : (hVal - 0.35) / 0.65;
130
+ level *= level;
131
+
132
+ vec2 fp = uv - vec2(0.45, 0.55);
133
+ float gFade = max(1.0 - smoothstep(0.15, 0.45, length(fp * vec2(1.3, 1.6))), 0.0);
134
+
135
+ float gridReveal = smoothstep(0.5, 3.0, time);
136
+ float grid = inSquare * level * gFade * gridReveal * 0.10;
137
+
138
+ // ═══ COMPOSE ═══
139
+ vec2 vc = uv - 0.5;
140
+ float vignette = 1.0 - dot(vc, vc) * 0.3;
141
+
142
+ float col = noise * vignette + grid;
143
+ gl_FragColor = vec4(vec3(col), 1.0);
144
+ }`;
145
+
146
+ export function HalftoneBackground(): React.JSX.Element {
147
+ const canvasRef = useRef<HTMLCanvasElement>(null);
148
+ const wrapperRef = useRef<HTMLDivElement>(null);
149
+ const frameRef = useRef<number>(0);
150
+ const mouseRef = useRef({ x: -1, y: -1 });
151
+ const smoothMouseRef = useRef({ x: -1, y: -1 });
152
+ const clickRef = useRef({ strength: 0, x: 0, y: 0 });
153
+
154
+ useEffect(() => {
155
+ const canvas = canvasRef.current;
156
+ const wrapper = wrapperRef.current;
157
+ if (!canvas || !wrapper) return;
158
+
159
+ const gl = canvas.getContext("webgl", {
160
+ alpha: false,
161
+ antialias: false,
162
+ preserveDrawingBuffer: false,
163
+ });
164
+ if (!gl) return;
165
+
166
+ function createShader(type: number, source: string) {
167
+ const shader = gl!.createShader(type)!;
168
+ gl!.shaderSource(shader, source);
169
+ gl!.compileShader(shader);
170
+ return shader;
171
+ }
172
+
173
+ const vs = createShader(gl.VERTEX_SHADER, VERTEX_SHADER);
174
+ const fs = createShader(gl.FRAGMENT_SHADER, FRAGMENT_SHADER);
175
+
176
+ const program = gl.createProgram()!;
177
+ gl.attachShader(program, vs);
178
+ gl.attachShader(program, fs);
179
+ gl.linkProgram(program);
180
+ gl.useProgram(program);
181
+
182
+ const buffer = gl.createBuffer();
183
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
184
+ gl.bufferData(
185
+ gl.ARRAY_BUFFER,
186
+ new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]),
187
+ gl.STATIC_DRAW,
188
+ );
189
+
190
+ const aPosition = gl.getAttribLocation(program, "a_position");
191
+ gl.enableVertexAttribArray(aPosition);
192
+ gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0);
193
+
194
+ const uTime = gl.getUniformLocation(program, "u_time");
195
+ const uResolution = gl.getUniformLocation(program, "u_resolution");
196
+ const uMouse = gl.getUniformLocation(program, "u_mouse");
197
+ const uClick = gl.getUniformLocation(program, "u_click");
198
+ const uClickPos = gl.getUniformLocation(program, "u_clickPos");
199
+
200
+ const startTime = performance.now();
201
+
202
+ const resize = () => {
203
+ const dpr = Math.min(window.devicePixelRatio || 1, 2);
204
+ const scale = 0.5 * dpr;
205
+ canvas.width = canvas.offsetWidth * scale;
206
+ canvas.height = canvas.offsetHeight * scale;
207
+ gl.viewport(0, 0, canvas.width, canvas.height);
208
+ };
209
+
210
+ resize();
211
+ window.addEventListener("resize", resize);
212
+
213
+ const onMouseMove = (e: MouseEvent) => {
214
+ const rect = wrapper.getBoundingClientRect();
215
+ mouseRef.current.x = (e.clientX - rect.left) / rect.width;
216
+ mouseRef.current.y = 1.0 - (e.clientY - rect.top) / rect.height;
217
+ };
218
+
219
+ const onMouseLeave = () => {
220
+ mouseRef.current.x = -1;
221
+ mouseRef.current.y = -1;
222
+ };
223
+
224
+ const onClick = (e: MouseEvent) => {
225
+ const rect = wrapper.getBoundingClientRect();
226
+ clickRef.current.x = (e.clientX - rect.left) / rect.width;
227
+ clickRef.current.y = 1.0 - (e.clientY - rect.top) / rect.height;
228
+ clickRef.current.strength = 1.0;
229
+ };
230
+
231
+ window.addEventListener("mousemove", onMouseMove, { passive: true });
232
+ wrapper.addEventListener("mouseleave", onMouseLeave);
233
+ wrapper.addEventListener("click", onClick);
234
+
235
+ const draw = () => {
236
+ const elapsed = (performance.now() - startTime) / 1000;
237
+
238
+ const lerp = 0.08;
239
+ const target = mouseRef.current;
240
+ const smooth = smoothMouseRef.current;
241
+ if (target.x < 0) {
242
+ smooth.x += (target.x - smooth.x) * 0.02;
243
+ smooth.y += (target.y - smooth.y) * 0.02;
244
+ } else {
245
+ smooth.x += (target.x - smooth.x) * lerp;
246
+ smooth.y += (target.y - smooth.y) * lerp;
247
+ }
248
+
249
+ clickRef.current.strength *= 0.96;
250
+
251
+ gl.uniform1f(uTime, elapsed);
252
+ gl.uniform2f(uResolution, canvas.width, canvas.height);
253
+ gl.uniform2f(uMouse, smooth.x, smooth.y);
254
+ gl.uniform1f(uClick, clickRef.current.strength);
255
+ gl.uniform2f(uClickPos, clickRef.current.x, clickRef.current.y);
256
+ gl.drawArrays(gl.TRIANGLES, 0, 6);
257
+ frameRef.current = requestAnimationFrame(draw);
258
+ };
259
+
260
+ frameRef.current = requestAnimationFrame(draw);
261
+
262
+ return () => {
263
+ cancelAnimationFrame(frameRef.current);
264
+ window.removeEventListener("resize", resize);
265
+ window.removeEventListener("mousemove", onMouseMove);
266
+ wrapper.removeEventListener("mouseleave", onMouseLeave);
267
+ wrapper.removeEventListener("click", onClick);
268
+ gl.deleteProgram(program);
269
+ gl.deleteShader(vs);
270
+ gl.deleteShader(fs);
271
+ gl.deleteBuffer(buffer);
272
+ };
273
+ }, []);
274
+
275
+ return (
276
+ <div
277
+ ref={wrapperRef}
278
+ className="absolute inset-0 overflow-hidden"
279
+ style={{ background: "var(--shader-bg)" }}
280
+ aria-hidden="true"
281
+ >
282
+ <canvas
283
+ ref={canvasRef}
284
+ className="w-full h-full"
285
+ style={{ filter: "var(--shader-filter)" }}
286
+ />
287
+ </div>
288
+ );
289
+ }
@@ -0,0 +1,175 @@
1
+ // Adapted from better-hub by Better Auth — https://github.com/better-auth/better-hub
2
+ // (apps/web/src/components/ui/input-group.tsx). Full credit to the Better Auth team.
3
+
4
+ import type * as React from "react";
5
+ import type { ComponentProps } from "react";
6
+ import { cva, type VariantProps } from "class-variance-authority";
7
+ import type { ClassProp } from "class-variance-authority/types";
8
+
9
+ import { cn } from "../foundation/cn";
10
+ import { Button } from "./button";
11
+ import { Input } from "./input";
12
+ import { Textarea } from "./textarea";
13
+
14
+ function InputGroup({ className, ...props }: ComponentProps<"div">): React.JSX.Element {
15
+ return (
16
+ <div
17
+ data-slot="input-group"
18
+ role="group"
19
+ className={cn(
20
+ "group/input-group relative flex w-full items-center rounded-md border border-input shadow-xs transition-[color,box-shadow] outline-none dark:bg-input/30",
21
+ "h-9 min-w-0 has-[>textarea]:h-auto",
22
+
23
+ // Variants based on alignment.
24
+ "has-[>[data-align=inline-start]]:[&>input]:pl-2",
25
+ "has-[>[data-align=inline-end]]:[&>input]:pr-2",
26
+ "has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3",
27
+ "has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3",
28
+
29
+ // Focus state.
30
+ "has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-[3px] has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50",
31
+
32
+ // Error state.
33
+ "has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-destructive/20 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",
34
+
35
+ className,
36
+ )}
37
+ {...props}
38
+ />
39
+ );
40
+ }
41
+
42
+ const inputGroupAddonVariants: (
43
+ props?:
44
+ | ({
45
+ align?: "inline-start" | "inline-end" | "block-start" | "block-end" | null | undefined;
46
+ } & ClassProp)
47
+ | undefined,
48
+ ) => string = cva(
49
+ "flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
50
+ {
51
+ variants: {
52
+ align: {
53
+ "inline-start":
54
+ "order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]",
55
+ "inline-end":
56
+ "order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]",
57
+ "block-start":
58
+ "order-first w-full justify-start px-3 pt-3 group-has-[>input]/input-group:pt-2.5 [.border-b]:pb-3",
59
+ "block-end":
60
+ "order-last w-full justify-start px-3 pb-3 group-has-[>input]/input-group:pb-2.5 [.border-t]:pt-3",
61
+ },
62
+ },
63
+ defaultVariants: {
64
+ align: "inline-start",
65
+ },
66
+ },
67
+ );
68
+
69
+ function InputGroupAddon({
70
+ className,
71
+ align = "inline-start",
72
+ ...props
73
+ }: ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>): React.JSX.Element {
74
+ return (
75
+ <div
76
+ role="group"
77
+ data-slot="input-group-addon"
78
+ data-align={align}
79
+ className={cn(inputGroupAddonVariants({ align }), className)}
80
+ onClick={(e) => {
81
+ if ((e.target as HTMLElement).closest("button")) {
82
+ return;
83
+ }
84
+ e.currentTarget.parentElement?.querySelector("input")?.focus();
85
+ }}
86
+ {...props}
87
+ />
88
+ );
89
+ }
90
+
91
+ const inputGroupButtonVariants: (
92
+ props?:
93
+ | ({
94
+ size?: "xs" | "sm" | "icon-xs" | "icon-sm" | null | undefined;
95
+ } & ClassProp)
96
+ | undefined,
97
+ ) => string = cva("flex items-center gap-2 text-sm shadow-none", {
98
+ variants: {
99
+ size: {
100
+ xs: "h-6 gap-1 rounded-[calc(var(--radius)-5px)] px-2 has-[>svg]:px-2 [&>svg:not([class*='size-'])]:size-3.5",
101
+ sm: "h-8 gap-1.5 rounded-md px-2.5 has-[>svg]:px-2.5",
102
+ "icon-xs": "size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
103
+ "icon-sm": "size-8 p-0 has-[>svg]:p-0",
104
+ },
105
+ },
106
+ defaultVariants: {
107
+ size: "xs",
108
+ },
109
+ });
110
+
111
+ function InputGroupButton({
112
+ className,
113
+ type = "button",
114
+ variant = "ghost",
115
+ size = "xs",
116
+ ...props
117
+ }: Omit<ComponentProps<typeof Button>, "size"> &
118
+ VariantProps<typeof inputGroupButtonVariants>): React.JSX.Element {
119
+ return (
120
+ <Button
121
+ type={type}
122
+ data-size={size}
123
+ variant={variant}
124
+ className={cn(inputGroupButtonVariants({ size }), className)}
125
+ {...props}
126
+ />
127
+ );
128
+ }
129
+
130
+ function InputGroupText({ className, ...props }: ComponentProps<"span">): React.JSX.Element {
131
+ return (
132
+ <span
133
+ className={cn(
134
+ "flex items-center gap-2 text-sm text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
135
+ className,
136
+ )}
137
+ {...props}
138
+ />
139
+ );
140
+ }
141
+
142
+ function InputGroupInput({ className, ...props }: ComponentProps<"input">): React.JSX.Element {
143
+ return (
144
+ <Input
145
+ data-slot="input-group-control"
146
+ className={cn(
147
+ "flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent",
148
+ className,
149
+ )}
150
+ {...props}
151
+ />
152
+ );
153
+ }
154
+
155
+ function InputGroupTextarea({ className, ...props }: ComponentProps<"textarea">): React.JSX.Element {
156
+ return (
157
+ <Textarea
158
+ data-slot="input-group-control"
159
+ className={cn(
160
+ "flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent",
161
+ className,
162
+ )}
163
+ {...props}
164
+ />
165
+ );
166
+ }
167
+
168
+ export {
169
+ InputGroup,
170
+ InputGroupAddon,
171
+ InputGroupButton,
172
+ InputGroupText,
173
+ InputGroupInput,
174
+ InputGroupTextarea,
175
+ };
@@ -0,0 +1,25 @@
1
+ // Adapted from better-hub by Better Auth — https://github.com/better-auth/better-hub
2
+ // (apps/web/src/components/ui/input.tsx). Full credit to the Better Auth team.
3
+
4
+ import type * as React from "react";
5
+ import type { ComponentProps } from 'react';
6
+
7
+ import { cn } from '../foundation/cn';
8
+
9
+ function Input({ className, type, ...props }: ComponentProps<'input'>): React.JSX.Element {
10
+ return (
11
+ <input
12
+ type={type}
13
+ data-slot="input"
14
+ className={cn(
15
+ "h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30",
16
+ "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
17
+ "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
18
+ className,
19
+ )}
20
+ {...props}
21
+ />
22
+ );
23
+ }
24
+
25
+ export { Input };
@@ -0,0 +1,70 @@
1
+ // Adapted from better-hub by Better Auth — https://github.com/better-auth/better-hub
2
+ // (apps/web/src/components/ui/live-duration.tsx). Full credit to the Better Auth team.
3
+
4
+ import type * as React from "react";
5
+ import { useState, useEffect } from "react";
6
+
7
+ // Inlined from "@/lib/utils" (formatDuration) — only this helper is used here.
8
+ function formatDuration(startedAt: string | null, completedAt: string | null): string {
9
+ if (!startedAt) return "";
10
+ const start = new Date(startedAt);
11
+ const end = completedAt ? new Date(completedAt) : new Date();
12
+ const totalSeconds = Math.max(0, Math.floor((end.getTime() - start.getTime()) / 1000));
13
+ const hours = Math.floor(totalSeconds / 3600);
14
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
15
+ const seconds = totalSeconds % 60;
16
+ if (hours > 0) return `${hours}h ${minutes}m`;
17
+ if (minutes > 0) return `${minutes}m ${seconds}s`;
18
+ return `${seconds}s`;
19
+ }
20
+
21
+ // Inlined from "@/lib/live-tick".
22
+ // Shared tick for LiveDuration components. One interval drives all subscribers
23
+ // instead of each component running its own 1s interval.
24
+ const callbacks = new Set<() => void>();
25
+ const tickState: { intervalId: ReturnType<typeof setInterval> | null } = {
26
+ intervalId: null,
27
+ };
28
+
29
+ function subscribeLiveTick(callback: () => void): () => void {
30
+ callbacks.add(callback);
31
+ if (tickState.intervalId === null) {
32
+ tickState.intervalId = setInterval(() => {
33
+ callbacks.forEach((cb) => cb());
34
+ }, 1000);
35
+ }
36
+ return () => {
37
+ callbacks.delete(callback);
38
+ if (callbacks.size === 0 && tickState.intervalId !== null) {
39
+ clearInterval(tickState.intervalId);
40
+ tickState.intervalId = null;
41
+ }
42
+ };
43
+ }
44
+
45
+ interface LiveDurationProps {
46
+ startedAt: string | null;
47
+ completedAt?: string | null;
48
+ className?: string;
49
+ }
50
+
51
+ export function LiveDuration({ startedAt, completedAt, className }: LiveDurationProps): React.ReactNode {
52
+ // tick forces re-renders when subscribeLiveTick fires; value is unused in render
53
+ const [, setTick] = useState(0);
54
+
55
+ useEffect(() => {
56
+ if (!startedAt || completedAt != null) return;
57
+ setTick(0);
58
+ return subscribeLiveTick(() => setTick((t) => t + 1));
59
+ }, [startedAt, completedAt]);
60
+
61
+ if (!startedAt) return null;
62
+
63
+ const formatted = formatDuration(startedAt, completedAt ?? null);
64
+
65
+ return (
66
+ <span className={className} suppressHydrationWarning>
67
+ {formatted}
68
+ </span>
69
+ );
70
+ }
@@ -0,0 +1,20 @@
1
+ // Adapted from better-hub by Better Auth — https://github.com/better-auth/better-hub
2
+ // (apps/web/src/components/ui/logo.tsx). Full credit to the Better Auth team.
3
+ import type * as React from "react";
4
+ import { cn } from '../foundation/cn';
5
+
6
+ export function Logo({ className }: { className?: string }): React.JSX.Element {
7
+ return (
8
+ <span className={cn("font-mono text-sm font-medium tracking-tight", className)}>
9
+ BETTER-HUB.
10
+ </span>
11
+ );
12
+ }
13
+
14
+ export function LogoMark({ className }: { className?: string }): React.JSX.Element {
15
+ return (
16
+ <span className={cn("font-mono text-sm font-bold tracking-tight", className)}>
17
+ b.
18
+ </span>
19
+ );
20
+ }
@@ -0,0 +1,82 @@
1
+ // Adapted from better-hub by Better Auth — https://github.com/better-auth/better-hub
2
+ // (apps/web/src/components/ui/popover.tsx). Full credit to the Better Auth team.
3
+
4
+ import type * as React from "react";
5
+ import type { ComponentProps } from "react";
6
+ import { Popover as PopoverPrimitive } from "radix-ui";
7
+
8
+ import { cn } from "../foundation/cn";
9
+
10
+ function Popover({ ...props }: ComponentProps<typeof PopoverPrimitive.Root>): React.JSX.Element {
11
+ return <PopoverPrimitive.Root data-slot="popover" {...props} />;
12
+ }
13
+
14
+ function PopoverTrigger({ ...props }: ComponentProps<typeof PopoverPrimitive.Trigger>): React.JSX.Element {
15
+ return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
16
+ }
17
+
18
+ function PopoverContent({
19
+ className,
20
+ align = "center",
21
+ sideOffset = 4,
22
+ ...props
23
+ }: ComponentProps<typeof PopoverPrimitive.Content>): React.JSX.Element {
24
+ return (
25
+ <PopoverPrimitive.Portal>
26
+ <PopoverPrimitive.Content
27
+ data-slot="popover-content"
28
+ align={align}
29
+ sideOffset={sideOffset}
30
+ className={cn(
31
+ "z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-hidden data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
32
+ className,
33
+ )}
34
+ {...props}
35
+ />
36
+ </PopoverPrimitive.Portal>
37
+ );
38
+ }
39
+
40
+ function PopoverAnchor({ ...props }: ComponentProps<typeof PopoverPrimitive.Anchor>): React.JSX.Element {
41
+ return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />;
42
+ }
43
+
44
+ function PopoverHeader({ className, ...props }: ComponentProps<"div">): React.JSX.Element {
45
+ return (
46
+ <div
47
+ data-slot="popover-header"
48
+ className={cn("flex flex-col gap-1 text-sm", className)}
49
+ {...props}
50
+ />
51
+ );
52
+ }
53
+
54
+ function PopoverTitle({ className, ...props }: ComponentProps<"h2">): React.JSX.Element {
55
+ return (
56
+ <div
57
+ data-slot="popover-title"
58
+ className={cn("font-medium", className)}
59
+ {...props}
60
+ />
61
+ );
62
+ }
63
+
64
+ function PopoverDescription({ className, ...props }: ComponentProps<"p">): React.JSX.Element {
65
+ return (
66
+ <p
67
+ data-slot="popover-description"
68
+ className={cn("text-muted-foreground", className)}
69
+ {...props}
70
+ />
71
+ );
72
+ }
73
+
74
+ export {
75
+ Popover,
76
+ PopoverTrigger,
77
+ PopoverContent,
78
+ PopoverAnchor,
79
+ PopoverHeader,
80
+ PopoverTitle,
81
+ PopoverDescription,
82
+ };