@a5c-ai/babysitter-observer-dashboard 1.0.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.
Files changed (205) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +490 -0
  3. package/next.config.mjs +25 -0
  4. package/package.json +104 -0
  5. package/postcss.config.mjs +8 -0
  6. package/src/app/actions/__tests__/approve-breakpoint.test.ts +246 -0
  7. package/src/app/actions/approve-breakpoint.ts +145 -0
  8. package/src/app/api/config/route.ts +137 -0
  9. package/src/app/api/digest/route.ts +45 -0
  10. package/src/app/api/runs/[runId]/events/route.ts +56 -0
  11. package/src/app/api/runs/[runId]/route.ts +84 -0
  12. package/src/app/api/runs/[runId]/tasks/[effectId]/route.ts +44 -0
  13. package/src/app/api/runs/route.ts +48 -0
  14. package/src/app/api/stream/route.ts +136 -0
  15. package/src/app/api/test/route.ts +1 -0
  16. package/src/app/api/version/route.ts +57 -0
  17. package/src/app/globals.css +555 -0
  18. package/src/app/icon.svg +20 -0
  19. package/src/app/layout.tsx +39 -0
  20. package/src/app/not-found.tsx +16 -0
  21. package/src/app/page.tsx +120 -0
  22. package/src/app/runs/[runId]/page.tsx +279 -0
  23. package/src/cli.ts +271 -0
  24. package/src/components/breakpoint/__tests__/breakpoint-approval.test.tsx +212 -0
  25. package/src/components/breakpoint/__tests__/breakpoint-panel.test.tsx +130 -0
  26. package/src/components/breakpoint/__tests__/file-preview.test.tsx +313 -0
  27. package/src/components/breakpoint/breakpoint-approval.tsx +138 -0
  28. package/src/components/breakpoint/breakpoint-panel.tsx +95 -0
  29. package/src/components/breakpoint/file-preview.tsx +215 -0
  30. package/src/components/dashboard/.gitkeep +0 -0
  31. package/src/components/dashboard/__tests__/breakpoint-banner.test.tsx +177 -0
  32. package/src/components/dashboard/__tests__/catch-up-banner.test.tsx +141 -0
  33. package/src/components/dashboard/__tests__/executive-summary-banner.test.tsx +164 -0
  34. package/src/components/dashboard/__tests__/kpi-grid.test.tsx +101 -0
  35. package/src/components/dashboard/__tests__/pagination-controls.test.tsx +125 -0
  36. package/src/components/dashboard/__tests__/project-accordion.test.tsx +97 -0
  37. package/src/components/dashboard/__tests__/project-list-view.test.tsx +174 -0
  38. package/src/components/dashboard/__tests__/project-search-input.test.tsx +110 -0
  39. package/src/components/dashboard/__tests__/project-section-header.test.tsx +91 -0
  40. package/src/components/dashboard/__tests__/project-section.test.tsx +151 -0
  41. package/src/components/dashboard/__tests__/run-card.test.tsx +164 -0
  42. package/src/components/dashboard/__tests__/run-filter-bar.test.tsx +109 -0
  43. package/src/components/dashboard/__tests__/run-list.test.tsx +123 -0
  44. package/src/components/dashboard/__tests__/search-filter.test.tsx +150 -0
  45. package/src/components/dashboard/__tests__/virtualized-run-list.test.tsx +179 -0
  46. package/src/components/dashboard/breakpoint-banner.tsx +301 -0
  47. package/src/components/dashboard/catch-up-banner.tsx +88 -0
  48. package/src/components/dashboard/executive-summary-banner.tsx +174 -0
  49. package/src/components/dashboard/global-search.tsx +323 -0
  50. package/src/components/dashboard/kpi-grid.tsx +140 -0
  51. package/src/components/dashboard/pagination-controls.tsx +100 -0
  52. package/src/components/dashboard/project-accordion.tsx +72 -0
  53. package/src/components/dashboard/project-health-card.tsx +536 -0
  54. package/src/components/dashboard/project-list-view.tsx +246 -0
  55. package/src/components/dashboard/project-search-input.tsx +41 -0
  56. package/src/components/dashboard/project-section-header.tsx +73 -0
  57. package/src/components/dashboard/project-section.tsx +89 -0
  58. package/src/components/dashboard/run-card.tsx +218 -0
  59. package/src/components/dashboard/run-filter-bar.tsx +100 -0
  60. package/src/components/dashboard/run-list.tsx +77 -0
  61. package/src/components/dashboard/search-filter.tsx +69 -0
  62. package/src/components/dashboard/virtualized-run-list.tsx +130 -0
  63. package/src/components/details/.gitkeep +0 -0
  64. package/src/components/details/__tests__/agent-panel.test.tsx +236 -0
  65. package/src/components/details/__tests__/json-tree.test.tsx +347 -0
  66. package/src/components/details/__tests__/log-viewer.test.tsx +168 -0
  67. package/src/components/details/__tests__/task-detail.test.tsx +212 -0
  68. package/src/components/details/__tests__/timing-panel.test.tsx +271 -0
  69. package/src/components/details/agent-panel.tsx +234 -0
  70. package/src/components/details/json-tree/categorize.ts +131 -0
  71. package/src/components/details/json-tree/index.tsx +120 -0
  72. package/src/components/details/json-tree/json-node.tsx +223 -0
  73. package/src/components/details/json-tree/smart-summary.tsx +596 -0
  74. package/src/components/details/json-tree/tree-controls.tsx +47 -0
  75. package/src/components/details/json-tree.tsx +9 -0
  76. package/src/components/details/log-viewer.tsx +140 -0
  77. package/src/components/details/task-detail.tsx +114 -0
  78. package/src/components/details/timing-panel.tsx +247 -0
  79. package/src/components/events/.gitkeep +0 -0
  80. package/src/components/events/__tests__/event-item.test.tsx +211 -0
  81. package/src/components/events/__tests__/event-stream.test.tsx +225 -0
  82. package/src/components/events/event-item.tsx +121 -0
  83. package/src/components/events/event-stream.tsx +260 -0
  84. package/src/components/notifications/.gitkeep +0 -0
  85. package/src/components/notifications/__tests__/notification-panel.test.tsx +287 -0
  86. package/src/components/notifications/__tests__/notification-provider.test.tsx +585 -0
  87. package/src/components/notifications/__tests__/toast-stack.test.tsx +217 -0
  88. package/src/components/notifications/notification-panel.tsx +124 -0
  89. package/src/components/notifications/notification-provider.tsx +175 -0
  90. package/src/components/notifications/toast-stack.tsx +75 -0
  91. package/src/components/pipeline/.gitkeep +0 -0
  92. package/src/components/pipeline/__tests__/parallel-group.test.tsx +88 -0
  93. package/src/components/pipeline/__tests__/pipeline-view.test.tsx +345 -0
  94. package/src/components/pipeline/__tests__/step-card.test.tsx +330 -0
  95. package/src/components/pipeline/parallel-group.tsx +39 -0
  96. package/src/components/pipeline/pipeline-view.tsx +197 -0
  97. package/src/components/pipeline/step-card.tsx +166 -0
  98. package/src/components/providers/event-stream-provider.tsx +29 -0
  99. package/src/components/providers.tsx +24 -0
  100. package/src/components/shared/.gitkeep +0 -0
  101. package/src/components/shared/__tests__/empty-state.test.tsx +49 -0
  102. package/src/components/shared/__tests__/friendly-id.test.tsx +47 -0
  103. package/src/components/shared/__tests__/kbd.test.tsx +45 -0
  104. package/src/components/shared/__tests__/kind-badge.test.tsx +71 -0
  105. package/src/components/shared/__tests__/metrics-row.test.tsx +74 -0
  106. package/src/components/shared/__tests__/outcome-banner.test.tsx +71 -0
  107. package/src/components/shared/__tests__/progress-bar.test.tsx +89 -0
  108. package/src/components/shared/__tests__/session-pill.test.tsx +62 -0
  109. package/src/components/shared/__tests__/settings-modal.test.tsx +201 -0
  110. package/src/components/shared/__tests__/shortcuts-help.test.tsx +103 -0
  111. package/src/components/shared/__tests__/status-badge.test.tsx +98 -0
  112. package/src/components/shared/__tests__/theme-provider.test.tsx +100 -0
  113. package/src/components/shared/__tests__/truncated-id.test.tsx +53 -0
  114. package/src/components/shared/app-footer.tsx +80 -0
  115. package/src/components/shared/app-header.tsx +160 -0
  116. package/src/components/shared/empty-state.tsx +18 -0
  117. package/src/components/shared/error-boundary.tsx +81 -0
  118. package/src/components/shared/friendly-id.tsx +48 -0
  119. package/src/components/shared/kbd.tsx +15 -0
  120. package/src/components/shared/kind-badge.tsx +51 -0
  121. package/src/components/shared/metrics-row.tsx +106 -0
  122. package/src/components/shared/outcome-banner.tsx +56 -0
  123. package/src/components/shared/progress-bar.tsx +42 -0
  124. package/src/components/shared/session-pill.tsx +69 -0
  125. package/src/components/shared/settings-modal.tsx +509 -0
  126. package/src/components/shared/shortcuts-help.tsx +113 -0
  127. package/src/components/shared/status-badge.tsx +110 -0
  128. package/src/components/shared/theme-provider.tsx +46 -0
  129. package/src/components/shared/truncated-id.tsx +51 -0
  130. package/src/components/ui/.gitkeep +0 -0
  131. package/src/components/ui/__tests__/accordion.test.tsx +96 -0
  132. package/src/components/ui/__tests__/badge.test.tsx +69 -0
  133. package/src/components/ui/__tests__/button.test.tsx +113 -0
  134. package/src/components/ui/__tests__/tabs.test.tsx +75 -0
  135. package/src/components/ui/__tests__/tooltip.test.tsx +90 -0
  136. package/src/components/ui/accordion.tsx +61 -0
  137. package/src/components/ui/badge.tsx +25 -0
  138. package/src/components/ui/button.tsx +40 -0
  139. package/src/components/ui/card.tsx +21 -0
  140. package/src/components/ui/scroll-area.tsx +35 -0
  141. package/src/components/ui/separator.tsx +24 -0
  142. package/src/components/ui/tabs.tsx +64 -0
  143. package/src/components/ui/tooltip.tsx +37 -0
  144. package/src/hooks/.gitkeep +0 -0
  145. package/src/hooks/__tests__/use-animated-number.test.ts +184 -0
  146. package/src/hooks/__tests__/use-batched-updates.test.ts +315 -0
  147. package/src/hooks/__tests__/use-event-stream.test.ts +243 -0
  148. package/src/hooks/__tests__/use-keyboard.test.ts +217 -0
  149. package/src/hooks/__tests__/use-notifications.test.ts +230 -0
  150. package/src/hooks/__tests__/use-polling.test.ts +274 -0
  151. package/src/hooks/__tests__/use-project-runs.test.ts +163 -0
  152. package/src/hooks/__tests__/use-projects.test.ts +248 -0
  153. package/src/hooks/__tests__/use-run-dashboard.test.ts +168 -0
  154. package/src/hooks/__tests__/use-run-detail.test.ts +273 -0
  155. package/src/hooks/__tests__/use-smart-polling.test.ts +305 -0
  156. package/src/hooks/use-animated-number.ts +87 -0
  157. package/src/hooks/use-batched-updates.ts +150 -0
  158. package/src/hooks/use-event-stream.ts +150 -0
  159. package/src/hooks/use-keyboard.ts +45 -0
  160. package/src/hooks/use-notifications.ts +82 -0
  161. package/src/hooks/use-persisted-state.ts +60 -0
  162. package/src/hooks/use-polling.ts +60 -0
  163. package/src/hooks/use-project-runs.ts +51 -0
  164. package/src/hooks/use-projects.ts +26 -0
  165. package/src/hooks/use-run-dashboard.ts +207 -0
  166. package/src/hooks/use-run-detail.ts +77 -0
  167. package/src/hooks/use-smart-polling.ts +144 -0
  168. package/src/lib/.gitkeep +0 -0
  169. package/src/lib/__tests__/cn.test.ts +69 -0
  170. package/src/lib/__tests__/config-loader.test.ts +210 -0
  171. package/src/lib/__tests__/config.test.ts +561 -0
  172. package/src/lib/__tests__/error-handler.test.ts +143 -0
  173. package/src/lib/__tests__/fetcher.test.ts +517 -0
  174. package/src/lib/__tests__/global-registry.test.ts +214 -0
  175. package/src/lib/__tests__/parser.test.ts +1532 -0
  176. package/src/lib/__tests__/path-resolver.test.ts +112 -0
  177. package/src/lib/__tests__/run-cache.test.ts +591 -0
  178. package/src/lib/__tests__/server-init.test.ts +512 -0
  179. package/src/lib/__tests__/source-discovery.test.ts +246 -0
  180. package/src/lib/__tests__/utils.test.ts +160 -0
  181. package/src/lib/__tests__/watcher.test.ts +227 -0
  182. package/src/lib/cn.ts +6 -0
  183. package/src/lib/config-loader.ts +195 -0
  184. package/src/lib/config.ts +20 -0
  185. package/src/lib/error-handler.ts +76 -0
  186. package/src/lib/fetcher.ts +394 -0
  187. package/src/lib/global-registry.ts +117 -0
  188. package/src/lib/parser.ts +794 -0
  189. package/src/lib/path-resolver.ts +16 -0
  190. package/src/lib/run-cache.ts +404 -0
  191. package/src/lib/server-init.ts +226 -0
  192. package/src/lib/services/__tests__/run-query-service.test.ts +819 -0
  193. package/src/lib/services/run-query-service.ts +286 -0
  194. package/src/lib/source-discovery.ts +216 -0
  195. package/src/lib/utils.ts +103 -0
  196. package/src/lib/watcher.ts +265 -0
  197. package/src/test/fixtures.ts +269 -0
  198. package/src/test/mocks/handlers.ts +110 -0
  199. package/src/test/mocks/server.ts +17 -0
  200. package/src/test/setup.ts +200 -0
  201. package/src/test/test-utils.tsx +36 -0
  202. package/src/types/.gitkeep +0 -0
  203. package/src/types/breakpoint.ts +17 -0
  204. package/src/types/index.ts +214 -0
  205. package/tsconfig.json +50 -0
