@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.
- package/LICENSE +21 -0
- package/README.md +490 -0
- package/next.config.mjs +25 -0
- package/package.json +104 -0
- package/postcss.config.mjs +8 -0
- package/src/app/actions/__tests__/approve-breakpoint.test.ts +246 -0
- package/src/app/actions/approve-breakpoint.ts +145 -0
- package/src/app/api/config/route.ts +137 -0
- package/src/app/api/digest/route.ts +45 -0
- package/src/app/api/runs/[runId]/events/route.ts +56 -0
- package/src/app/api/runs/[runId]/route.ts +84 -0
- package/src/app/api/runs/[runId]/tasks/[effectId]/route.ts +44 -0
- package/src/app/api/runs/route.ts +48 -0
- package/src/app/api/stream/route.ts +136 -0
- package/src/app/api/test/route.ts +1 -0
- package/src/app/api/version/route.ts +57 -0
- package/src/app/globals.css +555 -0
- package/src/app/icon.svg +20 -0
- package/src/app/layout.tsx +39 -0
- package/src/app/not-found.tsx +16 -0
- package/src/app/page.tsx +120 -0
- package/src/app/runs/[runId]/page.tsx +279 -0
- package/src/cli.ts +271 -0
- package/src/components/breakpoint/__tests__/breakpoint-approval.test.tsx +212 -0
- package/src/components/breakpoint/__tests__/breakpoint-panel.test.tsx +130 -0
- package/src/components/breakpoint/__tests__/file-preview.test.tsx +313 -0
- package/src/components/breakpoint/breakpoint-approval.tsx +138 -0
- package/src/components/breakpoint/breakpoint-panel.tsx +95 -0
- package/src/components/breakpoint/file-preview.tsx +215 -0
- package/src/components/dashboard/.gitkeep +0 -0
- package/src/components/dashboard/__tests__/breakpoint-banner.test.tsx +177 -0
- package/src/components/dashboard/__tests__/catch-up-banner.test.tsx +141 -0
- package/src/components/dashboard/__tests__/executive-summary-banner.test.tsx +164 -0
- package/src/components/dashboard/__tests__/kpi-grid.test.tsx +101 -0
- package/src/components/dashboard/__tests__/pagination-controls.test.tsx +125 -0
- package/src/components/dashboard/__tests__/project-accordion.test.tsx +97 -0
- package/src/components/dashboard/__tests__/project-list-view.test.tsx +174 -0
- package/src/components/dashboard/__tests__/project-search-input.test.tsx +110 -0
- package/src/components/dashboard/__tests__/project-section-header.test.tsx +91 -0
- package/src/components/dashboard/__tests__/project-section.test.tsx +151 -0
- package/src/components/dashboard/__tests__/run-card.test.tsx +164 -0
- package/src/components/dashboard/__tests__/run-filter-bar.test.tsx +109 -0
- package/src/components/dashboard/__tests__/run-list.test.tsx +123 -0
- package/src/components/dashboard/__tests__/search-filter.test.tsx +150 -0
- package/src/components/dashboard/__tests__/virtualized-run-list.test.tsx +179 -0
- package/src/components/dashboard/breakpoint-banner.tsx +301 -0
- package/src/components/dashboard/catch-up-banner.tsx +88 -0
- package/src/components/dashboard/executive-summary-banner.tsx +174 -0
- package/src/components/dashboard/global-search.tsx +323 -0
- package/src/components/dashboard/kpi-grid.tsx +140 -0
- package/src/components/dashboard/pagination-controls.tsx +100 -0
- package/src/components/dashboard/project-accordion.tsx +72 -0
- package/src/components/dashboard/project-health-card.tsx +536 -0
- package/src/components/dashboard/project-list-view.tsx +246 -0
- package/src/components/dashboard/project-search-input.tsx +41 -0
- package/src/components/dashboard/project-section-header.tsx +73 -0
- package/src/components/dashboard/project-section.tsx +89 -0
- package/src/components/dashboard/run-card.tsx +218 -0
- package/src/components/dashboard/run-filter-bar.tsx +100 -0
- package/src/components/dashboard/run-list.tsx +77 -0
- package/src/components/dashboard/search-filter.tsx +69 -0
- package/src/components/dashboard/virtualized-run-list.tsx +130 -0
- package/src/components/details/.gitkeep +0 -0
- package/src/components/details/__tests__/agent-panel.test.tsx +236 -0
- package/src/components/details/__tests__/json-tree.test.tsx +347 -0
- package/src/components/details/__tests__/log-viewer.test.tsx +168 -0
- package/src/components/details/__tests__/task-detail.test.tsx +212 -0
- package/src/components/details/__tests__/timing-panel.test.tsx +271 -0
- package/src/components/details/agent-panel.tsx +234 -0
- package/src/components/details/json-tree/categorize.ts +131 -0
- package/src/components/details/json-tree/index.tsx +120 -0
- package/src/components/details/json-tree/json-node.tsx +223 -0
- package/src/components/details/json-tree/smart-summary.tsx +596 -0
- package/src/components/details/json-tree/tree-controls.tsx +47 -0
- package/src/components/details/json-tree.tsx +9 -0
- package/src/components/details/log-viewer.tsx +140 -0
- package/src/components/details/task-detail.tsx +114 -0
- package/src/components/details/timing-panel.tsx +247 -0
- package/src/components/events/.gitkeep +0 -0
- package/src/components/events/__tests__/event-item.test.tsx +211 -0
- package/src/components/events/__tests__/event-stream.test.tsx +225 -0
- package/src/components/events/event-item.tsx +121 -0
- package/src/components/events/event-stream.tsx +260 -0
- package/src/components/notifications/.gitkeep +0 -0
- package/src/components/notifications/__tests__/notification-panel.test.tsx +287 -0
- package/src/components/notifications/__tests__/notification-provider.test.tsx +585 -0
- package/src/components/notifications/__tests__/toast-stack.test.tsx +217 -0
- package/src/components/notifications/notification-panel.tsx +124 -0
- package/src/components/notifications/notification-provider.tsx +175 -0
- package/src/components/notifications/toast-stack.tsx +75 -0
- package/src/components/pipeline/.gitkeep +0 -0
- package/src/components/pipeline/__tests__/parallel-group.test.tsx +88 -0
- package/src/components/pipeline/__tests__/pipeline-view.test.tsx +345 -0
- package/src/components/pipeline/__tests__/step-card.test.tsx +330 -0
- package/src/components/pipeline/parallel-group.tsx +39 -0
- package/src/components/pipeline/pipeline-view.tsx +197 -0
- package/src/components/pipeline/step-card.tsx +166 -0
- package/src/components/providers/event-stream-provider.tsx +29 -0
- package/src/components/providers.tsx +24 -0
- package/src/components/shared/.gitkeep +0 -0
- package/src/components/shared/__tests__/empty-state.test.tsx +49 -0
- package/src/components/shared/__tests__/friendly-id.test.tsx +47 -0
- package/src/components/shared/__tests__/kbd.test.tsx +45 -0
- package/src/components/shared/__tests__/kind-badge.test.tsx +71 -0
- package/src/components/shared/__tests__/metrics-row.test.tsx +74 -0
- package/src/components/shared/__tests__/outcome-banner.test.tsx +71 -0
- package/src/components/shared/__tests__/progress-bar.test.tsx +89 -0
- package/src/components/shared/__tests__/session-pill.test.tsx +62 -0
- package/src/components/shared/__tests__/settings-modal.test.tsx +201 -0
- package/src/components/shared/__tests__/shortcuts-help.test.tsx +103 -0
- package/src/components/shared/__tests__/status-badge.test.tsx +98 -0
- package/src/components/shared/__tests__/theme-provider.test.tsx +100 -0
- package/src/components/shared/__tests__/truncated-id.test.tsx +53 -0
- package/src/components/shared/app-footer.tsx +80 -0
- package/src/components/shared/app-header.tsx +160 -0
- package/src/components/shared/empty-state.tsx +18 -0
- package/src/components/shared/error-boundary.tsx +81 -0
- package/src/components/shared/friendly-id.tsx +48 -0
- package/src/components/shared/kbd.tsx +15 -0
- package/src/components/shared/kind-badge.tsx +51 -0
- package/src/components/shared/metrics-row.tsx +106 -0
- package/src/components/shared/outcome-banner.tsx +56 -0
- package/src/components/shared/progress-bar.tsx +42 -0
- package/src/components/shared/session-pill.tsx +69 -0
- package/src/components/shared/settings-modal.tsx +509 -0
- package/src/components/shared/shortcuts-help.tsx +113 -0
- package/src/components/shared/status-badge.tsx +110 -0
- package/src/components/shared/theme-provider.tsx +46 -0
- package/src/components/shared/truncated-id.tsx +51 -0
- package/src/components/ui/.gitkeep +0 -0
- package/src/components/ui/__tests__/accordion.test.tsx +96 -0
- package/src/components/ui/__tests__/badge.test.tsx +69 -0
- package/src/components/ui/__tests__/button.test.tsx +113 -0
- package/src/components/ui/__tests__/tabs.test.tsx +75 -0
- package/src/components/ui/__tests__/tooltip.test.tsx +90 -0
- package/src/components/ui/accordion.tsx +61 -0
- package/src/components/ui/badge.tsx +25 -0
- package/src/components/ui/button.tsx +40 -0
- package/src/components/ui/card.tsx +21 -0
- package/src/components/ui/scroll-area.tsx +35 -0
- package/src/components/ui/separator.tsx +24 -0
- package/src/components/ui/tabs.tsx +64 -0
- package/src/components/ui/tooltip.tsx +37 -0
- package/src/hooks/.gitkeep +0 -0
- package/src/hooks/__tests__/use-animated-number.test.ts +184 -0
- package/src/hooks/__tests__/use-batched-updates.test.ts +315 -0
- package/src/hooks/__tests__/use-event-stream.test.ts +243 -0
- package/src/hooks/__tests__/use-keyboard.test.ts +217 -0
- package/src/hooks/__tests__/use-notifications.test.ts +230 -0
- package/src/hooks/__tests__/use-polling.test.ts +274 -0
- package/src/hooks/__tests__/use-project-runs.test.ts +163 -0
- package/src/hooks/__tests__/use-projects.test.ts +248 -0
- package/src/hooks/__tests__/use-run-dashboard.test.ts +168 -0
- package/src/hooks/__tests__/use-run-detail.test.ts +273 -0
- package/src/hooks/__tests__/use-smart-polling.test.ts +305 -0
- package/src/hooks/use-animated-number.ts +87 -0
- package/src/hooks/use-batched-updates.ts +150 -0
- package/src/hooks/use-event-stream.ts +150 -0
- package/src/hooks/use-keyboard.ts +45 -0
- package/src/hooks/use-notifications.ts +82 -0
- package/src/hooks/use-persisted-state.ts +60 -0
- package/src/hooks/use-polling.ts +60 -0
- package/src/hooks/use-project-runs.ts +51 -0
- package/src/hooks/use-projects.ts +26 -0
- package/src/hooks/use-run-dashboard.ts +207 -0
- package/src/hooks/use-run-detail.ts +77 -0
- package/src/hooks/use-smart-polling.ts +144 -0
- package/src/lib/.gitkeep +0 -0
- package/src/lib/__tests__/cn.test.ts +69 -0
- package/src/lib/__tests__/config-loader.test.ts +210 -0
- package/src/lib/__tests__/config.test.ts +561 -0
- package/src/lib/__tests__/error-handler.test.ts +143 -0
- package/src/lib/__tests__/fetcher.test.ts +517 -0
- package/src/lib/__tests__/global-registry.test.ts +214 -0
- package/src/lib/__tests__/parser.test.ts +1532 -0
- package/src/lib/__tests__/path-resolver.test.ts +112 -0
- package/src/lib/__tests__/run-cache.test.ts +591 -0
- package/src/lib/__tests__/server-init.test.ts +512 -0
- package/src/lib/__tests__/source-discovery.test.ts +246 -0
- package/src/lib/__tests__/utils.test.ts +160 -0
- package/src/lib/__tests__/watcher.test.ts +227 -0
- package/src/lib/cn.ts +6 -0
- package/src/lib/config-loader.ts +195 -0
- package/src/lib/config.ts +20 -0
- package/src/lib/error-handler.ts +76 -0
- package/src/lib/fetcher.ts +394 -0
- package/src/lib/global-registry.ts +117 -0
- package/src/lib/parser.ts +794 -0
- package/src/lib/path-resolver.ts +16 -0
- package/src/lib/run-cache.ts +404 -0
- package/src/lib/server-init.ts +226 -0
- package/src/lib/services/__tests__/run-query-service.test.ts +819 -0
- package/src/lib/services/run-query-service.ts +286 -0
- package/src/lib/source-discovery.ts +216 -0
- package/src/lib/utils.ts +103 -0
- package/src/lib/watcher.ts +265 -0
- package/src/test/fixtures.ts +269 -0
- package/src/test/mocks/handlers.ts +110 -0
- package/src/test/mocks/server.ts +17 -0
- package/src/test/setup.ts +200 -0
- package/src/test/test-utils.tsx +36 -0
- package/src/types/.gitkeep +0 -0
- package/src/types/breakpoint.ts +17 -0
- package/src/types/index.ts +214 -0
- 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
|
+
});
|