-
- {loading ? "Loading..." : title}
-
+
+
+ {loading ? "Loading..." : title}
+
+
+
+
+
@@ -60,9 +130,7 @@ function VodPage() {
{(author?.displayName || author?.handle) && (
@
)}
-
- {author?.handle || user}
-
+ {author?.handle || user}
{duration && (
diff --git a/js/web/src/routes/__root.tsx b/js/web/src/routes/__root.tsx
index 384f5043b..71df09bba 100644
--- a/js/web/src/routes/__root.tsx
+++ b/js/web/src/routes/__root.tsx
@@ -1,35 +1,134 @@
import Header from "@/components/header";
import SidebarComponent from "@/components/sidebar";
-import { createRootRoute, Outlet } from "@tanstack/react-router";
-import { useState } from "react";
+import { TooltipProvider } from "@/components/ui/tooltip";
+import { FullscreenProvider } from "@/contexts/fullscreen-context";
+import {
+ createRootRoute,
+ Outlet,
+ useRouterState,
+} from "@tanstack/react-router";
+import { Component, type ReactNode, useEffect, useState } from "react";
import { SidebarInset, SidebarProvider } from "../components/ui/sidebar";
+import { getStoredPreference, syncThemeClass } from "../hooks/use-color-scheme";
+
+/** Routes that should render without sidebar/header chrome. */
+const POPOUT_PREFIXES = ["/chat-popout/"];
export const Route = createRootRoute({
component: RootLayout,
+ pendingComponent: RouteLoadingSkeleton,
});
+// --- Error boundary ---
+
+interface ErrorBoundaryState {
+ error: Error | null;
+}
+
+class ErrorBoundary extends Component<
+ { children: ReactNode },
+ ErrorBoundaryState
+> {
+ state: ErrorBoundaryState = { error: null };
+
+ static getDerivedStateFromError(error: Error) {
+ return { error };
+ }
+
+ componentDidCatch(error: Error, info: React.ErrorInfo) {
+ console.error("Unhandled render error:", error, info);
+ }
+
+ render() {
+ if (this.state.error) {
+ return (
+
+
+ Something went wrong
+
+
+ {this.state.error.message || "An unexpected error occurred."}
+
+
this.setState({ error: null })}
+ className="h-10 px-4 rounded-md bg-[var(--color-accent)] hover:bg-[var(--color-accent-hover)] text-[var(--color-accent-fg)] font-medium transition-colors"
+ >
+ Try again
+
+
+ );
+ }
+ return this.props.children;
+ }
+}
+
+// --- Root layout ---
+
+function RouteLoadingSkeleton() {
+ return (
+
+ );
+}
+
function RootLayout() {
+ const pathname = useRouterState({
+ select: (s) => s.resolvedLocation?.pathname ?? "",
+ });
+ const isPopout = POPOUT_PREFIXES.some((p) => pathname.startsWith(p));
+
const [open, setOpen] = useState(() => {
if (typeof localStorage === "undefined") return true;
return localStorage.getItem("streamplace:nav-open") !== "false";
});
- return (
-
{
- setOpen(o);
- localStorage.setItem("streamplace:nav-open", String(o));
- }}
- className="h-svh"
- >
-
-
-
-
+ // Re-sync theme when system preference changes (only matters if pref is "system").
+ useEffect(() => {
+ const mql = window.matchMedia("(prefers-color-scheme: dark)");
+ const onChange = () => {
+ if (getStoredPreference() === "system") {
+ syncThemeClass();
+ }
+ };
+ mql.addEventListener("change", onChange);
+ return () => mql.removeEventListener("change", onChange);
+ }, []);
+
+ // Popout routes get no chrome — just providers and the outlet.
+ if (isPopout) {
+ return (
+
+
-
-
-
+
+
+ );
+ }
+
+ return (
+
+
+
+ {
+ setOpen(o);
+ localStorage.setItem("streamplace:nav-open", String(o));
+ }}
+ className="h-svh"
+ >
+
+
+
+
+
+
+
+
+
+
+
);
}
diff --git a/js/web/src/routes/chat-popout.$user.tsx b/js/web/src/routes/chat-popout.$user.tsx
new file mode 100644
index 000000000..d1fc8a37c
--- /dev/null
+++ b/js/web/src/routes/chat-popout.$user.tsx
@@ -0,0 +1,183 @@
+// Standalone chat popout window. Minimal chrome — just the chat panel
+// and input, no player, no sidebar, no header.
+//
+// Query parameters (useful for OBS browser sources):
+// reverse=true Show newest messages at top
+// reverse=false Show newest messages at bottom (default)
+// hideAfter=N Hide the chat after N seconds
+// hideChatBox=true Hide the chat input (read-only)
+// hidePinnedComments=true Hide pinned comment notifications
+import { ChatInput } from "@/components/stream/chat-input";
+import { ChatPanel } from "@/components/stream/chat-panel";
+import { StreamNotifications } from "@/components/stream/stream-notifications";
+import { getStreamplaceUrl } from "@/lib/streamplace-url";
+import {
+ handleWebSocketMessages,
+ makeLivestreamStore,
+ type LivestreamStore,
+} from "@streamplace/core";
+import { createFileRoute } from "@tanstack/react-router";
+import { useEffect, useRef, useState } from "react";
+
+export const Route = createFileRoute("/chat-popout/$user")({
+ validateSearch: (
+ search: Record
,
+ ): {
+ reverse?: boolean;
+ hideAfter?: number;
+ hideChatBox?: boolean;
+ hidePinnedComments?: boolean;
+ } => {
+ const toBool = (v: unknown): boolean | undefined => {
+ if (v === undefined || v === null) return undefined;
+ if (typeof v === "boolean") return v;
+ return String(v).toLowerCase() === "true";
+ };
+ const toNum = (v: unknown): number | undefined => {
+ if (typeof v === "number" && !isNaN(v)) return v;
+ if (typeof v === "string" && !isNaN(Number(v))) return Number(v);
+ return undefined;
+ };
+ const result: {
+ reverse?: boolean;
+ hideAfter?: number;
+ hideChatBox?: boolean;
+ hidePinnedComments?: boolean;
+ } = {};
+ const reverse = toBool(search.reverse);
+ if (reverse !== undefined) result.reverse = reverse;
+ const hideAfter = toNum(search.hideAfter);
+ if (hideAfter !== undefined) result.hideAfter = hideAfter;
+ const hideChatBox = toBool(search.hideChatBox);
+ if (hideChatBox !== undefined) result.hideChatBox = hideChatBox;
+ const hidePinnedComments = toBool(search.hidePinnedComments);
+ if (hidePinnedComments !== undefined)
+ result.hidePinnedComments = hidePinnedComments;
+ return result;
+ },
+ component: ChatPopoutPage,
+});
+
+function ChatPopoutPage() {
+ const { user } = Route.useParams();
+ const { reverse, hideAfter, hideChatBox, hidePinnedComments } =
+ Route.useSearch();
+ const reverseVal = reverse ?? false;
+ const hideChatBoxVal = hideChatBox ?? false;
+ const hidePinnedCommentsVal = hidePinnedComments ?? false;
+ const store = useRef(null);
+ const [initialized, setInitialized] = useState(false);
+ const [hidden, setHidden] = useState(false);
+
+ useEffect(() => {
+ const s = makeLivestreamStore();
+ store.current = s;
+ setInitialized(true);
+ }, []);
+
+ // hideAfter timer
+ useEffect(() => {
+ if (!hideAfter || hideAfter <= 0) return;
+ const timer = setTimeout(() => setHidden(true), hideAfter * 1000);
+ return () => clearTimeout(timer);
+ }, [hideAfter]);
+
+ useEffect(() => {
+ if (!store.current) return;
+
+ const wsUrl = `${getStreamplaceUrl()}/api/websocket/${user}`;
+
+ let ws: WebSocket | null = null;
+ let reconnectTimeout: ReturnType | null = null;
+ let currentConnectId = 0;
+ let mounted = true;
+
+ const scheduleReconnect = () => {
+ if (!mounted || reconnectTimeout) return;
+ reconnectTimeout = setTimeout(connect, 3000);
+ };
+
+ const connect = () => {
+ if (reconnectTimeout) {
+ clearTimeout(reconnectTimeout);
+ reconnectTimeout = null;
+ }
+
+ const connectId = ++currentConnectId;
+
+ ws = new WebSocket(wsUrl);
+
+ ws.onopen = () => {};
+
+ let messageBuffer: any[] = [];
+ let flushTimer: ReturnType | null = null;
+ const flush = () => {
+ flushTimer = null;
+ const batch = messageBuffer;
+ messageBuffer = [];
+ store.current?.setState((s) => handleWebSocketMessages(s, batch));
+ };
+
+ ws.onmessage = (event) => {
+ if (connectId !== currentConnectId) return;
+ try {
+ const messages = JSON.parse(event.data);
+ const list = Array.isArray(messages) ? messages : [messages];
+ messageBuffer.push(...list);
+ if (!flushTimer) {
+ flushTimer = setTimeout(flush, 0);
+ }
+ } catch {}
+ };
+
+ ws.onclose = () => {
+ if (connectId !== currentConnectId) return;
+ store.current?.setState((s) => ({ ...s, websocketConnected: false }));
+ scheduleReconnect();
+ };
+
+ ws.onerror = () => {
+ if (connectId !== currentConnectId) return;
+ store.current?.setState((s) => ({ ...s, websocketConnected: false }));
+ ws?.close();
+ scheduleReconnect();
+ };
+ };
+
+ connect();
+
+ return () => {
+ mounted = false;
+ if (flushTimer) clearTimeout(flushTimer);
+ if (reconnectTimeout) clearTimeout(reconnectTimeout);
+ ws?.close();
+ };
+ }, [user]);
+
+ if (!initialized || !store.current) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {!hidePinnedCommentsVal &&
}
+
+
+
+ {!hideChatBoxVal && (
+
+
+
+ )}
+
+ );
+}
diff --git a/js/web/src/routes/index.tsx b/js/web/src/routes/index.tsx
index 078511d21..6a8b1fdf4 100644
--- a/js/web/src/routes/index.tsx
+++ b/js/web/src/routes/index.tsx
@@ -1,26 +1,20 @@
import { createFileRoute, Link } from "@tanstack/react-router";
import { StreamCard } from "../components/stream/stream-card";
import useAvatars from "../hooks/use-avatars";
+import { useLiveUsers } from "../hooks/use-live-users";
import { EMPTY_LOGIN_SEARCH } from "../lib/login-search";
-import {
- useLiveUsers,
- useLiveUsersError,
- useLiveUsersLoading,
-} from "../lib/store/hooks";
export const Route = createFileRoute("/")({
component: HomePage,
});
function HomePage() {
- const streams = useLiveUsers();
- const loading = useLiveUsersLoading();
- const error = useLiveUsersError();
+ const { data: streams, isLoading, error } = useLiveUsers();
const allDids = streams?.map((s) => s.author.did) ?? [];
const avatars = useAvatars(allDids);
- if (streams === null && loading) {
+ if (streams === undefined && isLoading) {
return (
@@ -43,7 +37,7 @@ function HomePage() {
return (
- {error && streams === null && (
+ {error && !streams && (
Could not load streams. You might be offline.
@@ -231,38 +225,47 @@ function HomePage() {
r="2"
>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
@@ -286,7 +289,7 @@ function HomePage() {
)}
- {streams === null && !loading && (
+ {!streams && !isLoading && !error && (
The video layer for everything.
diff --git a/js/web/src/routes/search.tsx b/js/web/src/routes/search.tsx
new file mode 100644
index 000000000..b5fe4d08e
--- /dev/null
+++ b/js/web/src/routes/search.tsx
@@ -0,0 +1,163 @@
+// Search page. Searches actors via searchActorsTypeahead and shows
+// results as profile cards linking to their streams.
+import useAvatars from "@/hooks/use-avatars";
+import { usePDSAgent } from "@/lib/store/hooks";
+import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
+import { Search } from "lucide-react";
+import { useCallback, useEffect, useRef, useState } from "react";
+
+interface SearchResult {
+ did: string;
+ handle: string;
+ displayName?: string;
+}
+
+export const Route = createFileRoute("/search")({
+ validateSearch: (
+ search: Record,
+ ): { q: string | undefined } => ({
+ q: typeof search.q === "string" ? search.q : undefined,
+ }),
+ component: SearchPage,
+});
+
+function SearchPage() {
+ const { q: initialQ } = Route.useSearch();
+ const navigate = useNavigate({ from: Route.fullPath });
+ const agent = usePDSAgent();
+ const [query, setQuery] = useState(initialQ ?? "");
+ const [results, setResults] = useState([]);
+ const [searching, setSearching] = useState(false);
+ const timeoutRef = useRef | null>(null);
+ const inputRef = useRef(null);
+
+ const dids = results.map((r) => r.did);
+ const avatars = useAvatars(dids);
+
+ const searchActors = useCallback(
+ async (q: string) => {
+ if (!agent || !q.trim()) {
+ setResults([]);
+ return;
+ }
+ try {
+ setSearching(true);
+ const response = await agent.place.stream.live.searchActorsTypeahead({
+ q,
+ limit: 20,
+ });
+ setResults(
+ response.data.actors.map((a: any) => ({
+ did: a.did,
+ handle: a.handle,
+ displayName: a.displayName,
+ })),
+ );
+ } catch (error) {
+ console.error("Search failed:", error);
+ setResults([]);
+ } finally {
+ setSearching(false);
+ }
+ },
+ [agent],
+ );
+
+ // Sync initial query param into the input and trigger a search.
+ useEffect(() => {
+ if (initialQ) {
+ searchActors(initialQ);
+ }
+ inputRef.current?.focus();
+ }, [initialQ, searchActors]);
+
+ const handleChange = (value: string) => {
+ setQuery(value);
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
+ if (value.trim()) {
+ timeoutRef.current = setTimeout(() => {
+ searchActors(value);
+ navigate({ search: { q: value }, replace: true });
+ }, 300);
+ } else {
+ setResults([]);
+ navigate({ search: { q: undefined }, replace: true });
+ }
+ };
+
+ return (
+
+
+
+ handleChange(e.target.value)}
+ placeholder="Search for streamers..."
+ className="w-full h-10 pl-9 pr-4 rounded-md border border-[var(--color-border)] bg-[var(--color-bg)] text-[var(--color-fg)] placeholder:text-[var(--color-fg-muted)] focus:outline-none focus:ring-1 focus:ring-[var(--color-ring)]"
+ />
+
+
+ {searching && results.length === 0 && (
+
+ )}
+
+ {!searching && query.trim() && results.length === 0 && (
+
+
+ No results for “{query}”
+
+
+ )}
+
+ {results.length > 0 && (
+
+ {results.map((actor) => (
+
+ {avatars[actor.did]?.avatar ? (
+
+ ) : (
+
+ {(
+ actor.displayName?.[0] ||
+ actor.handle[0] ||
+ "?"
+ ).toUpperCase()}
+
+ )}
+
+
+ {actor.displayName || actor.handle}
+
+
+ @{actor.handle}
+
+
+
+ ))}
+
+ )}
+
+ {!query.trim() && (
+
+
+
+ Find streamers by name or handle
+
+
+ )}
+
+ );
+}
diff --git a/js/web/src/styles.css b/js/web/src/styles.css
index 559249d7b..28082dc03 100644
--- a/js/web/src/styles.css
+++ b/js/web/src/styles.css
@@ -1,12 +1,10 @@
/* tw overwrites this but that's ok, we want both */
-@import "reset.css";
+@import "./fonts.css";
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@import "tw-animate-css";
-@import "./fonts.css";
-
@custom-variant dark (&:where(.dark, .dark *));
code {
@@ -135,7 +133,7 @@ code {
--color-accent-hover: var(--color-accent);
--color-accent-fg: var(--color-accent-foreground);
--color-danger: var(--color-chart-1);
- --color-success: var(--color-primary);
+ --color-success: var(--color-chart-4);
--color-info: var(--color-chart-3);
@@ -345,3 +343,29 @@ body {
animation: fade-in 0.8s ease-out both;
animation-delay: 1s;
}
+
+/* Custom scrollbar styles */
+* {
+ scrollbar-width: thin;
+ scrollbar-color: var(--color-sidebar-ring) transparent;
+}
+
+*::-webkit-scrollbar {
+ width: 6px;
+ height: 6px;
+}
+
+*::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+*::-webkit-scrollbar-thumb {
+ background: var(--color-sidebar-ring);
+ opacity: 0.2;
+ border-radius: 999px;
+}
+
+*::-webkit-scrollbar-thumb:hover {
+ background: var(--color-sidebar-ring);
+ opacity: 0.2;
+}
diff --git a/js/web/vite.config.ts b/js/web/vite.config.ts
index 3a265c50e..977fbb4f3 100644
--- a/js/web/vite.config.ts
+++ b/js/web/vite.config.ts
@@ -4,35 +4,57 @@ import react from "@vitejs/plugin-react";
import { resolve } from "node:path";
import { defineConfig } from "vite";
-// https://vite.dev/config/
-export default defineConfig({
- plugins: [
- // TanStack Router must be registered BEFORE @vitejs/plugin-react so the
- // generated route tree is in place when the React plugin runs.
- tanstackRouter({
- target: "react",
- autoCodeSplitting: true,
- routesDirectory: "./src/routes",
- generatedRouteTree: "./src/routeTree.gen.ts",
+// Conditionally add Sentry plugin for source maps upload in CI.
+function sentryPlugin() {
+ const authToken = process.env.SENTRY_AUTH_TOKEN;
+ const org = process.env.SENTRY_ORG;
+ const project = process.env.SENTRY_PROJECT;
+ if (!authToken || !org || !project) return [];
+ // Dynamic import so the plugin isn't required for local dev.
+ return import("@sentry/vite-plugin").then(({ sentryVitePlugin }) =>
+ sentryVitePlugin({
+ org,
+ project,
+ authToken,
+ telemetry: false,
+ sourcemaps: { assets: "./dist/**" },
}),
- react(),
- tailwindcss(),
- ],
- resolve: {
- alias: {
- // Workspace packages — Vite reads each package's package.json `main`/`types`
- // fields directly, so these aliases just make resolution explicit.
- "@streamplace/core": resolve(__dirname, "../core/src"),
- streamplace: resolve(__dirname, "../streamplace/src"),
- // @-prefix for src/ — makes imports a bit cleaner and avoids relative path hell.
- "@": resolve(__dirname, "./src"),
+ );
+}
+
+// https://vite.dev/config/
+export default defineConfig(async () => {
+ const sentry = await sentryPlugin();
+ return {
+ plugins: [
+ // TanStack Router must be registered BEFORE @vitejs/plugin-react so the
+ // generated route tree is in place when the React plugin runs.
+ tanstackRouter({
+ target: "react",
+ autoCodeSplitting: true,
+ routesDirectory: "./src/routes",
+ generatedRouteTree: "./src/routeTree.gen.ts",
+ }),
+ react(),
+ tailwindcss(),
+ ...(sentry ? [sentry] : []),
+ ],
+ resolve: {
+ alias: {
+ // Workspace packages — Vite reads each package's package.json `main`/`types`
+ // fields directly, so these aliases just make resolution explicit.
+ "@streamplace/core": resolve(__dirname, "../core/src"),
+ streamplace: resolve(__dirname, "../streamplace/src"),
+ // @-prefix for src/ — makes imports a bit cleaner and avoids relative path hell.
+ "@": resolve(__dirname, "./src"),
+ },
+ },
+ server: {
+ port: 5173,
+ strictPort: false,
+ },
+ preview: {
+ port: 5173,
},
- },
- server: {
- port: 5173,
- strictPort: false,
- },
- preview: {
- port: 5173,
- },
+ };
});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ac90f1562..eaab817fa 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -810,6 +810,9 @@ importers:
'@hello-pangea/dnd':
specifier: ^18.0.1
version: 18.0.1(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ '@sentry/react':
+ specifier: ^10.57.0
+ version: 10.57.0(react@19.2.0)
'@streamplace/core':
specifier: workspace:*
version: link:../core
@@ -819,6 +822,9 @@ importers:
'@tailwindcss/typography':
specifier: ^0.5.19
version: 0.5.19(tailwindcss@4.3.0)
+ '@tanstack/react-query':
+ specifier: ^5.90.21
+ version: 5.90.21(react@19.2.0)
'@tanstack/react-router':
specifier: ^1.170.0
version: 1.170.15(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
@@ -889,6 +895,9 @@ importers:
specifier: ^5.0.5
version: 5.0.11(@types/react@19.2.14)(immer@10.1.1)(react@19.2.0)(use-sync-external-store@1.6.0(react@19.2.0))
devDependencies:
+ '@sentry/vite-plugin':
+ specifier: ^5.3.0
+ version: 5.3.0(encoding@0.1.13)(rollup@4.59.0)
'@tailwindcss/vite':
specifier: ^4.3.0
version: 4.3.0(vite@6.4.1(@types/node@25.5.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)(yaml@2.8.2))
@@ -5481,31 +5490,64 @@ packages:
resolution: {integrity: sha512-UOJtYmdcxHCcV0NPfXFff/a95iXl/E0EhuQ1y0uE0BuZDMupWSF5t2BgC4HaE5Aw3RTjDF3XkSHWoIF6ohy7eA==}
engines: {node: '>=18'}
+ '@sentry-internal/browser-utils@10.57.0':
+ resolution: {integrity: sha512-tXObp954rMTSYKlbftjVXHtNl4t/6ssks3jkqyzmKb+PDPWzabGQO7sWwqVuTjT8Kx/8A3FmriS1bGmqxiJy3A==}
+ engines: {node: '>=18'}
+
'@sentry-internal/feedback@10.38.0':
resolution: {integrity: sha512-JXneg9zRftyfy1Fyfc39bBlF/Qd8g4UDublFFkVvdc1S6JQPlK+P6q22DKz3Pc8w3ySby+xlIq/eTu9Pzqi4KA==}
engines: {node: '>=18'}
+ '@sentry-internal/feedback@10.57.0':
+ resolution: {integrity: sha512-ZcF4QhkqGX3iiQSXB2N0N3Awp+j5iqnDRu6PA/qyLFrWqH5ZiiAAgu59OLD9E6XAdg6iFtLYw19MAMZVK8qNOQ==}
+ engines: {node: '>=18'}
+
'@sentry-internal/replay-canvas@10.38.0':
resolution: {integrity: sha512-OXWM9jEqNYh4VTvrMu7v+z1anz+QKQ/fZXIZdsO7JTT2lGNZe58UUMeoq386M+Saxen8F9SUH7yTORy/8KI5qw==}
engines: {node: '>=18'}
+ '@sentry-internal/replay-canvas@10.57.0':
+ resolution: {integrity: sha512-zsfa4JcfV0AEc9YhNxNabd5lSZL2Av84saAyexGAqcHs+67m9Gd0cGStOzMb/nCl7UAtmdP0aI+G7a3rcxxN/A==}
+ engines: {node: '>=18'}
+
'@sentry-internal/replay@10.38.0':
resolution: {integrity: sha512-YWIkL6/dnaiQyFiZXJ/nN+NXGv/15z45ia86bE/TMq01CubX/DUOilgsFz0pk2v/pg3tp/U2MskLO9Hz0cnqeg==}
engines: {node: '>=18'}
+ '@sentry-internal/replay@10.57.0':
+ resolution: {integrity: sha512-Wmnx/6ABynVH1iwuoNUqJNyjIUqsqoGML7qsyivBRKb5Wo2YQtPOQlQYfxfZSvWzGpcoSVdInkRjDssUQxQEQg==}
+ engines: {node: '>=18'}
+
'@sentry/babel-plugin-component-annotate@4.9.1':
resolution: {integrity: sha512-0gEoi2Lb54MFYPOmdTfxlNKxI7kCOvNV7gP8lxMXJ7nCazF5OqOOZIVshfWjDLrc0QrSV6XdVvwPV9GDn4wBMg==}
engines: {node: '>= 14'}
+ '@sentry/babel-plugin-component-annotate@5.3.0':
+ resolution: {integrity: sha512-p4q8gn8wcFqZGP/s2MnJCAAd8fTikaU6A0mM97RDHQgStcrYiaS0Sc5zUNfb1V+UOLPuvdEdL6MwyxfzjYJQTA==}
+ engines: {node: '>= 18'}
+
'@sentry/browser@10.38.0':
resolution: {integrity: sha512-3phzp1YX4wcQr9mocGWKbjv0jwtuoDBv7+Y6Yfrys/kwyaL84mDLjjQhRf4gL5SX7JdYkhBp4WaiNlR0UC4kTA==}
engines: {node: '>=18'}
+ '@sentry/browser@10.57.0':
+ resolution: {integrity: sha512-s36AQy/CKXTfyY9Z+qUhzNomntZXgfs0rbaK7q9ffnFkqcPwzE8qQtVs58y3Suut56u+AhwSztgQtERcuZ5VIA==}
+ engines: {node: '>=18'}
+
+ '@sentry/bundler-plugin-core@5.3.0':
+ resolution: {integrity: sha512-L5T60sWdAI3qWwdg3Ptwek/0TY59PERrxyqp4XMUkroayQvGd9r5dIW9Q1kSeXX9iJ442nXbFZKAOyCKV4Z13Q==}
+ engines: {node: '>= 18'}
+
'@sentry/cli-darwin@2.58.4':
resolution: {integrity: sha512-kbTD+P4X8O+nsNwPxCywtj3q22ecyRHWff98rdcmtRrvwz8CKi/T4Jxn/fnn2i4VEchy08OWBuZAqaA5Kh2hRQ==}
engines: {node: '>=10'}
os: [darwin]
+ '@sentry/cli-darwin@2.58.6':
+ resolution: {integrity: sha512-udAVvcyfNa0R+95GvPz/+43/N3TC0TYKdkQ7D7jhPSzbcMc7l2fxRNN5yB3UpCA5fWFnW4toeaqwDBhb/Wh3LA==}
+ engines: {node: '>=10'}
+ os: [darwin]
+
'@sentry/cli-darwin@3.3.3':
resolution: {integrity: sha512-P8DoL79eX5fhKCfBHHl7xwwTShDPOb2drJC8lizZ3v1iS1JLPrNweM1KEzDefR30zH1wghbLSwsYv/svWdM3wA==}
engines: {node: '>=18'}
@@ -5517,6 +5559,12 @@ packages:
cpu: [arm64]
os: [linux, freebsd, android]
+ '@sentry/cli-linux-arm64@2.58.6':
+ resolution: {integrity: sha512-q8mEcNNmeXMy5i+jWT30TVpH7LcP4HD21CD5XRSPAd/a912HF6EpK0ybf/1USO14WOhoXbAGi9txwaWabSe33g==}
+ engines: {node: '>=10'}
+ cpu: [arm64]
+ os: [linux, freebsd, android]
+
'@sentry/cli-linux-arm64@3.3.3':
resolution: {integrity: sha512-9jaX9RGyTpjo9u2urNi5ciBDpRdTt107YJpFXev+BFHJ6Lwz/owgRuYzPRfAen8hKkOOFheZ3iy07kl576eZzw==}
engines: {node: '>=18'}
@@ -5529,6 +5577,12 @@ packages:
cpu: [arm]
os: [linux, freebsd, android]
+ '@sentry/cli-linux-arm@2.58.6':
+ resolution: {integrity: sha512-pD0LAt5PcUzAinBwvDqc66x9+2CabHEv486yP0gRjWO7SakbaxmfVq/EXd8VLq/Tzi39LAu422UYK1lpW3MILw==}
+ engines: {node: '>=10'}
+ cpu: [arm]
+ os: [linux, freebsd, android]
+
'@sentry/cli-linux-arm@3.3.3':
resolution: {integrity: sha512-a7o/huozveLIImXHe0HDwEMVhvDopOP2tLcopvV7sQsVE8f/QOShR5FudKjmiaZz2opdLzPJO9pv5WuF9jAZPg==}
engines: {node: '>=18'}
@@ -5541,6 +5595,12 @@ packages:
cpu: [x86, ia32]
os: [linux, freebsd, android]
+ '@sentry/cli-linux-i686@2.58.6':
+ resolution: {integrity: sha512-q8vNJi1eOV/4vxAFWBsEwLHoSYapaZHIf4j76KJGJXFKTkEbsjCOOsKbwUIBTQQhRgV4DFWh3ryfsPS/que4Kg==}
+ engines: {node: '>=10'}
+ cpu: [x86, ia32]
+ os: [linux, freebsd, android]
+
'@sentry/cli-linux-i686@3.3.3':
resolution: {integrity: sha512-VngQYzR2kDm2oojCuYF20ebLTK8HKvEwxe785J6gxob8Ef9JvZkERyUqENYppBa9aVgN0pandqPAqOECWykTMA==}
engines: {node: '>=18'}
@@ -5553,6 +5613,12 @@ packages:
cpu: [x64]
os: [linux, freebsd, android]
+ '@sentry/cli-linux-x64@2.58.6':
+ resolution: {integrity: sha512-DZu956Mhi3ZRjTBe1WdbGV46ldVbA8d2rgp/fh51GsI25zjBHah4wZnPTSzpc+YqxU6pJpg579B/r3jrIK530Q==}
+ engines: {node: '>=10'}
+ cpu: [x64]
+ os: [linux, freebsd, android]
+
'@sentry/cli-linux-x64@3.3.3':
resolution: {integrity: sha512-rBxXQeIYGefUNI2cXHxEr0y3bhxDQjOD4G6j/gqLz/Dj+l8gJ/iKP64kTudnoViNIpn0pdYccG69th7zmzM/Fg==}
engines: {node: '>=18'}
@@ -5565,6 +5631,12 @@ packages:
cpu: [arm64]
os: [win32]
+ '@sentry/cli-win32-arm64@2.58.6':
+ resolution: {integrity: sha512-nj0Ff/kmAB73EPDhR8B4O9r+NUHK5GkPCkGWC+kXVemqAJWL5jcJ5KdxG0l/S0z6RoEoltID8/43/B+TaMlT7A==}
+ engines: {node: '>=10'}
+ cpu: [arm64]
+ os: [win32]
+
'@sentry/cli-win32-arm64@3.3.3':
resolution: {integrity: sha512-c52g+YS6BO0rzH8AEHqQPmpqZrw0GJjMWqy0tQ5jcqaGdaLVnxk0mMEubv8R6Dv5MR2LShoKjiNsaeVfrWIMUg==}
engines: {node: '>=18'}
@@ -5577,6 +5649,12 @@ packages:
cpu: [x86, ia32]
os: [win32]
+ '@sentry/cli-win32-i686@2.58.6':
+ resolution: {integrity: sha512-WNZiDzPbgsEMQWq4avsQ391v/xWKJDIWWWo9GYl+N/w5qcYKkoDW7wQG7T9FasI6ENn68phChTOAPXXxbfAdOg==}
+ engines: {node: '>=10'}
+ cpu: [x86, ia32]
+ os: [win32]
+
'@sentry/cli-win32-i686@3.3.3':
resolution: {integrity: sha512-DygYzSY/+tS7oFj/mfeg/yzYxsQx3fO8cI+IWc2pns/at+JcJ9O5xyM/x/q55wOxpnwla7RL1D3rsqK2mqkYfg==}
engines: {node: '>=18'}
@@ -5589,6 +5667,12 @@ packages:
cpu: [x64]
os: [win32]
+ '@sentry/cli-win32-x64@2.58.6':
+ resolution: {integrity: sha512-R35WJ17oF4D2eqI1DR2sQQqr0fjRTt5xoP16WrTu91XM2lndRMFsnjh+/GttbxapLCBNlrjzia99MJ0PZHZpgA==}
+ engines: {node: '>=10'}
+ cpu: [x64]
+ os: [win32]
+
'@sentry/cli-win32-x64@3.3.3':
resolution: {integrity: sha512-i0glPcHwkqbVA2Y+0Yz7CD/l8TSkfft1a+lTU9yk/+DDU8WGkyArEAxAji9bGo4p+k5HIFC8OC2MwpKdcdFM4Q==}
engines: {node: '>=18'}
@@ -5600,6 +5684,11 @@ packages:
engines: {node: '>= 10'}
hasBin: true
+ '@sentry/cli@2.58.6':
+ resolution: {integrity: sha512-baBcNPLLfUi9WuL+Tpri9BFaAdvugZIKelC5X0tt0Zdy+K0K+PCVSrnNmwMWU/HyaF/SEv6b6UHnXIdqanBlcg==}
+ engines: {node: '>= 10'}
+ hasBin: true
+
'@sentry/cli@3.3.3':
resolution: {integrity: sha512-4CZtfgiOraX+BntMjYQhfLDArXwpqt3sEo5Zdj2pqWSZSd4yI3ncfQ21CsxLcI/sUQrjmD5Vzidu4/1OShyxtA==}
engines: {node: '>= 18'}
@@ -5609,6 +5698,10 @@ packages:
resolution: {integrity: sha512-1pubWDZE5y5HZEPMAZERP4fVl2NH3Ihp1A+vMoVkb3Qc66Diqj1WierAnStlZP7tCx0TBa0dK85GTW/ZFYyB9g==}
engines: {node: '>=18'}
+ '@sentry/core@10.57.0':
+ resolution: {integrity: sha512-kntItTA2kiT0YpL7encXaF6mkdZMB+y48lwj8w1wkfBpfJAC7sifdgrzLQZqmsqVNE3crg9VfufaAGA+78uFMg==}
+ engines: {node: '>=18'}
+
'@sentry/react-native@7.13.0':
resolution: {integrity: sha512-pKY+rln3CEnhnG21eUXcl/yeG5fkNruqXXjEikE5FjsyLmJi2POAGwT4tlupyHtG3fQT5mp06QI7bQdpAuH4Xw==}
hasBin: true
@@ -5626,10 +5719,29 @@ packages:
peerDependencies:
react: ^16.14.0 || 17.x || 18.x || 19.x
+ '@sentry/react@10.57.0':
+ resolution: {integrity: sha512-6QThwQ4XWQ2rwKZEVQ9P9WKl7JlowC7S5LpAvmMdrwlfJBpLDFOsM7tycnIvbXTXf0ZOOuLFPa4L4YYbdyNGmA==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ react: ^16.14.0 || 17.x || 18.x || 19.x
+
+ '@sentry/rollup-plugin@5.3.0':
+ resolution: {integrity: sha512-hgPGPYdQJ/G1cGYOxAb7d4z3V+/k/E5/P/5TFPEEBLuIbFFk+JG0CISUDJdzXJjO382Lb99PBJuXGbueBmO79w==}
+ engines: {node: '>= 18'}
+ peerDependencies:
+ rollup: '>=3.2.0'
+ peerDependenciesMeta:
+ rollup:
+ optional: true
+
'@sentry/types@10.38.0':
resolution: {integrity: sha512-DoeyTv/TvnoVDhHgdyv/wehieAKdyjLjEMtPOqqq/AjkP02BxeC0JYUrrWKOjV0wdLq5ZP8jKcCX8GN7awZonQ==}
engines: {node: '>=18'}
+ '@sentry/vite-plugin@5.3.0':
+ resolution: {integrity: sha512-qcoSzo4n2MulVQ70UUPLq6dTleb2a2HwL2wuwvAgWhPChrYTuk6A6mDg6aQb9fairPAwFPiU9PzOANpoDJcz1A==}
+ engines: {node: '>= 18'}
+
'@shikijs/core@3.23.0':
resolution: {integrity: sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA==}
@@ -22610,22 +22722,42 @@ snapshots:
dependencies:
'@sentry/core': 10.38.0
+ '@sentry-internal/browser-utils@10.57.0':
+ dependencies:
+ '@sentry/core': 10.57.0
+
'@sentry-internal/feedback@10.38.0':
dependencies:
'@sentry/core': 10.38.0
+ '@sentry-internal/feedback@10.57.0':
+ dependencies:
+ '@sentry/core': 10.57.0
+
'@sentry-internal/replay-canvas@10.38.0':
dependencies:
'@sentry-internal/replay': 10.38.0
'@sentry/core': 10.38.0
+ '@sentry-internal/replay-canvas@10.57.0':
+ dependencies:
+ '@sentry-internal/replay': 10.57.0
+ '@sentry/core': 10.57.0
+
'@sentry-internal/replay@10.38.0':
dependencies:
'@sentry-internal/browser-utils': 10.38.0
'@sentry/core': 10.38.0
+ '@sentry-internal/replay@10.57.0':
+ dependencies:
+ '@sentry-internal/browser-utils': 10.57.0
+ '@sentry/core': 10.57.0
+
'@sentry/babel-plugin-component-annotate@4.9.1': {}
+ '@sentry/babel-plugin-component-annotate@5.3.0': {}
+
'@sentry/browser@10.38.0':
dependencies:
'@sentry-internal/browser-utils': 10.38.0
@@ -22634,51 +22766,96 @@ snapshots:
'@sentry-internal/replay-canvas': 10.38.0
'@sentry/core': 10.38.0
+ '@sentry/browser@10.57.0':
+ dependencies:
+ '@sentry-internal/browser-utils': 10.57.0
+ '@sentry-internal/feedback': 10.57.0
+ '@sentry-internal/replay': 10.57.0
+ '@sentry-internal/replay-canvas': 10.57.0
+ '@sentry/core': 10.57.0
+
+ '@sentry/bundler-plugin-core@5.3.0(encoding@0.1.13)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@sentry/babel-plugin-component-annotate': 5.3.0
+ '@sentry/cli': 2.58.6(encoding@0.1.13)
+ dotenv: 16.4.5
+ find-up: 5.0.0
+ glob: 13.0.6
+ magic-string: 0.30.21
+ transitivePeerDependencies:
+ - encoding
+ - supports-color
+
'@sentry/cli-darwin@2.58.4':
optional: true
+ '@sentry/cli-darwin@2.58.6':
+ optional: true
+
'@sentry/cli-darwin@3.3.3':
optional: true
'@sentry/cli-linux-arm64@2.58.4':
optional: true
+ '@sentry/cli-linux-arm64@2.58.6':
+ optional: true
+
'@sentry/cli-linux-arm64@3.3.3':
optional: true
'@sentry/cli-linux-arm@2.58.4':
optional: true
+ '@sentry/cli-linux-arm@2.58.6':
+ optional: true
+
'@sentry/cli-linux-arm@3.3.3':
optional: true
'@sentry/cli-linux-i686@2.58.4':
optional: true
+ '@sentry/cli-linux-i686@2.58.6':
+ optional: true
+
'@sentry/cli-linux-i686@3.3.3':
optional: true
'@sentry/cli-linux-x64@2.58.4':
optional: true
+ '@sentry/cli-linux-x64@2.58.6':
+ optional: true
+
'@sentry/cli-linux-x64@3.3.3':
optional: true
'@sentry/cli-win32-arm64@2.58.4':
optional: true
+ '@sentry/cli-win32-arm64@2.58.6':
+ optional: true
+
'@sentry/cli-win32-arm64@3.3.3':
optional: true
'@sentry/cli-win32-i686@2.58.4':
optional: true
+ '@sentry/cli-win32-i686@2.58.6':
+ optional: true
+
'@sentry/cli-win32-i686@3.3.3':
optional: true
'@sentry/cli-win32-x64@2.58.4':
optional: true
+ '@sentry/cli-win32-x64@2.58.6':
+ optional: true
+
'@sentry/cli-win32-x64@3.3.3':
optional: true
@@ -22702,6 +22879,26 @@ snapshots:
- encoding
- supports-color
+ '@sentry/cli@2.58.6(encoding@0.1.13)':
+ dependencies:
+ https-proxy-agent: 5.0.1
+ node-fetch: 2.7.0(encoding@0.1.13)
+ progress: 2.0.3
+ proxy-from-env: 1.1.0
+ which: 2.0.2
+ optionalDependencies:
+ '@sentry/cli-darwin': 2.58.6
+ '@sentry/cli-linux-arm': 2.58.6
+ '@sentry/cli-linux-arm64': 2.58.6
+ '@sentry/cli-linux-i686': 2.58.6
+ '@sentry/cli-linux-x64': 2.58.6
+ '@sentry/cli-win32-arm64': 2.58.6
+ '@sentry/cli-win32-i686': 2.58.6
+ '@sentry/cli-win32-x64': 2.58.6
+ transitivePeerDependencies:
+ - encoding
+ - supports-color
+
'@sentry/cli@3.3.3':
dependencies:
progress: 2.0.3
@@ -22720,6 +22917,8 @@ snapshots:
'@sentry/core@10.38.0': {}
+ '@sentry/core@10.57.0': {}
+
'@sentry/react-native@7.13.0(encoding@0.1.13)(expo@55.0.6)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0)':
dependencies:
'@sentry/babel-plugin-component-annotate': 4.9.1
@@ -22742,10 +22941,35 @@ snapshots:
'@sentry/core': 10.38.0
react: 19.2.0
+ '@sentry/react@10.57.0(react@19.2.0)':
+ dependencies:
+ '@sentry/browser': 10.57.0
+ '@sentry/core': 10.57.0
+ react: 19.2.0
+
+ '@sentry/rollup-plugin@5.3.0(encoding@0.1.13)(rollup@4.59.0)':
+ dependencies:
+ '@sentry/bundler-plugin-core': 5.3.0(encoding@0.1.13)
+ magic-string: 0.30.21
+ optionalDependencies:
+ rollup: 4.59.0
+ transitivePeerDependencies:
+ - encoding
+ - supports-color
+
'@sentry/types@10.38.0':
dependencies:
'@sentry/core': 10.38.0
+ '@sentry/vite-plugin@5.3.0(encoding@0.1.13)(rollup@4.59.0)':
+ dependencies:
+ '@sentry/bundler-plugin-core': 5.3.0(encoding@0.1.13)
+ '@sentry/rollup-plugin': 5.3.0(encoding@0.1.13)(rollup@4.59.0)
+ transitivePeerDependencies:
+ - encoding
+ - rollup
+ - supports-color
+
'@shikijs/core@3.23.0':
dependencies:
'@shikijs/types': 3.23.0