@@ -0,0 +1,61 @@
1
+ "use client";
2
+ import * as AccordionPrimitive from "@radix-ui/react-accordion";
3
+ import { ChevronRight } from "lucide-react";
4
+ import { cn } from "@/lib/cn";
5
+
6
+ interface AccordionProps {
7
+ type?: "single" | "multiple";
8
+ defaultValue?: string | string[];
9
+ value?: string | string[];
10
+ onValueChange?: (value: string | string[]) => void;
11
+ collapsible?: boolean;
12
+ className?: string;
13
+ children: React.ReactNode;
14
+ }
15
+
16
+ export function Accordion({ children, className, ...props }: AccordionProps) {
17
+ // Cast to any to avoid React 18 ForwardRefExoticComponent type mismatch
18
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
19
+ const Root = AccordionPrimitive.Root as any;
20
+ return <Root className={className} {...props}>{children}</Root>;
21
+ }
22
+
23
+ export function AccordionItem({ className, children, value }: { className?: string; children: React.ReactNode; value: string }) {
24
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
25
+ const Item = AccordionPrimitive.Item as any;
26
+ return (
27
+ <Item value={value} className={className}>
28
+ {children}
29
+ </Item>
30
+ );
31
+ }
32
+
33
+ export function AccordionTrigger({ className, children }: { className?: string; children: React.ReactNode }) {
34
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
35
+ const Header = AccordionPrimitive.Header as any;
36
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
37
+ const Trigger = AccordionPrimitive.Trigger as any;
38
+ return (
39
+ <Header className="flex">
40
+ <Trigger
41
+ className={cn(
42
+ "flex flex-1 items-center gap-2 py-3 text-sm font-medium transition-all duration-200 hover:text-primary [&[data-state=open]>svg]:rotate-90 [&[data-state=open]]:text-foreground",
43
+ className
44
+ )}
45
+ >
46
+ <ChevronRight className="h-4 w-4 shrink-0 text-foreground-muted transition-transform duration-300 ease-in-out" />
47
+ {children}
48
+ </Trigger>
49
+ </Header>
50
+ );
51
+ }
52
+
53
+ export function AccordionContent({ className, children }: { className?: string; children: React.ReactNode }) {
54
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
55
+ const Content = AccordionPrimitive.Content as any;
56
+ return (
57
+ <Content className="overflow-hidden text-sm data-[state=open]:animate-[fadeIn_200ms_ease-out] data-[state=closed]:animate-[fadeIn_200ms_ease-out_reverse]">
58
+ <div className={cn("pb-4 pt-0", className)}>{children}</div>
59
+ </Content>
60
+ );
61
+ }
@@ -0,0 +1,25 @@
1
+ import { cva, type VariantProps } from "class-variance-authority";
2
+ import { cn } from "@/lib/cn";
3
+
4
+ const badgeVariants = cva(
5
+ "inline-flex items-center rounded-full px-2.5 py-1 text-[11px] font-medium uppercase tracking-[0.08em] ring-1 ring-inset transition-all duration-200",
6
+ {
7
+ variants: {
8
+ variant: {
9
+ default: "bg-muted text-foreground-secondary ring-border",
10
+ success: "bg-success-muted text-success ring-success/20 shadow-neon-glow-success-sm",
11
+ error: "bg-error-muted text-error ring-error/20 shadow-neon-glow-error-sm",
12
+ warning: "bg-warning-muted text-warning ring-warning/20 shadow-neon-glow-warning-badge",
13
+ info: "bg-info-muted text-info ring-info/20 shadow-neon-glow-cyan-sm",
14
+ pending: "bg-pending-muted text-pending ring-pending/20",
15
+ },
16
+ },
17
+ defaultVariants: { variant: "default" },
18
+ }
19
+ );
20
+
21
+ interface BadgeProps extends React.HTMLAttributes<HTMLSpanElement>, VariantProps<typeof badgeVariants> {}
22
+
23
+ export function Badge({ className, variant, ...props }: BadgeProps) {
24
+ return <span className={cn(badgeVariants({ variant }), className)} {...props} />;
25
+ }
@@ -0,0 +1,40 @@
1
+ import { cva, type VariantProps } from "class-variance-authority";
2
+ import { cn } from "@/lib/cn";
3
+ import { Slot } from "@radix-ui/react-slot";
4
+
5
+ const buttonVariants = cva(
6
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md border text-sm font-medium italic tracking-[0.04em] transition-all duration-200 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 font-serif",
7
+ {
8
+ variants: {
9
+ variant: {
10
+ default: "border-primary/60 bg-primary text-primary-foreground shadow-sm hover:bg-[var(--primary-hover)] hover:shadow-md",
11
+ neon: "border-primary/40 bg-primary-muted text-primary hover:border-primary/60 hover:bg-primary-muted/80 hover:shadow-glow-primary",
12
+ outline: "border-border-hover bg-transparent text-foreground hover:bg-card hover:border-primary/30 hover:shadow-sm",
13
+ ghost: "border-transparent bg-transparent text-foreground-secondary hover:bg-muted hover:text-foreground",
14
+ destructive: "border-destructive/60 bg-destructive text-destructive-foreground hover:bg-destructive/90 hover:shadow-glow-error",
15
+ },
16
+ size: {
17
+ default: "h-9 px-4 py-2",
18
+ sm: "h-11 px-3 text-xs",
19
+ lg: "h-10 px-6",
20
+ icon: "h-11 w-11",
21
+ },
22
+ },
23
+ defaultVariants: { variant: "default", size: "default" },
24
+ }
25
+ );
26
+
27
+ interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
28
+ asChild?: boolean;
29
+ }
30
+
31
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
32
+ const SlotComp = Slot as any;
33
+
34
+ export function Button({ className, variant, size, asChild = false, ...props }: ButtonProps) {
35
+ const classes = cn(buttonVariants({ variant, size }), className);
36
+ if (asChild) {
37
+ return <SlotComp className={classes} {...props} />;
38
+ }
39
+ return <button className={classes} {...props} />;
40
+ }
@@ -0,0 +1,21 @@
1
+ import { cn } from "@/lib/cn";
2
+
3
+ export function Card({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
4
+ return (
5
+ <div
6
+ className={cn(
7
+ "rounded-xl border border-card-border bg-card text-card-foreground shadow-sm transition-all duration-200 hover:bg-[var(--card-hover)] hover:border-[var(--border-hover)] hover:shadow-md",
8
+ className
9
+ )}
10
+ {...props}
11
+ />
12
+ );
13
+ }
14
+
15
+ export function CardHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
16
+ return <div className={cn("flex flex-col space-y-1.5 p-4", className)} {...props} />;
17
+ }
18
+
19
+ export function CardContent({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
20
+ return <div className={cn("p-4 pt-0", className)} {...props} />;
21
+ }
@@ -0,0 +1,35 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
4
+ import { cn } from "@/lib/cn";
5
+
6
+ interface ScrollAreaProps extends React.HTMLAttributes<HTMLDivElement> {
7
+ className?: string;
8
+ children: React.ReactNode;
9
+ }
10
+
11
+ export const ScrollArea = React.forwardRef<HTMLDivElement, ScrollAreaProps>(
12
+ function ScrollArea({ className, children, ...props }, ref) {
13
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
14
+ const Root = ScrollAreaPrimitive.Root as any;
15
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
+ const Viewport = ScrollAreaPrimitive.Viewport as any;
17
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
18
+ const Scrollbar = ScrollAreaPrimitive.Scrollbar as any;
19
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
20
+ const Thumb = ScrollAreaPrimitive.Thumb as any;
21
+ return (
22
+ <Root ref={ref} className={cn("relative overflow-hidden", className)} {...props}>
23
+ <Viewport className="h-full w-full rounded-[inherit]">
24
+ {children}
25
+ </Viewport>
26
+ <Scrollbar
27
+ orientation="vertical"
28
+ className="flex touch-none select-none p-0.5 transition-colors data-[orientation=vertical]:w-2"
29
+ >
30
+ <Thumb className="relative flex-1 rounded-full bg-primary/20 hover:bg-primary/35 transition-colors duration-200" />
31
+ </Scrollbar>
32
+ </Root>
33
+ );
34
+ }
35
+ );
@@ -0,0 +1,24 @@
1
+ "use client";
2
+ import * as SeparatorPrimitive from "@radix-ui/react-separator";
3
+ import { cn } from "@/lib/cn";
4
+
5
+ export function Separator({
6
+ className,
7
+ orientation = "horizontal",
8
+ }: {
9
+ className?: string;
10
+ orientation?: "horizontal" | "vertical";
11
+ }) {
12
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
+ const Root = SeparatorPrimitive.Root as any;
14
+ return (
15
+ <Root
16
+ orientation={orientation}
17
+ className={cn(
18
+ "shrink-0 bg-border",
19
+ orientation === "horizontal" ? "h-px w-full" : "h-full w-px",
20
+ className
21
+ )}
22
+ />
23
+ );
24
+ }
@@ -0,0 +1,64 @@
1
+ "use client";
2
+ import * as TabsPrimitive from "@radix-ui/react-tabs";
3
+ import { cn } from "@/lib/cn";
4
+
5
+ interface TabsProps {
6
+ className?: string;
7
+ children: React.ReactNode;
8
+ defaultValue?: string;
9
+ value?: string;
10
+ onValueChange?: (value: string) => void;
11
+ "data-testid"?: string;
12
+ }
13
+
14
+ export function Tabs({ className, children, "data-testid": testId, ...props }: TabsProps) {
15
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
+ const Root = TabsPrimitive.Root as any;
17
+ return (
18
+ <Root className={className} data-testid={testId} {...props}>
19
+ {children}
20
+ </Root>
21
+ );
22
+ }
23
+
24
+ export function TabsList({ className, children }: { className?: string; children: React.ReactNode }) {
25
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
26
+ const List = TabsPrimitive.List as any;
27
+ return (
28
+ <List
29
+ className={cn("inline-flex h-9 items-center gap-1 rounded-lg bg-background-secondary p-1", className)}
30
+ >
31
+ {children}
32
+ </List>
33
+ );
34
+ }
35
+
36
+ export function TabsTrigger({ className, children, value }: { className?: string; children: React.ReactNode; value: string }) {
37
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
38
+ const Trigger = TabsPrimitive.Trigger as any;
39
+ return (
40
+ <Trigger
41
+ value={value}
42
+ className={cn(
43
+ "relative inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium text-foreground-muted transition-all duration-200",
44
+ "hover:text-foreground-secondary hover:bg-muted/50",
45
+ "data-[state=active]:bg-background-tertiary data-[state=active]:text-foreground data-[state=active]:shadow-sm",
46
+ "after:absolute after:bottom-0 after:left-1/2 after:-translate-x-1/2 after:h-0.5 after:w-0 after:rounded-full after:bg-primary after:transition-all after:duration-200",
47
+ "data-[state=active]:after:w-2/3",
48
+ className
49
+ )}
50
+ >
51
+ {children}
52
+ </Trigger>
53
+ );
54
+ }
55
+
56
+ export function TabsContent({ className, children, value }: { className?: string; children: React.ReactNode; value: string }) {
57
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
58
+ const Content = TabsPrimitive.Content as any;
59
+ return (
60
+ <Content value={value} className={cn("mt-2 focus-visible:outline-none", className)}>
61
+ {children}
62
+ </Content>
63
+ );
64
+ }
@@ -0,0 +1,37 @@
1
+ "use client";
2
+ import * as TooltipPrimitive from "@radix-ui/react-tooltip";
3
+ import { cn } from "@/lib/cn";
4
+
5
+ export function TooltipProvider({ children }: { children: React.ReactNode }) {
6
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
+ const Provider = TooltipPrimitive.Provider as any;
8
+ return <Provider delayDuration={200}>{children}</Provider>;
9
+ }
10
+
11
+ export function Tooltip({ children, open, defaultOpen, onOpenChange }: { children: React.ReactNode; open?: boolean; defaultOpen?: boolean; onOpenChange?: (open: boolean) => void }) {
12
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
+ const Root = TooltipPrimitive.Root as any;
14
+ return <Root open={open} defaultOpen={defaultOpen} onOpenChange={onOpenChange}>{children}</Root>;
15
+ }
16
+
17
+ export function TooltipTrigger({ children, asChild }: { children: React.ReactNode; asChild?: boolean }) {
18
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
19
+ const Trigger = TooltipPrimitive.Trigger as any;
20
+ return <Trigger asChild={asChild}>{children}</Trigger>;
21
+ }
22
+
23
+ export function TooltipContent({ className, children, sideOffset = 4 }: { className?: string; children: React.ReactNode; sideOffset?: number }) {
24
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
25
+ const Content = TooltipPrimitive.Content as any;
26
+ return (
27
+ <Content
28
+ sideOffset={sideOffset}
29
+ className={cn(
30
+ "z-50 overflow-hidden rounded-md border border-card-border bg-card px-3 py-1.5 text-xs text-foreground-secondary shadow-md backdrop-blur-sm",
31
+ className
32
+ )}
33
+ >
34
+ {children}
35
+ </Content>
36
+ );
37
+ }
File without changes
@@ -0,0 +1,184 @@
1
+ import { renderHook, act } from "@testing-library/react";
2
+ import { useAnimatedNumber } from "../use-animated-number";
3
+
4
+ describe("useAnimatedNumber", () => {
5
+ let mockNow = 0;
6
+ let rafCallbacks: Map<number, FrameRequestCallback>;
7
+ let rafId: number;
8
+
9
+ beforeEach(() => {
10
+ vi.useFakeTimers();
11
+ mockNow = 0;
12
+ rafId = 0;
13
+ rafCallbacks = new Map();
14
+
15
+ // Mock performance.now to advance in sync with our fake time
16
+ vi.spyOn(performance, "now").mockImplementation(() => mockNow);
17
+
18
+ // Mock requestAnimationFrame: schedules via setTimeout(16ms) and
19
+ // passes the current mock time to the callback.
20
+ vi.stubGlobal("requestAnimationFrame", (cb: FrameRequestCallback) => {
21
+ const id = ++rafId;
22
+ const timer = setTimeout(() => {
23
+ rafCallbacks.delete(id);
24
+ cb(mockNow);
25
+ }, 16);
26
+ rafCallbacks.set(id, timer as any);
27
+ return id;
28
+ });
29
+
30
+ vi.stubGlobal("cancelAnimationFrame", (id: number) => {
31
+ const timer = rafCallbacks.get(id);
32
+ if (timer !== undefined) {
33
+ clearTimeout(timer as any);
34
+ rafCallbacks.delete(id);
35
+ }
36
+ });
37
+ });
38
+
39
+ afterEach(() => {
40
+ vi.useRealTimers();
41
+ vi.restoreAllMocks();
42
+ });
43
+
44
+ /** Advance fake time by `ms` milliseconds, also advancing performance.now(). */
45
+ async function advanceTime(ms: number) {
46
+ mockNow += ms;
47
+ await act(async () => {
48
+ await vi.advanceTimersByTimeAsync(ms);
49
+ });
50
+ }
51
+
52
+ it("returns the target value immediately on first render", () => {
53
+ const { result } = renderHook(() => useAnimatedNumber(42));
54
+ expect(result.current).toBe(42);
55
+ });
56
+
57
+ it("snaps immediately for small changes (diff <= 2)", () => {
58
+ const { result, rerender } = renderHook(
59
+ ({ target }) => useAnimatedNumber(target),
60
+ { initialProps: { target: 10 } }
61
+ );
62
+
63
+ expect(result.current).toBe(10);
64
+
65
+ // Change by 1 -- should snap immediately
66
+ rerender({ target: 11 });
67
+ expect(result.current).toBe(11);
68
+
69
+ // Change by 2 -- should snap immediately
70
+ rerender({ target: 13 });
71
+ expect(result.current).toBe(13);
72
+ });
73
+
74
+ it("animates towards the target for larger changes", async () => {
75
+ const { result, rerender } = renderHook(
76
+ ({ target }) => useAnimatedNumber(target, 600),
77
+ { initialProps: { target: 10 } }
78
+ );
79
+
80
+ expect(result.current).toBe(10);
81
+
82
+ // Change by 40 -- should start animating
83
+ rerender({ target: 50 });
84
+
85
+ // Advance a few frames -- value should be in between
86
+ await advanceTime(200);
87
+
88
+ expect(result.current).toBeGreaterThanOrEqual(10);
89
+ expect(result.current).toBeLessThanOrEqual(50);
90
+ });
91
+
92
+ it("reaches exact target after full animation duration", async () => {
93
+ const { result, rerender } = renderHook(
94
+ ({ target }) => useAnimatedNumber(target, 300),
95
+ { initialProps: { target: 0 } }
96
+ );
97
+
98
+ rerender({ target: 100 });
99
+
100
+ // Wait well past the animation duration
101
+ await advanceTime(500);
102
+
103
+ expect(result.current).toBe(100);
104
+ });
105
+
106
+ it("handles going from a higher number to lower", async () => {
107
+ const { result, rerender } = renderHook(
108
+ ({ target }) => useAnimatedNumber(target, 300),
109
+ { initialProps: { target: 100 } }
110
+ );
111
+
112
+ expect(result.current).toBe(100);
113
+
114
+ rerender({ target: 20 });
115
+
116
+ // Wait for animation to complete
117
+ await advanceTime(500);
118
+ expect(result.current).toBe(20);
119
+ });
120
+
121
+ it("interrupts in-progress animation when target changes mid-flight", async () => {
122
+ const { result, rerender } = renderHook(
123
+ ({ target }) => useAnimatedNumber(target, 600),
124
+ { initialProps: { target: 0 } }
125
+ );
126
+
127
+ // Start animating to 100
128
+ rerender({ target: 100 });
129
+
130
+ await advanceTime(100);
131
+
132
+ // Mid-animation, change target to 200
133
+ rerender({ target: 200 });
134
+
135
+ // Wait for new animation to fully complete
136
+ await advanceTime(800);
137
+
138
+ expect(result.current).toBe(200);
139
+ });
140
+
141
+ it("does not animate when target stays the same", () => {
142
+ const { result, rerender } = renderHook(
143
+ ({ target }) => useAnimatedNumber(target),
144
+ { initialProps: { target: 42 } }
145
+ );
146
+
147
+ expect(result.current).toBe(42);
148
+
149
+ // Re-render with same value
150
+ rerender({ target: 42 });
151
+ expect(result.current).toBe(42);
152
+ });
153
+
154
+ it("cleans up animation on unmount without errors", async () => {
155
+ const { unmount, rerender } = renderHook(
156
+ ({ target }) => useAnimatedNumber(target, 600),
157
+ { initialProps: { target: 10 } }
158
+ );
159
+
160
+ // Start animation
161
+ rerender({ target: 100 });
162
+
163
+ // Unmount mid-animation -- should not throw
164
+ unmount();
165
+
166
+ // Advance timers -- no error should occur
167
+ await advanceTime(700);
168
+ });
169
+
170
+ it("returns integer values (never fractional)", async () => {
171
+ const { result, rerender } = renderHook(
172
+ ({ target }) => useAnimatedNumber(target, 300),
173
+ { initialProps: { target: 0 } }
174
+ );
175
+
176
+ rerender({ target: 91 });
177
+
178
+ // Check at various points during animation
179
+ for (let ms = 0; ms < 400; ms += 16) {
180
+ await advanceTime(16);
181
+ expect(Number.isInteger(result.current)).toBe(true);
182
+ }
183
+ });
184
+ });