@playfast/reform-better-hub-ui-primitives 0.2.1 → 0.3.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.
@@ -1,249 +1,234 @@
1
- // Adapted from better-hub by Better Auth — https://github.com/better-auth/better-hub
2
- // (apps/web/src/components/ui/github-background.tsx). Full credit to the Better Auth team.
3
-
4
- import type * as React from "react";
5
- import { useMemo } from "react";
6
-
7
- const RNG_MULTIPLIER = 16807;
8
- const RNG_MODULUS = 2147483647;
9
- const RNG_DIVISOR = 2147483646;
10
- const RNG_SEED = 42;
11
- const WEEKS = 52;
12
- const DAYS_PER_WEEK = 7;
13
- const CELL = 15;
14
- const CELL_RECT_SIZE = 12;
15
- const CELL_ANIM_WEEK_DELAY = 0.04;
16
- const CELL_ANIM_DAY_DELAY = 0.015;
1
+ import type * as React from 'react'
2
+ import { useMemo } from 'react'
3
+
4
+ const RNG_MULTIPLIER = 16807
5
+ const RNG_MODULUS = 2147483647
6
+ const RNG_DIVISOR = 2147483646
7
+ const RNG_SEED = 42
8
+ const WEEKS = 52
9
+ const DAYS_PER_WEEK = 7
10
+ const CELL = 15
11
+ const CELL_RECT_SIZE = 12
12
+ const CELL_ANIM_WEEK_DELAY = 0.04
13
+ const CELL_ANIM_DAY_DELAY = 0.015
17
14
 
18
15
  const LEVELS = [
19
- "rgba(255,255,255,0.02)",
20
- "rgba(255,255,255,0.10)",
21
- "rgba(255,255,255,0.18)",
22
- "rgba(255,255,255,0.28)",
23
- "rgba(255,255,255,0.40)",
24
- ];
16
+ 'rgba(255,255,255,0.02)',
17
+ 'rgba(255,255,255,0.10)',
18
+ 'rgba(255,255,255,0.18)',
19
+ 'rgba(255,255,255,0.28)',
20
+ 'rgba(255,255,255,0.40)',
21
+ ]
25
22
 
26
- const LEVEL_THRESHOLDS = [0.3, 0.5, 0.7, 0.85];
23
+ const LEVEL_THRESHOLDS = [0.3, 0.5, 0.7, 0.85]
27
24
 
28
25
  const COMMIT_GROUPS = [
29
- {
30
- prefix: "m",
31
- cx: 200,
32
- radius: 4,
33
- fill: "rgba(255,255,255,0.25)",
34
- ys: [80, 150, 230, 300, 400, 480, 550, 620, 690, 770, 850],
35
- animBase: 0.5,
36
- animStep: 0.15,
37
- },
38
- {
39
- prefix: "f1",
40
- cx: 280,
41
- radius: 3.5,
42
- fill: "rgba(255,255,255,0.20)",
43
- ys: [230, 280, 330],
44
- animBase: 1.0,
45
- animStep: 0.2,
46
- },
47
- {
48
- prefix: "f2",
49
- cx: 130,
50
- radius: 3.5,
51
- fill: "rgba(255,255,255,0.18)",
52
- ys: [360, 420, 470],
53
- animBase: 1.5,
54
- animStep: 0.2,
55
- },
56
- {
57
- prefix: "hf",
58
- cx: 260,
59
- radius: 3,
60
- fill: "rgba(255,255,255,0.15)",
61
- ys: [600, 630, 660],
62
- animBase: 2.0,
63
- animStep: 0.2,
64
- },
65
- {
66
- prefix: "f3",
67
- cx: 110,
68
- radius: 3,
69
- fill: "rgba(255,255,255,0.12)",
70
- ys: [680, 730, 780],
71
- animBase: 2.4,
72
- animStep: 0.2,
73
- },
74
- ];
75
-
76
- const nextSeed = (seed: number) => (seed * RNG_MULTIPLIER) % RNG_MODULUS;
77
- const seedToUnit = (seed: number) => (seed - 1) / RNG_DIVISOR;
26
+ {
27
+ prefix: 'm',
28
+ cx: 200,
29
+ radius: 4,
30
+ fill: 'rgba(255,255,255,0.25)',
31
+ ys: [80, 150, 230, 300, 400, 480, 550, 620, 690, 770, 850],
32
+ animBase: 0.5,
33
+ animStep: 0.15,
34
+ },
35
+ {
36
+ prefix: 'f1',
37
+ cx: 280,
38
+ radius: 3.5,
39
+ fill: 'rgba(255,255,255,0.20)',
40
+ ys: [230, 280, 330],
41
+ animBase: 1.0,
42
+ animStep: 0.2,
43
+ },
44
+ {
45
+ prefix: 'f2',
46
+ cx: 130,
47
+ radius: 3.5,
48
+ fill: 'rgba(255,255,255,0.18)',
49
+ ys: [360, 420, 470],
50
+ animBase: 1.5,
51
+ animStep: 0.2,
52
+ },
53
+ {
54
+ prefix: 'hf',
55
+ cx: 260,
56
+ radius: 3,
57
+ fill: 'rgba(255,255,255,0.15)',
58
+ ys: [600, 630, 660],
59
+ animBase: 2.0,
60
+ animStep: 0.2,
61
+ },
62
+ {
63
+ prefix: 'f3',
64
+ cx: 110,
65
+ radius: 3,
66
+ fill: 'rgba(255,255,255,0.12)',
67
+ ys: [680, 730, 780],
68
+ animBase: 2.4,
69
+ animStep: 0.2,
70
+ },
71
+ ]
72
+
73
+ const nextSeed = (seed: number) => (seed * RNG_MULTIPLIER) % RNG_MODULUS
74
+ const seedToUnit = (seed: number) => (seed - 1) / RNG_DIVISOR
78
75
  const levelForValue = (sample: number) =>
