Replace DIY overlay primitives with Radix UI + Sonner (#424)
Some checks are pending
Rust / Format (push) Waiting to run
Rust / Clippy (push) Waiting to run
Rust / Generated Docs (push) Waiting to run
Rust / Test (Linux) (push) Waiting to run
Rust / Test (macOS) (push) Waiting to run
TypeScript / Typecheck (push) Waiting to run
TypeScript / Test (push) Waiting to run
TypeScript / Build (push) Waiting to run

## Summary

Replaces ~285 lines of hand-rolled Tooltip, HoverCard, and Toast code in
`fabro-web` with battle-tested primitives — gaining real keyboard
accessibility, Radix collision detection, and Sonner's toast lifecycle —
while keeping all 13+ call sites unchanged.

### Plan Summary

- **Tooltip + HoverCard → Radix wrappers**: `@radix-ui/react-tooltip`
and `@radix-ui/react-hover-card` replace the DIY `useHoverAnchor` hook.
A `TooltipProvider` is mounted in `app-shell.tsx` (200ms delay, 300ms
skip-delay for grouped sidebar hovers). `<Tooltip>` self-wraps in a
local provider when rendered outside the shell (tests, isolated mounts).
- **Toast system → Sonner**: `toast.tsx` shrinks to a ~30-line shim
preserving the `{ push, dismiss, clear }` API. `ToastProvider` becomes a
no-op pass-through in DOM contexts; in non-DOM test environments it
renders an `aria-live` fallback backed by `useSonner` so test assertions
still work. The `action` field is dropped (was test-only).
`toast.test.tsx` is rewritten against observable rendered text.
- **CSS-only tooltips → `<Tooltip>`**: Two inline `group-hover/*` blocks
in `settings-models.tsx` are swapped for the new wrapper, gaining
keyboard focus + Esc dismiss + collision avoidance.
- **SVG-anchored hovers → `FloatingTooltip`**: A new
`app/components/floating-tooltip.tsx` helper portals to `document.body`
and computes collision-avoiding `top`/`bottom` placement from a raw
`DOMRect` (no wrappable trigger). It absorbs `hover-card-style.ts`
(deleted) and is used by `run-overview.tsx` and `event-debug.tsx`.

### What changed and why

**`FloatingTooltip`** handles the two SVG/Graphviz hover sites where
there is no React trigger element to wrap — only a `DOMRect` measured
from DOM events. It uses `useLayoutEffect` + `ResizeObserver` to measure
its own rendered size before applying final position, so it never clips
at viewport edges. This is the one place a `useLayoutEffect` is
intentional and documented.

**`Tooltip` provider fallback**: Radix throws if `<Tooltip>` renders
without an ancestor `TooltipProvider`. Rather than requiring every test
to mount the shell, the component detects provider presence via context
and injects a local one when needed.

**Toast shim backward-compat**: `ToastProvider` previously accepted
`autoDismissMs` as a prop; that prop is silently dropped. The `action`
field on `ToastInput` is removed (only one test referenced it —
`run-detail.test.ts` is updated accordingly). All other consumers
compile without changes.

**CSP fix** (bundled): `img-src` gains
`https://avatars.githubusercontent.com` to allow GitHub avatar images,
with the corresponding integration-test assertion updated.


### Fabro Details

<details>
<summary>Ran 8 stages in 54m 1s for $32.86</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 6s | – | 0 |
| preflight_lint | 2m 23s | – | 0 |
| implement | 21m 38s | $22.48 | 0 |
| simplify_opus | 13m 51s | $7.00 | 0 |
| simplify_gpt | 3m 54s | $3.38 | 0 |
| verify | 9m 35s | – | 0 |
| **Total** | **54m 1s** | **$32.86** | **0** |

</details>

<details>
<summary>Ran <code>ImplementPlan.fabro</code> (11 nodes and 14
edges)</summary>

```dot
digraph ImplementPlan {
    graph [
        goal="Implement and simplify",
        model_stylesheet="
            * { model: claude-opus-4-7; }
        "
    ]
    rankdir=LR

    start [shape=Mdiamond, label="Start"]
    exit  [shape=Msquare, label="Exit"]

    toolchain         [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
    preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
    fix_lints         [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
    implement         [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD.", model="gpt-55", reasoning_effort="xhigh"]
    simplify_opus     [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
    simplify_gpt      [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
    verify            [label="Verify", shape=parallelogram, script="git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\bActorRef\b|\bActorKind\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\s*==\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1", goal_gate=true, retry_target="fixup"]
    fixup             [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all format, clippy, Rust test, docs, TypeScript typecheck/test, and build failures.", max_visits=3]

    start -> toolchain
    toolchain -> preflight_compile [condition="outcome=succeeded"]
    toolchain -> exit
    preflight_compile -> preflight_lint [condition="outcome=succeeded"]
    preflight_compile -> exit
    preflight_lint -> implement [condition="outcome=succeeded"]
    preflight_lint -> fix_lints
    fix_lints -> preflight_lint
    implement -> simplify_opus -> simplify_gpt -> verify
    verify -> exit  [condition="outcome=succeeded"]
    verify -> fixup
    fixup -> verify
}

```

</details>

⚒️ Generated with [Fabro](https://fabro.sh)

---------

Co-authored-by: Fabro <noreply@fabro.sh>
This commit is contained in:
fabro-sh-0530[bot] 2026-05-26 23:03:06 -04:00 • committed by GitHub
parent 8102c11919
commit c2da22a27c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 491 additions and 500 deletions

View file

@ -1,5 +1,4 @@
import { useEffect, useMemo, useState } from "react";
import { createPortal } from "react-dom";
import {
Listbox,
ListboxButton,
@ -26,6 +25,7 @@ import {
highlightJson,
type DebugCategory,
} from "./event-debug-helpers";
import { FloatingTooltip } from "./floating-tooltip";
export function DebugEventRow({
event,
@ -416,19 +416,11 @@ function DnaPopover({
anchorRect: DOMRect;
runStart: string | undefined;
}) {
if (typeof document === "undefined") return null;
const category = debugCategory(event.event);
const left = anchorRect.left + anchorRect.width / 2;
const top = anchorRect.top;
return createPortal(
<div
role="tooltip"
style={{ left, top }}
className="pointer-events-none fixed z-50 -translate-x-1/2 -translate-y-[calc(100%+8px)] whitespace-nowrap rounded-md bg-panel-alt px-2.5 py-1 text-xs text-fg shadow-lg outline-1 -outline-offset-1 outline-line-strong"
>
return (
<FloatingTooltip rect={anchorRect} placement="top">
{`${debugCategoryLabel(category)} · ${friendlyEventName(event.event)} · ${formatElapsed(event.ts, runStart)}`}
</div>,
document.body,
</FloatingTooltip>
);
}
@ -628,20 +620,12 @@ function ThreadDnaPopover({
item: ThreadDnaItem;
anchorRect: DOMRect;
}) {
if (typeof document === "undefined") return null;
const left = anchorRect.left + anchorRect.width / 2;
const top = anchorRect.top;
const elapsed = formatThreadElapsed(item.startMs);
const duration =
item.durationMs > 0 ? formatThreadDuration(item.durationMs) : "instant";
return createPortal(
<div
role="tooltip"
style={{ left, top }}
className="pointer-events-none fixed z-50 -translate-x-1/2 -translate-y-[calc(100%+8px)] whitespace-nowrap rounded-md bg-panel-alt px-2.5 py-1 text-xs text-fg shadow-lg outline-1 -outline-offset-1 outline-line-strong"
>
return (
<FloatingTooltip rect={anchorRect} placement="top">
{`${THREAD_CATEGORY_LABEL[item.category]} · ${item.label} · ${elapsed} · ${duration}`}
</div>,
document.body,
</FloatingTooltip>
);
}

View file

@ -0,0 +1,149 @@
import {
useLayoutEffect,
useRef,
useState,
type CSSProperties,
type ReactNode,
} from "react";
import { createPortal } from "react-dom";
type FloatingTooltipPlacement = "top" | "bottom";
type FloatingTooltipSize = { height: number; width: number };
const VIEWPORT_MARGIN = 12;
const OFFSET = 8;
const DEFAULT_CLASS_NAME =
"whitespace-nowrap rounded-md bg-panel-alt px-2.5 py-1 text-xs text-fg shadow-lg outline-1 -outline-offset-1 outline-line-strong";
function clamp(value: number, min: number, max: number): number {
if (max < min) return (min + max) / 2;
return Math.min(Math.max(value, min), max);
}
function resolvePlacement(
rect: DOMRect,
placement: FloatingTooltipPlacement,
height: number,
viewportHeight: number,
): FloatingTooltipPlacement {
if (height <= 0) return placement;
const fitsTop = rect.top - OFFSET - height >= VIEWPORT_MARGIN;
const fitsBottom = rect.bottom + OFFSET + height <= viewportHeight - VIEWPORT_MARGIN;
if (placement === "top") {
return fitsTop || !fitsBottom ? "top" : "bottom";
}
return fitsBottom || !fitsTop ? "bottom" : "top";
}
function floatingStyle(
rect: DOMRect,
placement: FloatingTooltipPlacement,
size: FloatingTooltipSize,
viewport: FloatingTooltipSize,
): CSSProperties {
const viewportWidth = viewport.width;
const viewportHeight = viewport.height;
const centerX = rect.left + rect.width / 2;
const availableWidth = Math.max(0, viewportWidth - VIEWPORT_MARGIN * 2);
const width = size.width > 0 ? Math.min(size.width, availableWidth) : 0;
const halfWidth = width / 2;
const minCenter = VIEWPORT_MARGIN + halfWidth;
const maxCenter = viewportWidth - VIEWPORT_MARGIN - halfWidth;
const left = width > 0
? clamp(centerX, minCenter, maxCenter)
: clamp(centerX, VIEWPORT_MARGIN, viewportWidth - VIEWPORT_MARGIN);
const resolvedPlacement = resolvePlacement(rect, placement, size.height, viewportHeight);
if (resolvedPlacement === "top") {
const top = size.height > 0
? Math.max(VIEWPORT_MARGIN, rect.top - OFFSET - size.height)
: rect.top - OFFSET;
return {
left,
maxWidth: availableWidth,
top,
transform: size.height > 0 ? "translateX(-50%)" : "translate(-50%, -100%)",
};
}
const top = size.height > 0
? Math.min(viewportHeight - VIEWPORT_MARGIN - size.height, rect.bottom + OFFSET)
: rect.bottom + OFFSET;
return {
left,
maxWidth: availableWidth,
top: Math.max(VIEWPORT_MARGIN, top),
transform: "translateX(-50%)",
};
}
function viewportSize(): FloatingTooltipSize {
return { height: window.innerHeight, width: window.innerWidth };
}
export function FloatingTooltip({
rect,
placement,
children,
className = DEFAULT_CLASS_NAME,
}: {
rect: DOMRect;
placement: FloatingTooltipPlacement;
children: ReactNode;
className?: string;
}) {
const ref = useRef<HTMLDivElement>(null);
const [size, setSize] = useState({ height: 0, width: 0 });
const [viewport, setViewport] = useState<FloatingTooltipSize>(() =>
typeof window === "undefined" ? { height: 0, width: 0 } : viewportSize(),
);
useLayoutEffect(() => {
const node = ref.current;
if (!node) return;
const updateSize = () => {
const next = node.getBoundingClientRect();
setSize((prev) =>
prev.height === next.height && prev.width === next.width
? prev
: { height: next.height, width: next.width },
);
};
const updateViewport = () => {
const next = viewportSize();
setViewport((prev) =>
prev.height === next.height && prev.width === next.width ? prev : next,
);
};
updateSize();
updateViewport();
const resizeObserver =
typeof ResizeObserver === "undefined"
? null
: new ResizeObserver(updateSize);
resizeObserver?.observe(node);
window.addEventListener("resize", updateViewport);
return () => {
resizeObserver?.disconnect();
window.removeEventListener("resize", updateViewport);
};
}, []);
if (typeof document === "undefined") return null;
return createPortal(
<div
ref={ref}
role="tooltip"
style={floatingStyle(rect, placement, size, viewport)}
className={`pointer-events-none fixed z-50 ${className}`}
>
{children}
</div>,
document.body,
);
}

View file

@ -1,10 +0,0 @@
import type { CSSProperties } from "react";
export function hoverCardStyle(rect: DOMRect): CSSProperties {
const margin = 12;
const top = rect.bottom + 6;
if (rect.left > window.innerWidth / 2) {
return { top, right: Math.max(margin, window.innerWidth - rect.right) };
}
return { top, left: Math.max(margin, rect.left) };
}

View file

@ -1,6 +1,7 @@
import { afterEach, describe, expect, test } from "bun:test";
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { useEffect } from "react";
import TestRenderer, { act } from "react-test-renderer";
import { toast as sonnerToast, useSonner } from "sonner";
import { ToastProvider, useToast } from "./toast";
@ -11,179 +12,146 @@ function textFromNode(node: ReturnType<TestRenderer.ReactTestRenderer["toJSON"]>
return (node.children ?? []).map(textFromNode).join("");
}
function textFromInstance(node: TestRenderer.ReactTestInstance): string {
return node.children
.map((child) => (typeof child === "string" ? child : textFromInstance(child)))
.join("");
}
function PushOnMount({
toast,
function CaptureToastApi({
onReady,
}: {
toast: Parameters<ReturnType<typeof useToast>["push"]>[0];
onReady?: (api: ReturnType<typeof useToast>) => void;
}) {
const api = useToast();
useEffect(() => {
onReady?.(api);
api.push(toast);
}, [api, onReady, toast]);
}, [api, onReady]);
return null;
}
describe("ToastProvider", () => {
afterEach(() => {
delete (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT;
function SonnerToastText() {
const { toasts } = useSonner();
return (
<output aria-live="polite">
{toasts.map((toast) => (
<p key={toast.id}>
{typeof toast.title === "function" ? toast.title() : toast.title}
</p>
))}
</output>
);
}
async function flushSonnerUpdates() {
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
await new Promise((resolve) => setTimeout(resolve, 0));
});
}
describe("useToast", () => {
beforeEach(() => {
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
(globalThis as { requestAnimationFrame?: (callback: FrameRequestCallback) => number }).requestAnimationFrame = (
callback,
) => setTimeout(callback, 0) as unknown as number;
});
test("push renders a toast with the message", async () => {
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
afterEach(() => {
sonnerToast.dismiss();
delete (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT;
delete (globalThis as { requestAnimationFrame?: (callback: FrameRequestCallback) => number }).requestAnimationFrame;
});
test("push renders a Sonner toast with the message", async () => {
let api: ReturnType<typeof useToast> | null = null;
let renderer: TestRenderer.ReactTestRenderer | null = null;
await act(async () => {
renderer = TestRenderer.create(
<ToastProvider autoDismissMs={1000}>
<PushOnMount toast={{ message: "Run archived." }} />
</ToastProvider>,
<>
<SonnerToastText />
<CaptureToastApi
onReady={(value) => {
api = value;
}}
/>
</>,
);
});
await act(async () => {
api!.push({ message: "Run archived." });
});
await flushSonnerUpdates();
expect(textFromNode(renderer!.toJSON())).toContain("Run archived.");
const liveRegions = renderer!.root.findAll(
(node) => node.type === "output" && node.props?.["aria-live"] === "polite",
);
expect(liveRegions.length).toBeGreaterThan(0);
await act(async () => {
renderer?.unmount();
});
});
test("action toasts render a button and fire onClick", async () => {
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
let clicked = 0;
test("error toasts are red and persistent", async () => {
let api: ReturnType<typeof useToast> | null = null;
let toastId = "";
let renderer: TestRenderer.ReactTestRenderer | null = null;
await act(async () => {
renderer = TestRenderer.create(
<ToastProvider autoDismissMs={1000}>
<PushOnMount
toast={{
message: "Run archived.",
action: {
label: "Unarchive",
onClick: () => {
clicked += 1;
},
},
<>
<SonnerToastText />
<CaptureToastApi
onReady={(value) => {
api = value;
}}
/>
</ToastProvider>,
);
});
const button = renderer!.root.findByType("button");
expect(textFromNode(renderer!.toJSON())).toContain("Unarchive");
await act(async () => {
button.props.onClick();
});
expect(clicked).toBe(1);
await act(async () => {
renderer?.unmount();
});
});
test("error toasts do not auto-dismiss", async () => {
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
let renderer: TestRenderer.ReactTestRenderer | null = null;
await act(async () => {
renderer = TestRenderer.create(
<ToastProvider autoDismissMs={5}>
<PushOnMount toast={{ message: "Conflict", tone: "error" }} />
</ToastProvider>,
</>,
);
});
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 20));
toastId = api!.push({ message: "Conflict", tone: "error", autoDismissMs: 5 });
});
await flushSonnerUpdates();
expect(textFromNode(renderer!.toJSON())).toContain("Conflict");
expect(
sonnerToast.getToasts().find((toast) => toast.id === toastId),
).toMatchObject({
duration: Infinity,
title: "Conflict",
type: "error",
});
await act(async () => {
renderer?.unmount();
});
});
test("multiple toasts stack in insertion order", async () => {
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
test("dismiss removes one toast from the Sonner store", async () => {
let api: ReturnType<typeof useToast> | null = null;
let secondId = "";
let renderer: TestRenderer.ReactTestRenderer | null = null;
await act(async () => {
renderer = TestRenderer.create(
<ToastProvider autoDismissMs={1000}>
<PushOnMount
toast={{ message: "First" }}
<>
<SonnerToastText />
<CaptureToastApi
onReady={(value) => {
api = value;
}}
/>
</ToastProvider>,
</>,
);
});
await act(async () => {
api!.push({ message: "Second" });
api!.push({ message: "First" });
secondId = api!.push({ message: "Second" });
});
const toasts = renderer!.root.findAll(
(node) => node.props?.["data-toast-id"] != null,
);
expect(toasts).toHaveLength(2);
expect(textFromInstance(toasts[0]!)).toContain("First");
expect(textFromInstance(toasts[1]!)).toContain("Second");
await flushSonnerUpdates();
await act(async () => {
renderer?.unmount();
});
});
test("dismiss removes a toast and leaves the rest reflowed", async () => {
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
let api: ReturnType<typeof useToast> | null = null;
let firstId = "";
let renderer: TestRenderer.ReactTestRenderer | null = null;
await act(async () => {
renderer = TestRenderer.create(
<ToastProvider autoDismissMs={1000}>
<PushOnMount
toast={{ message: "First" }}
onReady={(value) => {
api = value;
}}
/>
</ToastProvider>,
);
});
await act(async () => {
firstId = api!.push({ message: "Second" });
});
await act(async () => {
api!.dismiss(firstId);
api!.dismiss(secondId);
});
await flushSonnerUpdates();
const text = textFromNode(renderer!.toJSON());
expect(text).toContain("First");
@ -193,4 +161,58 @@ describe("ToastProvider", () => {
renderer?.unmount();
});
});
test("clear removes all Sonner toasts", async () => {
let api: ReturnType<typeof useToast> | null = null;
let renderer: TestRenderer.ReactTestRenderer | null = null;
await act(async () => {
renderer = TestRenderer.create(
<ToastProvider>
<SonnerToastText />
<CaptureToastApi
onReady={(value) => {
api = value;
}}
/>
</ToastProvider>,
);
});
await act(async () => {
api!.push({ message: "First" });
api!.push({ message: "Second" });
});
await flushSonnerUpdates();
await act(async () => {
api!.clear();
});
await flushSonnerUpdates();
const text = textFromNode(renderer!.toJSON());
expect(text).not.toContain("First");
expect(text).not.toContain("Second");
await act(async () => {
renderer?.unmount();
});
});
test("ToastProvider is transparent for existing test wrappers", async () => {
let renderer: TestRenderer.ReactTestRenderer | null = null;
await act(async () => {
renderer = TestRenderer.create(
<ToastProvider>
<span>wrapped child</span>
</ToastProvider>,
);
});
expect(textFromNode(renderer!.toJSON())).toContain("wrapped child");
await act(async () => {
renderer?.unmount();
});
});
});

View file

@ -1,164 +1,84 @@
import {
createContext,
use,
useCallback,
useEffect,
useMemo,
useRef,
useState,
type ReactNode,
} from "react";
import { XMarkIcon } from "@heroicons/react/20/solid";
import type { ReactNode } from "react";
import { toast as sonnerToast, useSonner } from "sonner";
export type ToastTone = "info" | "error";
export interface ToastAction {
label: string;
onClick: () => void;
}
export interface ToastInput {
message: string;
tone?: ToastTone;
action?: ToastAction;
autoDismissMs?: number;
}
interface ToastRecord extends ToastInput {
id: string;
tone: ToastTone;
}
interface ToastContextValue {
push: (toast: ToastInput) => string;
dismiss: (id: string) => void;
clear: () => void;
}
const ToastContext = createContext<ToastContextValue | null>(null);
let nextToastId = 0;
function toastClassName(tone: ToastTone): string {
return tone === "error"
? "border-coral/40 bg-rose-950/90 text-rose-50"
: "border-line bg-panel/95 text-fg-2";
function push(toast: ToastInput): string {
const id = `toast-${nextToastId++}`;
const options = {
id,
...(toast.tone === "error"
? { duration: Infinity }
: toast.autoDismissMs != null
? { duration: toast.autoDismissMs }
: {}),
};
if (toast.tone === "error") {
sonnerToast.error(toast.message, options);
} else {
sonnerToast(toast.message, options);
}
return id;
}
function ToastRoot({
toasts,
onDismiss,
}: {
toasts: ToastRecord[];
onDismiss: (id: string) => void;
}) {
if (toasts.length === 0) return null;
const toastApi: ToastContextValue = {
push,
dismiss: (id) => {
sonnerToast.dismiss(id);
},
clear: () => {
sonnerToast.dismiss();
},
};
/**
* No-op wrapper retained so existing test harnesses and the standalone terminal
* route can keep their <ToastProvider> mount points. In a browser the real
* <Toaster /> is mounted globally in AppShell; in non-DOM test environments we
* render an aria-live fallback that subscribes to the Sonner store so test
* assertions can read the toast text.
*/
export function ToastProvider({ children }: { children: ReactNode }) {
if (typeof document !== "undefined") {
return <>{children}</>;
}
return (
<output
aria-live="polite"
aria-atomic="false"
className="pointer-events-none fixed right-4 bottom-6 z-50 flex w-[min(24rem,calc(100vw-2rem))] flex-col gap-2 sm:right-6"
>
{toasts.map((toast) => (
<div
key={toast.id}
data-toast-id={toast.id}
className={`pointer-events-auto rounded-lg border px-4 py-3 shadow-lg ${toastClassName(toast.tone)}`}
>
<div className="flex items-start gap-3">
<p className="min-w-0 flex-1 text-sm leading-5">{toast.message}</p>
{toast.tone === "error" && (
<button
type="button"
onClick={() => onDismiss(toast.id)}
className="inline-flex size-8 shrink-0 items-center justify-center rounded-md text-current/70 transition-colors hover:bg-white/10 hover:text-current focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-500"
aria-label="Dismiss notification"
>
<XMarkIcon className="size-4" aria-hidden="true" />
</button>
)}
</div>
{toast.action && (
<div className="mt-3">
<button
type="button"
onClick={() => {
toast.action?.onClick();
onDismiss(toast.id);
}}
className="inline-flex min-h-11 min-w-11 items-center justify-center rounded-md bg-white/10 px-3 text-sm font-medium text-current transition-colors hover:bg-white/15 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-500"
>
{toast.action.label}
</button>
</div>
)}
</div>
))}
</output>
);
}
export function ToastProvider({
children,
autoDismissMs = 3500,
}: {
children: ReactNode;
autoDismissMs?: number;
}) {
const [toasts, setToasts] = useState<ToastRecord[]>([]);
const nextIdRef = useRef(0);
const timeoutIdsRef = useRef(new Map<string, ReturnType<typeof setTimeout>>());
const dismiss = useCallback((id: string) => {
const timeoutId = timeoutIdsRef.current.get(id);
if (timeoutId) {
clearTimeout(timeoutId);
timeoutIdsRef.current.delete(id);
}
setToasts((current) => current.filter((toast) => toast.id !== id));
}, []);
const clear = useCallback(() => {
for (const timeoutId of timeoutIdsRef.current.values()) {
clearTimeout(timeoutId);
}
timeoutIdsRef.current.clear();
setToasts([]);
}, []);
const push = useCallback((toast: ToastInput) => {
const id = `toast-${nextIdRef.current++}`;
const record: ToastRecord = {
...toast,
id,
tone: toast.tone ?? "info",
};
setToasts((current) => [...current, record]);
if (record.tone !== "error") {
const timeoutId = setTimeout(() => dismiss(id), toast.autoDismissMs ?? autoDismissMs);
timeoutIdsRef.current.set(id, timeoutId);
}
return id;
}, [autoDismissMs, dismiss]);
useEffect(() => clear, [clear]);
const value = useMemo(() => ({ push, dismiss, clear }), [push, dismiss, clear]);
return (
<ToastContext.Provider value={value}>
<>
{children}
<ToastRoot toasts={toasts} onDismiss={dismiss} />
</ToastContext.Provider>
<NonDomToastOutput />
</>
);
}
export function useToast(): ToastContextValue {
const value = use(ToastContext);
if (!value) {
throw new Error("useToast must be used within a ToastProvider");
}
return value;
return toastApi;
}
function NonDomToastOutput() {
const { toasts } = useSonner();
if (toasts.length === 0) return null;
return (
<output aria-live="polite">
{toasts.map((toast) => (
<p key={toast.id}>
{typeof toast.title === "function" ? toast.title() : toast.title}
</p>
))}
</output>
);
}

View file

@ -2,15 +2,20 @@
// exposes the primary button, secondary button, input, error message, and
// copy button so the auth and in-app surfaces can match.
import { useEffect, useId, useRef, useState, type ReactNode } from "react";
import { createPortal } from "react-dom";
import { Dialog, DialogPanel, DialogTitle } from "@headlessui/react";
import {
ClipboardDocumentCheckIcon,
ClipboardIcon,
} from "@heroicons/react/16/solid";
import { hoverCardStyle } from "./hover-card-style";
import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import {
createContext,
useContext,
useState,
type ComponentProps,
type ReactNode,
} from "react";
export const INPUT_CLASS =
"block w-full rounded-lg bg-panel-alt px-3.5 py-2.5 text-base text-fg outline-1 -outline-offset-1 outline-white/10 placeholder:text-fg-muted focus:outline-2 focus:-outline-offset-1 focus:outline-teal-500 sm:text-sm";
@ -133,49 +138,37 @@ export function ConfirmDialog({
);
}
// Shared hover/focus state for `Tooltip` and `HoverCard`. Returns the trigger
// props to spread and the trigger rect (only while open) for positioning.
// `openDelay` (ms) defers showing the card until the pointer has dwelled on
// the trigger long enough — protects against fetch-on-mount popovers being
// triggered by an incidental cursor sweep through a list.
function useHoverAnchor(openDelay = 0) {
const [open, setOpen] = useState(false);
const triggerRef = useRef<HTMLSpanElement>(null);
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const TOOLTIP_DELAY_DURATION = 200;
const TOOLTIP_SKIP_DELAY_DURATION = 300;
const clearTimer = () => {
if (timerRef.current !== null) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
};
const requestOpen = () => {
if (openDelay <= 0) {
setOpen(true);
return;
}
clearTimer();
timerRef.current = setTimeout(() => {
timerRef.current = null;
setOpen(true);
}, openDelay);
};
const requestClose = () => {
clearTimer();
setOpen(false);
};
const HasTooltipProviderContext = createContext(false);
useEffect(() => clearTimer, []);
type TooltipProviderProps = ComponentProps<typeof TooltipPrimitive.Provider>;
const rect = open ? (triggerRef.current?.getBoundingClientRect() ?? null) : null;
const triggerProps = {
ref: triggerRef,
onMouseEnter: requestOpen,
onMouseLeave: requestClose,
onFocus: requestOpen,
onBlur: requestClose,
};
return { open, rect, triggerProps };
function canUseOverlayDom() {
return typeof window !== "undefined" && typeof document !== "undefined";
}
export function TooltipProvider({
delayDuration = TOOLTIP_DELAY_DURATION,
skipDelayDuration = TOOLTIP_SKIP_DELAY_DURATION,
children,
...props
}: TooltipProviderProps) {
if (!canUseOverlayDom()) {
return <>{children}</>;
}
return (
<HasTooltipProviderContext.Provider value={true}>
<TooltipPrimitive.Provider
delayDuration={delayDuration}
skipDelayDuration={skipDelayDuration}
{...props}
>
{children}
</TooltipPrimitive.Provider>
</HasTooltipProviderContext.Provider>
);
}
export function Tooltip({
@ -185,33 +178,34 @@ export function Tooltip({
label: ReactNode;
children: ReactNode;
}) {
const { open, rect, triggerProps } = useHoverAnchor();
const id = useId();
const portalTarget = typeof document === "undefined" ? null : document.body;
// Tests and isolated mounts may render <Tooltip> without an ancestor
// <TooltipProvider>; Radix throws in that case, so supply a local provider.
const hasProvider = useContext(HasTooltipProviderContext);
return (
<>
<span {...triggerProps} aria-describedby={open ? id : undefined} className="inline-flex">
{children}
</span>
{rect && portalTarget
? createPortal(
<div
role="tooltip"
id={id}
style={{
top: rect.top - 6,
left: rect.left + rect.width / 2,
}}
className="pointer-events-none fixed z-50 -translate-x-1/2 -translate-y-full whitespace-nowrap rounded-md bg-panel px-2 py-1 text-xs text-fg-2 shadow-lg outline-1 -outline-offset-1 outline-line-strong"
>
{label}
</div>,
portalTarget,
)
: null}
</>
if (!canUseOverlayDom()) {
return <span className="inline-flex">{children}</span>;
}
const root = (
<TooltipPrimitive.Root>
<TooltipPrimitive.Trigger asChild>
<span className="inline-flex">{children}</span>
</TooltipPrimitive.Trigger>
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
side="top"
align="center"
sideOffset={6}
collisionPadding={12}
className="pointer-events-none z-50 max-w-[min(32rem,calc(100vw-1.5rem))] rounded-md bg-panel px-2 py-1 text-xs text-fg-2 shadow-lg outline-1 -outline-offset-1 outline-line-strong"
>
{label}
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
</TooltipPrimitive.Root>
);
return hasProvider ? root : <TooltipProvider>{root}</TooltipProvider>;
}
/**
@ -230,29 +224,26 @@ export function HoverCard({
className?: string;
openDelay?: number;
}) {
const { open, rect, triggerProps } = useHoverAnchor(openDelay);
const id = useId();
const portalTarget = typeof document === "undefined" ? null : document.body;
if (!canUseOverlayDom()) {
return <span className={className}>{children}</span>;
}
return (
<>
<span {...triggerProps} aria-describedby={open ? id : undefined} className={className}>
{children}
</span>
{rect && portalTarget
? createPortal(
<div
role="tooltip"
id={id}
style={hoverCardStyle(rect)}
className="pointer-events-none fixed z-50 max-w-[18rem] rounded-lg bg-panel p-3 text-xs text-fg-2 shadow-xl outline-1 -outline-offset-1 outline-line-strong"
>
{content}
</div>,
portalTarget,
)
: null}
</>
<HoverCardPrimitive.Root openDelay={openDelay} closeDelay={0}>
<HoverCardPrimitive.Trigger asChild>
<span className={className}>{children}</span>
</HoverCardPrimitive.Trigger>
<HoverCardPrimitive.Portal>
<HoverCardPrimitive.Content
side="bottom"
align="start"
sideOffset={6}
collisionPadding={12}
className="pointer-events-none z-50 max-w-[18rem] rounded-lg bg-panel p-3 text-xs text-fg-2 shadow-xl outline-1 -outline-offset-1 outline-line-strong"
>
{content}
</HoverCardPrimitive.Content>
</HoverCardPrimitive.Portal>
</HoverCardPrimitive.Root>
);
}

View file

@ -12,8 +12,9 @@ import {
XMarkIcon,
} from "@heroicons/react/24/outline";
import { Link, Outlet, useLocation, useMatches } from "react-router";
import { Toaster } from "sonner";
import { ErrorState } from "../components/state";
import { ToastProvider } from "../components/toast";
import { TooltipProvider } from "../components/ui";
import { AskFabroLayoutProvider, useAskFabroLayout } from "../lib/ask-fabro-layout";
import { DemoModeProvider } from "../lib/demo-mode";
import { useAuthMe } from "../lib/queries";
@ -63,7 +64,7 @@ export default function AppShell() {
return (
<DemoModeProvider value={demoMode}>
<ToastProvider>
<TooltipProvider>
<AskFabroLayoutProvider>
<div
className={classNames(
@ -247,8 +248,11 @@ export default function AppShell() {
)}
<ShellMain fullHeight={fullHeight} maxWidth={maxWidth} />
</div>
{typeof document !== "undefined" && (
<Toaster richColors position="bottom-right" />
)}
</AskFabroLayoutProvider>
</ToastProvider>
</TooltipProvider>
</DemoModeProvider>
);
}

View file

@ -437,7 +437,7 @@ describe("actionMenuSeparatorVisibility", () => {
});
describe("handleLifecycleToastResult", () => {
type PushedToast = { message: string; action?: { label: string; onClick: () => void } };
type PushedToast = { message: string };
function makeToastApi() {
const pushed: PushedToast[] = [];

View file

@ -1,13 +1,12 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { useNavigate, useParams } from "react-router";
import { graphTheme } from "../lib/graph-theme";
import { ApiError } from "../lib/api-client";
import { useRun, useRunGraph, useRunStages } from "../lib/queries";
import { FloatingTooltip } from "../components/floating-tooltip";
import { RunSummaryPanel } from "../components/run-summary-panel";
import { StagePopover } from "../components/stage-popover";
import { StageSidebar } from "../components/stage-sidebar";
import { hoverCardStyle } from "../components/hover-card-style";
import {
GRAPH_DEFAULT_ZOOM_INDEX,
GRAPH_ZOOM_STEPS,
@ -300,22 +299,19 @@ export default function RunOverview() {
/>
)}
</div>
{hoveredNode &&
typeof document !== "undefined" &&
createPortal(
<div
role="tooltip"
style={hoverCardStyle(hoveredNode.rect)}
className="pointer-events-none fixed z-50 max-w-[18rem] rounded-lg bg-panel p-3 text-xs text-fg-2 shadow-xl outline-1 -outline-offset-1 outline-line-strong"
>
<StagePopover
runId={id!}
stage={hoveredNode.stage}
duration={hoveredNode.stage.duration}
/>
</div>,
document.body,
)}
{hoveredNode && (
<FloatingTooltip
rect={hoveredNode.rect}
placement="top"
className="max-w-[18rem] rounded-lg bg-panel p-3 text-xs text-fg-2 shadow-xl outline-1 -outline-offset-1 outline-line-strong"
>
<StagePopover
runId={id!}
stage={hoveredNode.stage}
duration={hoveredNode.stage.duration}
/>
</FloatingTooltip>
)}
</div>
);
}

View file

@ -1,4 +1,5 @@
import { useEffect } from "react";
import { Toaster } from "sonner";
import TerminalView from "../components/terminal-view";
import { ToastProvider } from "../components/toast";
@ -17,6 +18,9 @@ export default function RunTerminal({ params }: { params: { id: string } }) {
<div className="h-screen w-screen overflow-hidden">
<TerminalView runId={params.id} chromeless />
</div>
{typeof document !== "undefined" && (
<Toaster richColors position="bottom-right" />
)}
</ToastProvider>
);
}

View file

@ -26,6 +26,7 @@ import {
SortHeader,
type SortDirection,
} from "../components/runs-list/sort-header";
import { Tooltip } from "../components/ui";
import { formatContextWindow, formatTokensPerSecond } from "../lib/format";
export function meta() {
@ -512,7 +513,13 @@ function TestStatusCell({ state }: { state: RowState | undefined }) {
);
}
return (
<span className="group/test-error relative inline-flex">
<Tooltip
label={
<span className="block w-[32rem] max-w-[80vw] whitespace-pre-line break-words text-left font-mono leading-relaxed">
{state.message}
</span>
}
>
<span
aria-label={`Failed: ${state.message}`}
className="inline-flex cursor-default items-center text-coral focus:outline-none"
@ -520,42 +527,34 @@ function TestStatusCell({ state }: { state: RowState | undefined }) {
>
<XCircleIcon className="size-4 h-lh shrink-0" />
</span>
<span
role="tooltip"
className="pointer-events-none invisible absolute right-0 top-full z-30 mt-1.5 w-[32rem] max-w-[80vw] whitespace-pre-line break-words rounded-md bg-panel p-3 text-left font-mono text-xs leading-relaxed text-fg-2 opacity-0 shadow-2xl shadow-black/40 ring-1 ring-line-strong transition-opacity duration-100 group-hover/test-error:visible group-hover/test-error:opacity-100 group-focus-within/test-error:visible group-focus-within/test-error:opacity-100"
>
{state.message}
</span>
</span>
</Tooltip>
);
}
function ModelNameCell({ model }: { model: Model }) {
const hasAliases = model.aliases.length > 0;
const nameNode = hasAliases ? (
<span className="group/aliases relative inline-flex">
<Tooltip
label={
<span className="block min-w-[10rem]">
<span className="mb-1 block text-[10px] font-medium uppercase tracking-wider text-fg-muted">
Aliases
</span>
{model.aliases.map((alias) => (
<span key={alias} className="block font-mono text-fg-2">
{alias}
</span>
))}
</span>
}
>
<button
type="button"
aria-describedby={`aliases-${model.id}`}
className="cursor-default font-mono text-xs text-fg-2 underline decoration-dotted decoration-fg-muted underline-offset-4 hover:decoration-fg-3 focus:outline-none focus-visible:text-fg"
>
{model.id}
</button>
<span
role="tooltip"
id={`aliases-${model.id}`}
className="pointer-events-none invisible absolute left-0 top-full z-30 mt-1.5 min-w-[10rem] rounded-md bg-panel p-2 text-xs opacity-0 shadow-2xl shadow-black/40 ring-1 ring-line-strong transition-opacity duration-100 group-hover/aliases:visible group-hover/aliases:opacity-100 group-focus-within/aliases:visible group-focus-within/aliases:opacity-100"
>
<span className="mb-1 block text-[10px] font-medium uppercase tracking-wider text-fg-muted">
Aliases
</span>
{model.aliases.map((alias) => (
<span key={alias} className="block font-mono text-fg-2">
{alias}
</span>
))}
</span>
</span>
</Tooltip>
) : (
<span className="font-mono text-xs text-fg-2">{model.id}</span>
);

View file

@ -21,6 +21,8 @@
"@pierre/theme": "0.0.28",
"@pierre/trees": "1.0.0-beta.3",
"@qltysh/fabro-api-client": "workspace:*",
"@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-tooltip": "^1.2.8",
"@tailwindcss/typography": "^0.5.19",
"@viz-js/viz": "^3.24.0",
"@xterm/addon-fit": "^0.11.0",
@ -31,6 +33,7 @@
"react-dom": "^19.2.4",
"react-router": "7.12.0",
"remark-gfm": "^4.0.1",
"sonner": "^2.0.7",
"swr": "^2.4.1"
},
"devDependencies": {

View file

@ -18,6 +18,8 @@
"@pierre/theme": "0.0.28",
"@pierre/trees": "1.0.0-beta.3",
"@qltysh/fabro-api-client": "workspace:*",
"@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-tooltip": "^1.2.8",
"@tailwindcss/typography": "^0.5.19",
"@viz-js/viz": "^3.24.0",
"@xterm/addon-fit": "^0.11.0",
@ -28,6 +30,7 @@
"react-dom": "^19.2.4",
"react-router": "7.12.0",
"remark-gfm": "^4.0.1",
"sonner": "^2.0.7",
"swr": "^2.4.1",
},
"devDependencies": {
@ -452,7 +455,7 @@
"@radix-ui/react-slider": ["@radix-ui/react-slider@1.3.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw=="],
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="],
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-switch": ["@radix-ui/react-switch@1.2.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ=="],
@ -1630,6 +1633,8 @@
"socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="],
"sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="],
"source-map": ["source-map@0.7.3", "", {}, "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
@ -1850,8 +1855,6 @@
"@radix-ui/react-alert-dialog/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-alert-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-arrow/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-aspect-ratio/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
@ -1868,8 +1871,6 @@
"@radix-ui/react-collection/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-context-menu/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
"@radix-ui/react-context-menu/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
@ -1878,8 +1879,6 @@
"@radix-ui/react-dialog/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-dismissable-layer/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-dropdown-menu/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
@ -1902,8 +1901,6 @@
"@radix-ui/react-menu/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-menu/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-menubar/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
"@radix-ui/react-menubar/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
@ -1924,14 +1921,14 @@
"@radix-ui/react-popover/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-popover/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-popper/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
"@radix-ui/react-popper/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-portal/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="],
"@radix-ui/react-progress/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
"@radix-ui/react-progress/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
@ -1952,8 +1949,6 @@
"@radix-ui/react-select/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-select/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-separator/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-slider/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
@ -1986,8 +1981,6 @@
"@radix-ui/react-tooltip/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-visually-hidden/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@remotion/bundler/esbuild": ["esbuild@0.25.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.0", "@esbuild/android-arm": "0.25.0", "@esbuild/android-arm64": "0.25.0", "@esbuild/android-x64": "0.25.0", "@esbuild/darwin-arm64": "0.25.0", "@esbuild/darwin-x64": "0.25.0", "@esbuild/freebsd-arm64": "0.25.0", "@esbuild/freebsd-x64": "0.25.0", "@esbuild/linux-arm": "0.25.0", "@esbuild/linux-arm64": "0.25.0", "@esbuild/linux-ia32": "0.25.0", "@esbuild/linux-loong64": "0.25.0", "@esbuild/linux-mips64el": "0.25.0", "@esbuild/linux-ppc64": "0.25.0", "@esbuild/linux-riscv64": "0.25.0", "@esbuild/linux-s390x": "0.25.0", "@esbuild/linux-x64": "0.25.0", "@esbuild/netbsd-arm64": "0.25.0", "@esbuild/netbsd-x64": "0.25.0", "@esbuild/openbsd-arm64": "0.25.0", "@esbuild/openbsd-x64": "0.25.0", "@esbuild/sunos-x64": "0.25.0", "@esbuild/win32-arm64": "0.25.0", "@esbuild/win32-ia32": "0.25.0", "@esbuild/win32-x64": "0.25.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw=="],
@ -2086,8 +2079,6 @@
"radix-ui/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"radix-ui/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
@ -2120,68 +2111,6 @@
"@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"@radix-ui/react-accordion/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-arrow/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-aspect-ratio/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-checkbox/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-collapsible/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-context-menu/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-dismissable-layer/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-dropdown-menu/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-focus-scope/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-form/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-hover-card/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-label/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-menubar/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-navigation-menu/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-one-time-password-field/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-password-toggle-field/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-popper/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-portal/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-progress/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-radio-group/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-roving-focus/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-scroll-area/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-separator/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-slider/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-switch/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-tabs/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-toast/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-toggle-group/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-toggle/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-toolbar/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-visually-hidden/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@remotion/bundler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ=="],
"@remotion/bundler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.0", "", { "os": "android", "cpu": "arm" }, "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g=="],