@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,129 @@
1
+ // Adapted from better-hub by Better Auth — https://github.com/better-auth/better-hub
2
+ // (apps/web/src/components/ui/dialog.tsx). Full credit to the Better Auth team.
3
+
4
+ import * as DialogPrimitive from "@radix-ui/react-dialog";
5
+ import { XIcon } from "lucide-react";
6
+ import type * as React from "react";
7
+ import { cn } from "../foundation/cn";
8
+
9
+ function Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>): React.JSX.Element {
10
+ return <DialogPrimitive.Root data-slot="dialog" {...props} />;
11
+ }
12
+
13
+ function DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>): React.JSX.Element {
14
+ return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
15
+ }
16
+
17
+ function DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>): React.JSX.Element {
18
+ return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
19
+ }
20
+
21
+ function DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>): React.JSX.Element {
22
+ return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
23
+ }
24
+
25
+ function DialogOverlay({
26
+ className,
27
+ ...props
28
+ }: React.ComponentProps<typeof DialogPrimitive.Overlay>): React.JSX.Element {
29
+ return (
30
+ <DialogPrimitive.Overlay
31
+ data-slot="dialog-overlay"
32
+ className={cn(
33
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/25 dark:bg-black/70 backdrop-blur-[2px]",
34
+ className,
35
+ )}
36
+ {...props}
37
+ />
38
+ );
39
+ }
40
+
41
+ function DialogContent({
42
+ className,
43
+ children,
44
+ showCloseButton = true,
45
+ ...props
46
+ }: React.ComponentProps<typeof DialogPrimitive.Content> & {
47
+ showCloseButton?: boolean;
48
+ }): React.JSX.Element {
49
+ return (
50
+ <DialogPortal>
51
+ <DialogOverlay />
52
+ <DialogPrimitive.Content
53
+ data-slot="dialog-content"
54
+ className={cn(
55
+ "overflow-y-auto bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] max-h-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
56
+ className,
57
+ )}
58
+ {...props}
59
+ >
60
+ {children}
61
+ {showCloseButton && (
62
+ <DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
63
+ <XIcon />
64
+ <span className="sr-only">Close</span>
65
+ </DialogPrimitive.Close>
66
+ )}
67
+ </DialogPrimitive.Content>
68
+ </DialogPortal>
69
+ );
70
+ }
71
+
72
+ function DialogHeader({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element {
73
+ return (
74
+ <div
75
+ data-slot="dialog-header"
76
+ className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
77
+ {...props}
78
+ />
79
+ );
80
+ }
81
+
82
+ function DialogFooter({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element {
83
+ return (
84
+ <div
85
+ data-slot="dialog-footer"
86
+ className={cn(
87
+ "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
88
+ className,
89
+ )}
90
+ {...props}
91
+ />
92
+ );
93
+ }
94
+
95
+ function DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>): React.JSX.Element {
96
+ return (
97
+ <DialogPrimitive.Title
98
+ data-slot="dialog-title"
99
+ className={cn("text-lg leading-none font-semibold", className)}
100
+ {...props}
101
+ />
102
+ );
103
+ }
104
+
105
+ function DialogDescription({
106
+ className,
107
+ ...props
108
+ }: React.ComponentProps<typeof DialogPrimitive.Description>): React.JSX.Element {
109
+ return (
110
+ <DialogPrimitive.Description
111
+ data-slot="dialog-description"
112
+ className={cn("text-muted-foreground text-sm", className)}
113
+ {...props}
114
+ />
115
+ );
116
+ }
117
+
118
+ export {
119
+ Dialog,
120
+ DialogClose,
121
+ DialogContent,
122
+ DialogDescription,
123
+ DialogFooter,
124
+ DialogHeader,
125
+ DialogOverlay,
126
+ DialogPortal,
127
+ DialogTitle,
128
+ DialogTrigger,
129
+ };
@@ -0,0 +1,149 @@
1
+ // Adapted from better-hub by Better Auth — https://github.com/better-auth/better-hub
2
+ // (apps/web/src/components/ui/dropdown-menu.tsx). Full credit to the Better Auth team.
3
+
4
+ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
5
+ import { CheckIcon } from "lucide-react";
6
+ import type * as React from "react";
7
+ import { cn } from "../foundation/cn";
8
+
9
+ function DropdownMenu({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>): React.JSX.Element {
10
+ return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
11
+ }
12
+
13
+ function DropdownMenuTrigger({
14
+ ...props
15
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>): React.JSX.Element {
16
+ return (
17
+ <DropdownMenuPrimitive.Trigger
18
+ data-slot="dropdown-menu-trigger"
19
+ {...props}
20
+ className={cn("rounded-sm", props.className)}
21
+ />
22
+ );
23
+ }
24
+
25
+ function DropdownMenuContent({
26
+ className,
27
+ sideOffset = 4,
28
+ ...props
29
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Content>): React.JSX.Element {
30
+ return (
31
+ <DropdownMenuPrimitive.Portal>
32
+ <DropdownMenuPrimitive.Content
33
+ data-slot="dropdown-menu-content"
34
+ sideOffset={sideOffset}
35
+ className={cn(
36
+ "bg-background text-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
37
+ className,
38
+ )}
39
+ {...props}
40
+ />
41
+ </DropdownMenuPrimitive.Portal>
42
+ );
43
+ }
44
+
45
+ function DropdownMenuGroup({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>): React.JSX.Element {
46
+ return <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />;
47
+ }
48
+
49
+ function DropdownMenuItem({
50
+ className,
51
+ inset,
52
+ ...props
53
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
54
+ inset?: boolean;
55
+ }): React.JSX.Element {
56
+ return (
57
+ <DropdownMenuPrimitive.Item
58
+ data-slot="dropdown-menu-item"
59
+ data-inset={inset}
60
+ className={cn(
61
+ "focus:bg-accent [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
62
+ className,
63
+ )}
64
+ {...props}
65
+ />
66
+ );
67
+ }
68
+
69
+ function DropdownMenuCheckboxItem({
70
+ className,
71
+ children,
72
+ ...props
73
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>): React.JSX.Element {
74
+ return (
75
+ <DropdownMenuPrimitive.CheckboxItem
76
+ data-slot="dropdown-menu-checkbox-item"
77
+ className={cn(
78
+ "focus:bg-accent relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
79
+ className,
80
+ )}
81
+ {...props}
82
+ >
83
+ <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
84
+ <DropdownMenuPrimitive.ItemIndicator>
85
+ <CheckIcon className="size-4" />
86
+ </DropdownMenuPrimitive.ItemIndicator>
87
+ </span>
88
+ {children}
89
+ </DropdownMenuPrimitive.CheckboxItem>
90
+ );
91
+ }
92
+
93
+ function DropdownMenuLabel({
94
+ className,
95
+ inset,
96
+ ...props
97
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
98
+ inset?: boolean;
99
+ }): React.JSX.Element {
100
+ return (
101
+ <DropdownMenuPrimitive.Label
102
+ data-slot="dropdown-menu-label"
103
+ data-inset={inset}
104
+ className={cn(
105
+ "px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
106
+ className,
107
+ )}
108
+ {...props}
109
+ />
110
+ );
111
+ }
112
+
113
+ function DropdownMenuSeparator({
114
+ className,
115
+ ...props
116
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>): React.JSX.Element {
117
+ return (
118
+ <DropdownMenuPrimitive.Separator
119
+ data-slot="dropdown-menu-separator"
120
+ className={cn("bg-border -mx-1 my-1 h-px", className)}
121
+ {...props}
122
+ />
123
+ );
124
+ }
125
+
126
+ function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<"span">): React.JSX.Element {
127
+ return (
128
+ <span
129
+ data-slot="dropdown-menu-shortcut"
130
+ className={cn(
131
+ "text-muted-foreground ml-auto text-xs tracking-widest",
132
+ className,
133
+ )}
134
+ {...props}
135
+ />
136
+ );
137
+ }
138
+
139
+ export {
140
+ DropdownMenu,
141
+ DropdownMenuTrigger,
142
+ DropdownMenuContent,
143
+ DropdownMenuGroup,
144
+ DropdownMenuLabel,
145
+ DropdownMenuItem,
146
+ DropdownMenuCheckboxItem,
147
+ DropdownMenuSeparator,
148
+ DropdownMenuShortcut,
149
+ };
@@ -0,0 +1,280 @@
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
+ function createRng(seed: number) {
8
+ let s = seed;
9
+ return () => {
10
+ s = (s * 16807) % 2147483647;
11
+ return (s - 1) / 2147483646;
12
+ };
13
+ }
14
+
15
+ const LEVELS = [
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
+ ];
22
+
23
+ const CELL = 15;
24
+
25
+ export function GitHubBackground(): React.JSX.Element {
26
+ const grid = useMemo(() => {
27
+ const rng = createRng(42);
28
+ const cells: { w: number; d: number; level: number }[] = [];
29
+ for (let w = 0; w < 52; w++) {
30
+ for (let d = 0; d < 7; d++) {
31
+ const r = rng();
32
+ const level =
33
+ r < 0.3 ? 0 : r < 0.5 ? 1 : r < 0.7 ? 2 : r < 0.85 ? 3 : 4;
34
+ cells.push({ w, d, level });
35
+ }
36
+ }
37
+ return cells;
38
+ }, []);
39
+
40
+ return (
41
+ <div
42
+ className="absolute inset-0 pointer-events-none overflow-hidden"
43
+ style={{ zIndex: 2 }}
44
+ >
45
+ {/* Radial mask — fades edges into the shader */}
46
+ <div
47
+ className="absolute inset-0"
48
+ style={{
49
+ maskImage: "radial-gradient(ellipse 85% 75% at 45% 35%, black 15%, transparent 60%)",
50
+ WebkitMaskImage:
51
+ "radial-gradient(ellipse 85% 75% at 45% 35%, black 15%, transparent 60%)",
52
+ }}
53
+ >
54
+ {/* ── Contribution Graph ── */}
55
+ <div
56
+ className="absolute"
57
+ style={{
58
+ top: "8%",
59
+ left: "50%",
60
+ transform: "translate(-50%, 0) perspective(500px) rotateX(30deg) rotateZ(-2deg) scale(1.3)",
61
+ transformOrigin: "center top",
62
+ opacity: 0.4,
63
+ }}
64
+ >
65
+ <svg
66
+ width={52 * CELL}
67
+ height={7 * CELL}
68
+ viewBox={`0 0 ${52 * CELL} ${7 * CELL}`}
69
+ >
70
+ {grid.map(({ w, d, level }, i) => (
71
+ <rect
72
+ key={i}
73
+ x={w * CELL}
74
+ y={d * CELL}
75
+ width={12}
76
+ height={12}
77
+ rx={2}
78
+ fill={LEVELS[level]}
79
+ opacity={0}
80
+ >
81
+ <animate
82
+ attributeName="opacity"
83
+ from="0"
84
+ to="1"
85
+ dur="0.5s"
86
+ begin={`${w * 0.04 + d * 0.015}s`}
87
+ fill="freeze"
88
+ />
89
+ </rect>
90
+ ))}
91
+ </svg>
92
+ </div>
93
+
94
+ {/* ── Git Network Graph ── */}
95
+ <svg
96
+ className="absolute inset-0 w-full h-full"
97
+ viewBox="0 0 400 900"
98
+ preserveAspectRatio="xMidYMid slice"
99
+ style={{ opacity: 0.45 }}
100
+ >
101
+ {/* Main branch */}
102
+ <line
103
+ x1="200"
104
+ y1="0"
105
+ x2="200"
106
+ y2="900"
107
+ stroke="rgba(255,255,255,0.15)"
108
+ strokeWidth="2"
109
+ pathLength={1}
110
+ className="git-line-anim"
111
+ style={{ animationDelay: "0.3s" }}
112
+ />
113
+
114
+ {/* Feature branch 1 — forks right */}
115
+ <path
116
+ d="M 200 150 C 200 180, 280 200, 280 230 L 280 350 C 280 380, 200 395, 200 400"
117
+ stroke="rgba(255,255,255,0.12)"
118
+ strokeWidth="2"
119
+ fill="none"
120
+ pathLength={1}
121
+ className="git-line-anim"
122
+ style={{ animationDelay: "0.8s" }}
123
+ />
124
+
125
+ {/* Feature branch 2 — forks left */}
126
+ <path
127
+ d="M 200 300 C 200 325, 130 340, 130 360 L 130 500 C 130 525, 200 540, 200 550"
128
+ stroke="rgba(255,255,255,0.10)"
129
+ strokeWidth="2"
130
+ fill="none"
131
+ pathLength={1}
132
+ className="git-line-anim"
133
+ style={{ animationDelay: "1.3s" }}
134
+ />
135
+
136
+ {/* Hotfix branch — short fork right */}
137
+ <path
138
+ d="M 200 550 C 200 570, 260 580, 260 600 L 260 650 C 260 670, 200 680, 200 690"
139
+ stroke="rgba(255,255,255,0.08)"
140
+ strokeWidth="2"
141
+ fill="none"
142
+ pathLength={1}
143
+ className="git-line-anim"
144
+ style={{ animationDelay: "1.8s" }}
145
+ />
146
+
147
+ {/* Feature branch 3 — wide fork left */}
148
+ <path
149
+ d="M 200 620 C 200 640, 110 660, 110 680 L 110 800 C 110 820, 200 835, 200 850"
150
+ stroke="rgba(255,255,255,0.07)"
151
+ strokeWidth="2"
152
+ fill="none"
153
+ pathLength={1}
154
+ className="git-line-anim"
155
+ style={{ animationDelay: "2.2s" }}
156
+ />
157
+
158
+ {/* Commit nodes — main branch */}
159
+ {[80, 150, 230, 300, 400, 480, 550, 620, 690, 770, 850].map(
160
+ (y, i) => (
161
+ <circle
162
+ key={`m${i}`}
163
+ cx={200}
164
+ cy={y}
165
+ r={4}
166
+ fill="rgba(255,255,255,0.25)"
167
+ opacity={0}
168
+ >
169
+ <animate
170
+ attributeName="opacity"
171
+ from="0"
172
+ to="1"
173
+ dur="0.3s"
174
+ begin={`${0.5 + i * 0.15}s`}
175
+ fill="freeze"
176
+ />
177
+ </circle>
178
+ ),
179
+ )}
180
+
181
+ {/* Commit nodes — feature 1 */}
182
+ {[230, 280, 330].map((y, i) => (
183
+ <circle
184
+ key={`f1${i}`}
185
+ cx={280}
186
+ cy={y}
187
+ r={3.5}
188
+ fill="rgba(255,255,255,0.20)"
189
+ opacity={0}
190
+ >
191
+ <animate
192
+ attributeName="opacity"
193
+ from="0"
194
+ to="1"
195
+ dur="0.3s"
196
+ begin={`${1.0 + i * 0.2}s`}
197
+ fill="freeze"
198
+ />
199
+ </circle>
200
+ ))}
201
+
202
+ {/* Commit nodes — feature 2 */}
203
+ {[360, 420, 470].map((y, i) => (
204
+ <circle
205
+ key={`f2${i}`}
206
+ cx={130}
207
+ cy={y}
208
+ r={3.5}
209
+ fill="rgba(255,255,255,0.18)"
210
+ opacity={0}
211
+ >
212
+ <animate
213
+ attributeName="opacity"
214
+ from="0"
215
+ to="1"
216
+ dur="0.3s"
217
+ begin={`${1.5 + i * 0.2}s`}
218
+ fill="freeze"
219
+ />
220
+ </circle>
221
+ ))}
222
+
223
+ {/* Commit nodes — hotfix */}
224
+ {[600, 630, 660].map((y, i) => (
225
+ <circle
226
+ key={`hf${i}`}
227
+ cx={260}
228
+ cy={y}
229
+ r={3}
230
+ fill="rgba(255,255,255,0.15)"
231
+ opacity={0}
232
+ >
233
+ <animate
234
+ attributeName="opacity"
235
+ from="0"
236
+ to="1"
237
+ dur="0.3s"
238
+ begin={`${2.0 + i * 0.2}s`}
239
+ fill="freeze"
240
+ />
241
+ </circle>
242
+ ))}
243
+
244
+ {/* Commit nodes — feature 3 */}
245
+ {[680, 730, 780].map((y, i) => (
246
+ <circle
247
+ key={`f3${i}`}
248
+ cx={110}
249
+ cy={y}
250
+ r={3}
251
+ fill="rgba(255,255,255,0.12)"
252
+ opacity={0}
253
+ >
254
+ <animate
255
+ attributeName="opacity"
256
+ from="0"
257
+ to="1"
258
+ dur="0.3s"
259
+ begin={`${2.4 + i * 0.2}s`}
260
+ fill="freeze"
261
+ />
262
+ </circle>
263
+ ))}
264
+ </svg>
265
+ </div>
266
+
267
+ {/* CSS for stroke-dash draw animation */}
268
+ <style>{`
269
+ .git-line-anim {
270
+ stroke-dasharray: 1;
271
+ stroke-dashoffset: 1;
272
+ animation: gitDraw 2.5s ease-out forwards;
273
+ }
274
+ @keyframes gitDraw {
275
+ to { stroke-dashoffset: 0; }
276
+ }
277
+ `}</style>
278
+ </div>
279
+ );
280
+ }