79
- LEVEL_THRESHOLDS.filter((threshold) => sample >= threshold).length;
76
+ LEVEL_THRESHOLDS.filter((threshold) => sample >= threshold).length
80
77
 
81
- type Cell = { week: number; day: number; level: number };
78
+ type Cell = { week: number; day: number; level: number }
82
79
 
83
80
  export function GitHubBackground(): React.JSX.Element {
84
- const grid = useMemo<Cell[]>(
85
- () =>
86
- Array.from({ length: WEEKS * DAYS_PER_WEEK }).reduce<{
87
- seed: number;
88
- cells: Cell[];
89
- }>(
90
- (acc, _slot, flatIndex) => {
91
- const seed = nextSeed(acc.seed);
92
- const level = levelForValue(seedToUnit(seed));
93
- const week = Math.floor(flatIndex / DAYS_PER_WEEK);
94
- const day = flatIndex % DAYS_PER_WEEK;
95
- return { seed, cells: [...acc.cells, { week, day, level }] };
96
- },
97
- { seed: RNG_SEED, cells: [] },
98
- ).cells,
99
- [],
100
- );
101
-
102
- return (
103
- <div
104
- className="absolute inset-0 pointer-events-none overflow-hidden"
105
- style={{ zIndex: 2 }}
106
- >
107
- {/* Radial mask — fades edges into the shader */}
108
- <div
109
- className="absolute inset-0"
110
- style={{
111
- maskImage: "radial-gradient(ellipse 85% 75% at 45% 35%, black 15%, transparent 60%)",
112
- WebkitMaskImage:
113
- "radial-gradient(ellipse 85% 75% at 45% 35%, black 15%, transparent 60%)",
114
- }}
115
- >
116
- {/* ── Contribution Graph ── */}
117
- <div
118
- className="absolute"
119
- style={{
120
- top: "8%",
121
- left: "50%",
122
- transform: "translate(-50%, 0) perspective(500px) rotateX(30deg) rotateZ(-2deg) scale(1.3)",
123
- transformOrigin: "center top",
124
- opacity: 0.4,
125
- }}
126
- >
127
- <svg
128
- width={WEEKS * CELL}
129
- height={DAYS_PER_WEEK * CELL}
130
- viewBox={`0 0 ${WEEKS * CELL} ${DAYS_PER_WEEK * CELL}`}
131
- >
132
- {grid.map((cell, index) => (
133
- <rect
134
- key={index}
135
- x={cell.week * CELL}
136
- y={cell.day * CELL}
137
- width={CELL_RECT_SIZE}
138
- height={CELL_RECT_SIZE}
139
- rx={2}
140
- fill={LEVELS[cell.level]}
141
- opacity={0}
142
- >
143
- <animate
144
- attributeName="opacity"
145
- from="0"
146
- to="1"
147
- dur="0.5s"
148
- begin={`${cell.week * CELL_ANIM_WEEK_DELAY + cell.day * CELL_ANIM_DAY_DELAY}s`}
149
- fill="freeze"
150
- />
151
- </rect>
152
- ))}
153
- </svg>
154
- </div>
155
-
156
- {/* ── Git Network Graph ── */}
157
- <svg
158
- className="absolute inset-0 w-full h-full"
159
- viewBox="0 0 400 900"
160
- preserveAspectRatio="xMidYMid slice"
161
- style={{ opacity: 0.45 }}
162
- >
163
- {/* Main branch */}
164
- <line
165
- x1="200"
166
- y1="0"
167
- x2="200"
168
- y2="900"
169
- stroke="rgba(255,255,255,0.15)"
170
- strokeWidth="2"
171
- pathLength={1}
172
- className="git-line-anim"
173
- style={{ animationDelay: "0.3s" }}
174
- />
175
-
176
- {/* Feature branch 1 — forks right */}
177
- <path
178
- d="M 200 150 C 200 180, 280 200, 280 230 L 280 350 C 280 380, 200 395, 200 400"
179
- stroke="rgba(255,255,255,0.12)"
180
- strokeWidth="2"
181
- fill="none"
182
- pathLength={1}
183
- className="git-line-anim"
184
- style={{ animationDelay: "0.8s" }}
185
- />
186
-
187
- {/* Feature branch 2 — forks left */}
188
- <path
189
- d="M 200 300 C 200 325, 130 340, 130 360 L 130 500 C 130 525, 200 540, 200 550"
190
- stroke="rgba(255,255,255,0.10)"
191
- strokeWidth="2"
192
- fill="none"
193
- pathLength={1}
194
- className="git-line-anim"
195
- style={{ animationDelay: "1.3s" }}
196
- />
197
-
198
- {/* Hotfix branch — short fork right */}
199
- <path
200
- d="M 200 550 C 200 570, 260 580, 260 600 L 260 650 C 260 670, 200 680, 200 690"
201
- stroke="rgba(255,255,255,0.08)"
202
- strokeWidth="2"
203
- fill="none"
204
- pathLength={1}
205
- className="git-line-anim"
206
- style={{ animationDelay: "1.8s" }}
207
- />
208
-
209
- {/* Feature branch 3 — wide fork left */}
210
- <path
211
- d="M 200 620 C 200 640, 110 660, 110 680 L 110 800 C 110 820, 200 835, 200 850"
212
- stroke="rgba(255,255,255,0.07)"
213
- strokeWidth="2"
214
- fill="none"
215
- pathLength={1}
216
- className="git-line-anim"
217
- style={{ animationDelay: "2.2s" }}
218
- />
219
-
220
- {/* Commit nodes */}
221
- {COMMIT_GROUPS.map((group) =>
222
- group.ys.map((nodeY, index) => (
223
- <circle
224
- key={`${group.prefix}${index}`}
225
- cx={group.cx}
226
- cy={nodeY}
227
- r={group.radius}
228
- fill={group.fill}
229
- opacity={0}
230
- >
231
- <animate
232
- attributeName="opacity"
233
- from="0"
234
- to="1"
235
- dur="0.3s"
236
- begin={`${group.animBase + index * group.animStep}s`}
237
- fill="freeze"
238
- />
239
- </circle>
240
- )),
241
- )}
242
- </svg>
243
- </div>
244
-
245
- {/* CSS for stroke-dash draw animation */}
246
- <style>{`
81
+ const grid = useMemo<Cell[]>(
82
+ () =>
83
+ Array.from({ length: WEEKS * DAYS_PER_WEEK }).reduce<{
84
+ seed: number
85
+ cells: Cell[]
86
+ }>(
87
+ (acc, _slot, flatIndex) => {
88
+ const seed = nextSeed(acc.seed)
89
+ const level = levelForValue(seedToUnit(seed))
90
+ const week = Math.floor(flatIndex / DAYS_PER_WEEK)
91
+ const day = flatIndex % DAYS_PER_WEEK
92
+ return { seed, cells: [...acc.cells, { week, day, level }] }
93
+ },
94
+ { seed: RNG_SEED, cells: [] },
95
+ ).cells,
96
+ [],
97
+ )
98
+
99
+ return (
100
+ <div className="absolute inset-0 pointer-events-none overflow-hidden" style={{ zIndex: 2 }}>
101
+ <div
102
+ className="absolute inset-0"
103
+ style={{
104
+ maskImage: 'radial-gradient(ellipse 85% 75% at 45% 35%, black 15%, transparent 60%)',
105
+ WebkitMaskImage:
106
+ 'radial-gradient(ellipse 85% 75% at 45% 35%, black 15%, transparent 60%)',
107
+ }}
108
+ >
109
+ <div
110
+ className="absolute"
111
+ style={{
112
+ top: '8%',
113
+ left: '50%',
114
+ transform:
115
+ 'translate(-50%, 0) perspective(500px) rotateX(30deg) rotateZ(-2deg) scale(1.3)',
116
+ transformOrigin: 'center top',
117
+ opacity: 0.4,
118
+ }}
119
+ >
120
+ <svg
121
+ width={WEEKS * CELL}
122
+ height={DAYS_PER_WEEK * CELL}
123
+ viewBox={`0 0 ${WEEKS * CELL} ${DAYS_PER_WEEK * CELL}`}
124
+ >
125
+ {grid.map((cell, index) => (
126
+ <rect
127
+ key={index}
128
+ x={cell.week * CELL}
129
+ y={cell.day * CELL}
130
+ width={CELL_RECT_SIZE}
131
+ height={CELL_RECT_SIZE}
132
+ rx={2}
133
+ fill={LEVELS[cell.level]}
134
+ opacity={0}
135
+ >
136
+ <animate
137
+ attributeName="opacity"
138
+ from="0"
139
+ to="1"
140
+ dur="0.5s"
141
+ begin={`${cell.week * CELL_ANIM_WEEK_DELAY + cell.day * CELL_ANIM_DAY_DELAY}s`}
142
+ fill="freeze"
143
+ />
144
+ </rect>
145
+ ))}
146
+ </svg>
147
+ </div>
148
+
149
+ <svg
150
+ className="absolute inset-0 w-full h-full"
151
+ viewBox="0 0 400 900"
152
+ preserveAspectRatio="xMidYMid slice"
153
+ style={{ opacity: 0.45 }}
154
+ >
155
+ <line
156
+ x1="200"
157
+ y1="0"
158
+ x2="200"
159
+ y2="900"
160
+ stroke="rgba(255,255,255,0.15)"
161
+ strokeWidth="2"
162
+ pathLength={1}
163
+ className="git-line-anim"
164
+ style={{ animationDelay: '0.3s' }}
165
+ />
166
+
167
+ <path
168
+ d="M 200 150 C 200 180, 280 200, 280 230 L 280 350 C 280 380, 200 395, 200 400"
169
+ stroke="rgba(255,255,255,0.12)"
170
+ strokeWidth="2"
171
+ fill="none"
172
+ pathLength={1}
173
+ className="git-line-anim"
174
+ style={{ animationDelay: '0.8s' }}
175
+ />
176
+
177
+ <path
178
+ d="M 200 300 C 200 325, 130 340, 130 360 L 130 500 C 130 525, 200 540, 200 550"
179
+ stroke="rgba(255,255,255,0.10)"
180
+ strokeWidth="2"
181
+ fill="none"
182
+ pathLength={1}
183
+ className="git-line-anim"
184
+ style={{ animationDelay: '1.3s' }}
185
+ />
186
+
187
+ <path
188
+ d="M 200 550 C 200 570, 260 580, 260 600 L 260 650 C 260 670, 200 680, 200 690"
189
+ stroke="rgba(255,255,255,0.08)"
190
+ strokeWidth="2"
191
+ fill="none"
192
+ pathLength={1}
193
+ className="git-line-anim"
194
+ style={{ animationDelay: '1.8s' }}
195
+ />
196
+
197
+ <path
198
+ d="M 200 620 C 200 640, 110 660, 110 680 L 110 800 C 110 820, 200 835, 200 850"
199
+ stroke="rgba(255,255,255,0.07)"
200
+ strokeWidth="2"
201
+ fill="none"
202
+ pathLength={1}
203
+ className="git-line-anim"
204
+ style={{ animationDelay: '2.2s' }}
205
+ />
206
+
207
+ {COMMIT_GROUPS.map((group) =>
208
+ group.ys.map((nodeY, index) => (
209
+ <circle
210
+ key={`${group.prefix}${index}`}
211
+ cx={group.cx}
212
+ cy={nodeY}
213
+ r={group.radius}
214
+ fill={group.fill}
215
+ opacity={0}
216
+ >
217
+ <animate
218
+ attributeName="opacity"
219
+ from="0"
220
+ to="1"
221
+ dur="0.3s"
222
+ begin={`${group.animBase + index * group.animStep}s`}
223
+ fill="freeze"
224
+ />
225
+ </circle>
226
+ )),
227
+ )}
228
+ </svg>
229
+ </div>
230
+
231
+ <style>{`
247
232
  .git-line-anim {
248
233
  stroke-dasharray: 1;
249
234
  stroke-dashoffset: 1;
@@ -253,6 +238,6 @@ export function GitHubBackground(): React.JSX.Element {
253
238
  to { stroke-dashoffset: 0; }
254
239
  }
255
240
  `}</style>
256
- </div>
257
- );
241
+ </div>
242
+ )
258
243
  }