From c2da22a27c3cd73172aa85ba922d7e3cb491e962 Mon Sep 17 00:00:00 2001 From: "fabro-sh-0530[bot]" <281434857+fabro-sh-0530[bot]@users.noreply.github.com> Date: Tue, 26 May 2026 23:03:06 -0400 Subject: [PATCH] Replace DIY overlay primitives with Radix UI + Sonner (#424) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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). `` 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 → ``**: 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 `` 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
Ran 8 stages in 54m 1s for $32.86 | 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** |
Ran ImplementPlan.fabro (11 nodes and 14 edges) ```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 } ```
⚒️ Generated with [Fabro](https://fabro.sh) --------- Co-authored-by: Fabro --- apps/fabro-web/app/components/event-debug.tsx | 30 +-- .../app/components/floating-tooltip.tsx | 149 +++++++++++ .../app/components/hover-card-style.ts | 10 - apps/fabro-web/app/components/toast.test.tsx | 246 ++++++++++-------- apps/fabro-web/app/components/toast.tsx | 196 +++++--------- apps/fabro-web/app/components/ui.tsx | 173 ++++++------ apps/fabro-web/app/layouts/app-shell.tsx | 10 +- apps/fabro-web/app/routes/run-detail.test.ts | 2 +- apps/fabro-web/app/routes/run-overview.tsx | 32 +-- apps/fabro-web/app/routes/run-terminal.tsx | 4 + apps/fabro-web/app/routes/settings-models.tsx | 49 ++-- apps/fabro-web/package.json | 3 + bun.lock | 87 +------ 13 files changed, 491 insertions(+), 500 deletions(-) create mode 100644 apps/fabro-web/app/components/floating-tooltip.tsx delete mode 100644 apps/fabro-web/app/components/hover-card-style.ts diff --git a/apps/fabro-web/app/components/event-debug.tsx b/apps/fabro-web/app/components/event-debug.tsx index 1a1436264..2d9ccb8c6 100644 --- a/apps/fabro-web/app/components/event-debug.tsx +++ b/apps/fabro-web/app/components/event-debug.tsx @@ -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( -
+ return ( + {`${debugCategoryLabel(category)} · ${friendlyEventName(event.event)} · ${formatElapsed(event.ts, runStart)}`} -
, - document.body, + ); } @@ -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( -
+ return ( + {`${THREAD_CATEGORY_LABEL[item.category]} · ${item.label} · ${elapsed} · ${duration}`} -
, - document.body, + ); } diff --git a/apps/fabro-web/app/components/floating-tooltip.tsx b/apps/fabro-web/app/components/floating-tooltip.tsx new file mode 100644 index 000000000..3ce087904 --- /dev/null +++ b/apps/fabro-web/app/components/floating-tooltip.tsx @@ -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(null); + const [size, setSize] = useState({ height: 0, width: 0 }); + const [viewport, setViewport] = useState(() => + 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( +
+ {children} +
, + document.body, + ); +} diff --git a/apps/fabro-web/app/components/hover-card-style.ts b/apps/fabro-web/app/components/hover-card-style.ts deleted file mode 100644 index 94afd8c86..000000000 --- a/apps/fabro-web/app/components/hover-card-style.ts +++ /dev/null @@ -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) }; -} diff --git a/apps/fabro-web/app/components/toast.test.tsx b/apps/fabro-web/app/components/toast.test.tsx index 1adf14217..48f67514d 100644 --- a/apps/fabro-web/app/components/toast.test.tsx +++ b/apps/fabro-web/app/components/toast.test.tsx @@ -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 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["push"]>[0]; onReady?: (api: ReturnType) => 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 ( + + {toasts.map((toast) => ( +

+ {typeof toast.title === "function" ? toast.title() : toast.title} +

+ ))} +
+ ); +} + +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 | null = null; let renderer: TestRenderer.ReactTestRenderer | null = null; await act(async () => { renderer = TestRenderer.create( - - - , + <> + + { + 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 | null = null; + let toastId = ""; let renderer: TestRenderer.ReactTestRenderer | null = null; await act(async () => { renderer = TestRenderer.create( - - { - clicked += 1; - }, - }, + <> + + { + api = value; }} /> - , - ); - }); - - 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( - - - , + , ); }); 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 | null = null; + let secondId = ""; let renderer: TestRenderer.ReactTestRenderer | null = null; - await act(async () => { renderer = TestRenderer.create( - - + + { api = value; }} /> - , + , ); }); 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 | null = null; - let firstId = ""; - let renderer: TestRenderer.ReactTestRenderer | null = null; - - await act(async () => { - renderer = TestRenderer.create( - - { - api = value; - }} - /> - , - ); - }); - - 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 | null = null; + let renderer: TestRenderer.ReactTestRenderer | null = null; + + await act(async () => { + renderer = TestRenderer.create( + + + { + api = value; + }} + /> + , + ); + }); + + 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( + + wrapped child + , + ); + }); + + expect(textFromNode(renderer!.toJSON())).toContain("wrapped child"); + + await act(async () => { + renderer?.unmount(); + }); + }); }); diff --git a/apps/fabro-web/app/components/toast.tsx b/apps/fabro-web/app/components/toast.tsx index e17d194c0..f45c95ff4 100644 --- a/apps/fabro-web/app/components/toast.tsx +++ b/apps/fabro-web/app/components/toast.tsx @@ -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(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 mount points. In a browser the real + * 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 ( - - {toasts.map((toast) => ( -
-
-

{toast.message}

- {toast.tone === "error" && ( - - )} -
- {toast.action && ( -
- -
- )} -
- ))} -
- ); -} - -export function ToastProvider({ - children, - autoDismissMs = 3500, -}: { - children: ReactNode; - autoDismissMs?: number; -}) { - const [toasts, setToasts] = useState([]); - const nextIdRef = useRef(0); - const timeoutIdsRef = useRef(new Map>()); - - 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 ( - + <> {children} - - + + ); } 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 ( + + {toasts.map((toast) => ( +

+ {typeof toast.title === "function" ? toast.title() : toast.title} +

+ ))} +
+ ); } diff --git a/apps/fabro-web/app/components/ui.tsx b/apps/fabro-web/app/components/ui.tsx index fac25dcf4..8b3daa5de 100644 --- a/apps/fabro-web/app/components/ui.tsx +++ b/apps/fabro-web/app/components/ui.tsx @@ -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(null); - const timerRef = useRef | 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; - 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 ( + + + {children} + + + ); } 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 without an ancestor + // ; Radix throws in that case, so supply a local provider. + const hasProvider = useContext(HasTooltipProviderContext); - return ( - <> - - {children} - - {rect && portalTarget - ? createPortal( - , - portalTarget, - ) - : null} - + if (!canUseOverlayDom()) { + return {children}; + } + + const root = ( + + + {children} + + + + {label} + + + ); + + return hasProvider ? root : {root}; } /** @@ -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 {children}; + } return ( - <> - - {children} - - {rect && portalTarget - ? createPortal( - , - portalTarget, - ) - : null} - + + + {children} + + + + {content} + + + ); } diff --git a/apps/fabro-web/app/layouts/app-shell.tsx b/apps/fabro-web/app/layouts/app-shell.tsx index 32c222664..ca0cbe417 100644 --- a/apps/fabro-web/app/layouts/app-shell.tsx +++ b/apps/fabro-web/app/layouts/app-shell.tsx @@ -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 ( - +
+ {typeof document !== "undefined" && ( + + )}
-
+
); } diff --git a/apps/fabro-web/app/routes/run-detail.test.ts b/apps/fabro-web/app/routes/run-detail.test.ts index 6d0bd0427..e30d38da8 100644 --- a/apps/fabro-web/app/routes/run-detail.test.ts +++ b/apps/fabro-web/app/routes/run-detail.test.ts @@ -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[] = []; diff --git a/apps/fabro-web/app/routes/run-overview.tsx b/apps/fabro-web/app/routes/run-overview.tsx index 131c11738..688f2ddcf 100644 --- a/apps/fabro-web/app/routes/run-overview.tsx +++ b/apps/fabro-web/app/routes/run-overview.tsx @@ -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() { /> )} - {hoveredNode && - typeof document !== "undefined" && - createPortal( -
- -
, - document.body, - )} + {hoveredNode && ( + + + + )} ); } diff --git a/apps/fabro-web/app/routes/run-terminal.tsx b/apps/fabro-web/app/routes/run-terminal.tsx index 78c7f42cf..0f7d66af9 100644 --- a/apps/fabro-web/app/routes/run-terminal.tsx +++ b/apps/fabro-web/app/routes/run-terminal.tsx @@ -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 } }) {
+ {typeof document !== "undefined" && ( + + )}
); } diff --git a/apps/fabro-web/app/routes/settings-models.tsx b/apps/fabro-web/app/routes/settings-models.tsx index 6a3867275..936f77da0 100644 --- a/apps/fabro-web/app/routes/settings-models.tsx +++ b/apps/fabro-web/app/routes/settings-models.tsx @@ -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 ( - + + {state.message} + + } + > - - {state.message} - - +
); } function ModelNameCell({ model }: { model: Model }) { const hasAliases = model.aliases.length > 0; const nameNode = hasAliases ? ( - + + + Aliases + + {model.aliases.map((alias) => ( + + {alias} + + ))} + + } + > - - - Aliases - - {model.aliases.map((alias) => ( - - {alias} - - ))} - - +
) : ( {model.id} ); diff --git a/apps/fabro-web/package.json b/apps/fabro-web/package.json index 6dbad2669..ed95f5298 100644 --- a/apps/fabro-web/package.json +++ b/apps/fabro-web/package.json @@ -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": { diff --git a/bun.lock b/bun.lock index e78ff176e..c0fb7a68a 100644 --- a/bun.lock +++ b/bun.lock @@ -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=="],