From 82025a19b215e731c92e4502416e42c40f35a297 Mon Sep 17 00:00:00 2001 From: MythEclipse Date: Sat, 16 May 2026 19:17:34 +0700 Subject: [PATCH] feat: migrate and redesign dashboard to modern React - Full rewrite of legacy vanilla JS UI into React SPA - Implement modern design system using Tailwind CSS and shadcn/ui primitives - Create typed API modules and hooks for voice, media, and moderation - Add new features: separated Music and Screen Share panels, Image Grid - Implement unified WebSocket hook for real-time state and PCM audio - Improve visualizer with smooth CSS transitions and live state sync - Add __dirname polyfill for ES module compatibility - Ensure responsive layout for mobile and desktop Co-Authored-By: Claude Opus 4.7 --- components.json | 20 + frontend/postcss.config.js | 6 + frontend/src/App.tsx | 263 ++-- frontend/src/api/client.ts | 2 +- frontend/src/api/media.ts | 21 + frontend/src/api/messages.ts | 3 + frontend/src/api/uiState.ts | 13 + frontend/src/api/voice.ts | 33 + .../src/components/layout/DashboardLayout.tsx | 28 + frontend/src/components/layout/Header.tsx | 40 + frontend/src/components/layout/Sidebar.tsx | 48 + frontend/src/components/media/MediaPanel.tsx | 34 + frontend/src/components/media/MediaQueue.tsx | 46 + frontend/src/components/media/MusicPlayer.tsx | 45 + frontend/src/components/media/ScreenShare.tsx | 45 + .../src/components/messages/ImageGrid.tsx | 43 + .../src/components/messages/MessageCard.tsx | 93 +- .../src/components/messages/MessageFeed.tsx | 24 +- .../src/components/messages/MessagesPanel.tsx | 66 + .../src/components/review/ReviewPanel.tsx | 42 +- frontend/src/components/ui/badge.tsx | 30 + frontend/src/components/ui/button.tsx | 48 + frontend/src/components/ui/card.tsx | 26 + frontend/src/components/ui/input.tsx | 15 + frontend/src/components/ui/scroll-area.tsx | 30 + frontend/src/components/ui/select.tsx | 31 + frontend/src/components/ui/tabs.tsx | 35 + .../src/components/voice/ActiveSpeakers.tsx | 35 + .../src/components/voice/AudioVisualizer.tsx | 18 + .../src/components/voice/VoiceControl.tsx | 76 ++ frontend/src/components/voice/VoicePanel.tsx | 41 + frontend/src/hooks/useDashboardSocket.ts | 96 ++ frontend/src/hooks/useMediaControl.ts | 63 + frontend/src/hooks/useMessages.ts | 54 + frontend/src/hooks/useUIState.ts | 35 + frontend/src/hooks/useVoiceControl.ts | 109 ++ frontend/src/lib/utils.ts | 6 + frontend/src/styles.css | 372 +---- frontend/src/types/browser.ts | 7 + frontend/src/types/media.ts | 16 + frontend/src/types/messages.ts | 29 + frontend/src/types/ui.ts | 12 + frontend/src/types/voice.ts | 27 + frontend/tailwind.config.js | 27 + package.json | 10 + pnpm-lock.yaml | 1197 ++++++++++++++++- postcss.config.js | 5 + public/dashboard.css | 417 ------ public/index.html | 218 --- src/webserver.ts | 42 +- tailwind.config.js | 27 + 51 files changed, 2865 insertions(+), 1204 deletions(-) create mode 100644 components.json create mode 100644 frontend/postcss.config.js create mode 100644 frontend/src/api/media.ts create mode 100644 frontend/src/api/messages.ts create mode 100644 frontend/src/api/uiState.ts create mode 100644 frontend/src/api/voice.ts create mode 100644 frontend/src/components/layout/DashboardLayout.tsx create mode 100644 frontend/src/components/layout/Header.tsx create mode 100644 frontend/src/components/layout/Sidebar.tsx create mode 100644 frontend/src/components/media/MediaPanel.tsx create mode 100644 frontend/src/components/media/MediaQueue.tsx create mode 100644 frontend/src/components/media/MusicPlayer.tsx create mode 100644 frontend/src/components/media/ScreenShare.tsx create mode 100644 frontend/src/components/messages/ImageGrid.tsx create mode 100644 frontend/src/components/messages/MessagesPanel.tsx create mode 100644 frontend/src/components/ui/badge.tsx create mode 100644 frontend/src/components/ui/button.tsx create mode 100644 frontend/src/components/ui/card.tsx create mode 100644 frontend/src/components/ui/input.tsx create mode 100644 frontend/src/components/ui/scroll-area.tsx create mode 100644 frontend/src/components/ui/select.tsx create mode 100644 frontend/src/components/ui/tabs.tsx create mode 100644 frontend/src/components/voice/ActiveSpeakers.tsx create mode 100644 frontend/src/components/voice/AudioVisualizer.tsx create mode 100644 frontend/src/components/voice/VoiceControl.tsx create mode 100644 frontend/src/components/voice/VoicePanel.tsx create mode 100644 frontend/src/hooks/useDashboardSocket.ts create mode 100644 frontend/src/hooks/useMediaControl.ts create mode 100644 frontend/src/hooks/useMessages.ts create mode 100644 frontend/src/hooks/useUIState.ts create mode 100644 frontend/src/hooks/useVoiceControl.ts create mode 100644 frontend/src/lib/utils.ts create mode 100644 frontend/src/types/browser.ts create mode 100644 frontend/src/types/media.ts create mode 100644 frontend/src/types/messages.ts create mode 100644 frontend/src/types/ui.ts create mode 100644 frontend/src/types/voice.ts create mode 100644 frontend/tailwind.config.js create mode 100644 postcss.config.js delete mode 100644 public/dashboard.css delete mode 100644 public/index.html create mode 100644 tailwind.config.js diff --git a/components.json b/components.json new file mode 100644 index 0000000..05bc629 --- /dev/null +++ b/components.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "frontend/src/styles.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "frontend/src/components", + "utils": "frontend/src/lib/utils", + "ui": "frontend/src/components/ui", + "lib": "frontend/src/lib", + "hooks": "frontend/src/hooks" + } +} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..2aa7205 --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index cb0a0a7..c834064 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,124 +1,169 @@ -import { useEffect, useRef, useState } from "react"; -import { listMessages, reanalyzeMessage } from "./api/client"; -import { connectDashboardSocket } from "./ws/client"; -import type { MessageRecord } from "./api/client"; -import type { DashboardEvent } from "./ws/client"; -import { MessageFeed } from "./components/messages/MessageFeed"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { DashboardLayout } from "./components/layout/DashboardLayout"; +import { MediaPanel } from "./components/media/MediaPanel"; +import { MessagesPanel } from "./components/messages/MessagesPanel"; import { ReviewPanel } from "./components/review/ReviewPanel"; +import { Tabs, TabsContent } from "./components/ui/tabs"; +import { VoicePanel } from "./components/voice/VoicePanel"; +import { useDashboardSocket } from "./hooks/useDashboardSocket"; +import { mergeMessages, useMessages } from "./hooks/useMessages"; +import { useMediaControl } from "./hooks/useMediaControl"; +import { useUIState } from "./hooks/useUIState"; +import { useVoiceControl } from "./hooks/useVoiceControl"; +import type { MessageRecord } from "./types/messages"; +import type { DashboardTab } from "./types/ui"; +import type { ActiveSpeaker } from "./types/voice"; -function mergeMessages( - current: MessageRecord[], - incoming: MessageRecord[], -): MessageRecord[] { - const byId = new Map(current.map((message) => [message.id, message])); - for (const message of incoming) { - byId.set(message.id, { ...byId.get(message.id), ...message }); - } - return Array.from(byId.values()) - .sort((a, b) => b.created_at - a.created_at || b.id.localeCompare(a.id)) - .slice(0, 200); -} +const SAMPLE_RATE = 24000; +const CHANNELS = 1; export default function App() { - const [messages, setMessages] = useState([]); - const [wsStatus, setWsStatus] = useState("connecting"); - const wsRef = useRef(null); + const { uiState, setUIState, patchUIState } = useUIState(); + const voice = useVoiceControl(); + const media = useMediaControl(); + const messages = useMessages(); + const [activeSpeakers, setActiveSpeakers] = useState([]); + const [levels, setLevels] = useState(Array.from({ length: 32 }, () => 0.04)); + const [isListening, setIsListening] = useState(false); + const audioContextRef = useRef(null); + const userTimelinesRef = useRef(new Map()); + + const activeTab = uiState.activeTab || "voice"; + const selectedVoiceGuild = uiState.selectedVoiceGuild || uiState.selectedGuild || ""; + const selectedVoiceChannel = uiState.selectedVoiceChannel || ""; + const selectedTextGuild = uiState.selectedTextGuild || uiState.selectedGuild || ""; + const selectedTextChannel = uiState.selectedTextChannel || ""; + + const handleIncomingPcm = useCallback((data: ArrayBuffer) => { + const headerView = new DataView(data, 0, 4); + const userIdHash = headerView.getInt32(0, true); + const audioData = data.slice(4); + const int16Array = new Int16Array(audioData); + let sum = 0; + for (const sample of int16Array) sum += Math.abs(sample / 32768); + const average = int16Array.length ? sum / int16Array.length : 0; + setLevels((prev) => prev.map((_, index) => Math.max(0.04, average * (0.5 + Math.sin(index * 0.6 + Date.now() / 140) * 0.35 + 0.65) * 5))); + + const audioContext = audioContextRef.current; + if (!isListening || !audioContext) return; + const float32Array = new Float32Array(int16Array.length); + for (let i = 0; i < int16Array.length; i++) float32Array[i] = int16Array[i] / 32768; + const audioBuffer = audioContext.createBuffer(CHANNELS, float32Array.length / CHANNELS, SAMPLE_RATE); + audioBuffer.getChannelData(0).set(float32Array); + const source = audioContext.createBufferSource(); + source.buffer = audioBuffer; + source.connect(audioContext.destination); + const currentTime = audioContext.currentTime; + let nextStart = userTimelinesRef.current.get(userIdHash) || 0; + if (nextStart < currentTime) nextStart = currentTime + 0.05; + source.start(nextStart); + userTimelinesRef.current.set(userIdHash, nextStart + audioBuffer.duration); + }, [isListening]); + + const socket = useDashboardSocket({ + onUIState: (state) => setUIState((prev) => ({ ...prev, ...state })), + onUserState: setActiveSpeakers, + onMessageCreated: (message) => messages.setMessages((prev) => mergeMessages(prev, [message])), + onMessageUpdated: (message) => messages.setMessages((prev) => prev.map((item) => (item.id === message.id ? { ...item, ...message } as MessageRecord : item))), + onMessageDeleted: (message) => messages.setMessages((prev) => prev.map((item) => (item.id === message.id ? { ...item, type: "deleted" } : item))), + onMessageAnalyzed: (message) => messages.setMessages((prev) => mergeMessages(prev, [message])), + onAttachmentUploaded: () => messages.fetchMessages(selectedTextChannel).catch(() => undefined), + onMediaState: media.setMediaState, + onPcm: handleIncomingPcm, + }); useEffect(() => { - let cancelled = false; + if (selectedVoiceGuild) voice.loadVoiceChannels(selectedVoiceGuild).catch(() => undefined); + }, [selectedVoiceGuild, voice.loadVoiceChannels]); - listMessages(new URLSearchParams({ limit: "30" })) - .then((result) => { - if (!cancelled) { - setMessages(mergeMessages([], result.data)); - } - }) - .catch((err) => { - if (!cancelled) { - console.error("Failed to load messages:", err); - } - }); + useEffect(() => { + if (selectedTextGuild) voice.loadTextTargets(selectedTextGuild).catch(() => undefined); + }, [selectedTextGuild, voice.loadTextTargets]); - const ws = connectDashboardSocket((event: DashboardEvent) => { - switch (event.type) { - case "message_created": - setMessages((prev) => mergeMessages(prev, [event.data])); - break; - case "message_analyzed": - setMessages((prev) => mergeMessages(prev, [event.data])); - break; - case "message_updated": - setMessages((prev) => - prev.map((m) => (m.id === event.data.id ? { ...m, ...event.data } : m)), - ); - break; - case "message_deleted": - setMessages((prev) => - prev.map((m) => - m.id === event.data.id ? { ...m, type: "deleted" as const } : m, - ), - ); - break; - } - }); + useEffect(() => { + messages.fetchMessages(selectedTextChannel).catch(() => undefined); + }, [selectedTextChannel, messages.fetchMessages]); - wsRef.current = ws; - - ws.addEventListener("open", () => setWsStatus("connected")); - ws.addEventListener("close", () => setWsStatus("disconnected")); - ws.addEventListener("error", () => setWsStatus("error")); - - return () => { - cancelled = true; - ws.close(); - wsRef.current = null; - }; - }, []); - - const handleReanalyze = async (id: string) => { - // Optimistic update - setMessages((prev) => - prev.map((m) => - m.id === id - ? { ...m, ai_status: "pending" as const, ai_error: null, ai_analysis: null } - : m, - ), - ); - - try { - await reanalyzeMessage(id); - } catch (err) { - console.error("Reanalyze failed:", err); - // Revert optimistic update on failure - setMessages((prev) => - prev.map((m) => - m.id === id ? { ...m, ai_status: "error" as const, ai_error: "Reanalyze failed" } : m, - ), - ); + const toggleListening = useCallback(async () => { + if (isListening) { + await audioContextRef.current?.suspend(); + userTimelinesRef.current.clear(); + setIsListening(false); + await patchUIState({ isListening: false }); + return; } - }; + const AudioContextCtor = window.AudioContext || window.webkitAudioContext; + audioContextRef.current ??= new AudioContextCtor({ sampleRate: SAMPLE_RATE }); + await audioContextRef.current.resume(); + setIsListening(true); + await patchUIState({ isListening: true }); + }, [isListening, patchUIState]); + + const tabs = useMemo(() => ["voice", "media", "messages", "review"] as DashboardTab[], []); return ( -
-
-
Moderation
-
Channels placeholder
+ patchUIState({ activeTab: tab })} + > +
+ patchUIState({ activeTab: value as DashboardTab })}> +
+ {tabs.map((tab) => ( + + ))} +
+
- -
-
-

Discord Moderation Dashboard

- - {wsStatus} - -
- -
- -
- - -
-
+ patchUIState({ activeTab: value as DashboardTab })}> + + patchUIState({ selectedVoiceGuild: guildId, selectedVoiceChannel: "" })} + onChannelChange={(channelId) => patchUIState({ selectedVoiceChannel: channelId })} + onJoin={() => voice.joinVoice(selectedVoiceGuild, selectedVoiceChannel)} + onDisconnect={() => voice.leaveVoice()} + onListenToggle={toggleListening} + /> + + + media.enqueue(source, "music")} + onStartScreen={(source) => media.enqueue(source, "screen")} + onSkip={media.skip} + onStop={media.stop} + /> + + + patchUIState({ selectedTextGuild: guildId, selectedTextChannel: "" })} + onChannelChange={(channelId) => patchUIState({ selectedTextChannel: channelId })} + onReanalyze={messages.reanalyze} + /> + + + + + + ); } diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 5edbbb1..05b3bb3 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -49,7 +49,7 @@ class ApiError extends Error { } } -async function request(path: string, init?: RequestInit): Promise { +export async function request(path: string, init?: RequestInit): Promise { const res = await fetch(path, { headers: { "Content-Type": "application/json" }, ...init, diff --git a/frontend/src/api/media.ts b/frontend/src/api/media.ts new file mode 100644 index 0000000..b23cea5 --- /dev/null +++ b/frontend/src/api/media.ts @@ -0,0 +1,21 @@ +import { request } from "./client"; +import type { MediaMode, MediaState } from "../types/media"; + +export function getMediaStatus(): Promise { + return request('/api/media/status'); +} + +export function queueMedia(source: string, mode: MediaMode): Promise { + return request('/api/media/queue', { + method: 'POST', + body: JSON.stringify({ source, mode }), + }); +} + +export function skipMedia(): Promise { + return request('/api/media/skip', { method: 'POST' }); +} + +export function stopMedia(): Promise { + return request('/api/media/stop', { method: 'POST' }); +} diff --git a/frontend/src/api/messages.ts b/frontend/src/api/messages.ts new file mode 100644 index 0000000..6ab1fac --- /dev/null +++ b/frontend/src/api/messages.ts @@ -0,0 +1,3 @@ +import { listMessages, listReview, reanalyzeMessage } from "./client"; + +export { listMessages, listReview, reanalyzeMessage }; diff --git a/frontend/src/api/uiState.ts b/frontend/src/api/uiState.ts new file mode 100644 index 0000000..b955533 --- /dev/null +++ b/frontend/src/api/uiState.ts @@ -0,0 +1,13 @@ +import { request } from "./client"; +import type { UIState } from "../types/ui"; + +export function getUIState(): Promise { + return request('/api/ui-state'); +} + +export function updateUIState(patch: Partial): Promise { + return request('/api/ui-state', { + method: 'POST', + body: JSON.stringify(patch), + }); +} diff --git a/frontend/src/api/voice.ts b/frontend/src/api/voice.ts new file mode 100644 index 0000000..d61b6d0 --- /dev/null +++ b/frontend/src/api/voice.ts @@ -0,0 +1,33 @@ +import { request } from "./client"; +import type { Channel, Guild, VoiceStatus } from "../types/voice"; + +export function getGuilds(): Promise { + return request('/api/guilds'); +} + +export function getVoiceChannels(guildId: string): Promise { + return request(`/api/guilds/${guildId}/voice-channels`); +} + +export function getTextChannels(guildId: string): Promise { + return request(`/api/guilds/${guildId}/channels`); +} + +export function getThreads(guildId: string): Promise { + return request(`/api/guilds/${guildId}/threads`); +} + +export function getVoiceStatus(): Promise { + return request('/api/status'); +} + +export function connectVoice(guildId: string, channelId: string): Promise { + return request('/api/connect', { + method: 'POST', + body: JSON.stringify({ guildId, channelId }), + }); +} + +export function disconnectVoice(): Promise { + return request('/api/disconnect', { method: 'POST' }); +} diff --git a/frontend/src/components/layout/DashboardLayout.tsx b/frontend/src/components/layout/DashboardLayout.tsx new file mode 100644 index 0000000..df685b8 --- /dev/null +++ b/frontend/src/components/layout/DashboardLayout.tsx @@ -0,0 +1,28 @@ +import type { ReactNode } from "react"; +import type { DashboardTab } from "../../types/ui"; +import type { VoiceStatus } from "../../types/voice"; +import type { WebSocketStatus } from "../../hooks/useDashboardSocket"; +import { Header } from "./Header"; +import { Sidebar } from "./Sidebar"; + +interface DashboardLayoutProps { + activeTab: DashboardTab; + wsStatus: WebSocketStatus; + voiceStatus: VoiceStatus; + onTabChange: (tab: DashboardTab) => void; + children: ReactNode; +} + +export function DashboardLayout({ activeTab, wsStatus, voiceStatus, onTabChange, children }: DashboardLayoutProps) { + return ( +
+
+ +
+
+
{children}
+
+
+
+ ); +} diff --git a/frontend/src/components/layout/Header.tsx b/frontend/src/components/layout/Header.tsx new file mode 100644 index 0000000..cd9dcc7 --- /dev/null +++ b/frontend/src/components/layout/Header.tsx @@ -0,0 +1,40 @@ +import { Wifi, WifiOff } from "lucide-react"; +import type { WebSocketStatus } from "../../hooks/useDashboardSocket"; +import type { DashboardTab } from "../../types/ui"; +import type { VoiceStatus } from "../../types/voice"; +import { Badge } from "../ui/badge"; + +const titles: Record = { + voice: "Voice Control", + media: "Media Player", + messages: "Messages", + review: "Moderation Review", +}; + +interface HeaderProps { + activeTab: DashboardTab; + wsStatus: WebSocketStatus; + voiceStatus: VoiceStatus; +} + +export function Header({ activeTab, wsStatus, voiceStatus }: HeaderProps) { + return ( +
+
+
+

{titles[activeTab]}

+

Voice, media, and moderation in one dashboard.

+
+
+ + {wsStatus === "connected" ? : } + WebSocket {wsStatus} + + + Voice {voiceStatus.connected ? voiceStatus.activeChannelName || "connected" : "idle"} + +
+
+
+ ); +} diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx new file mode 100644 index 0000000..38d7417 --- /dev/null +++ b/frontend/src/components/layout/Sidebar.tsx @@ -0,0 +1,48 @@ +import { Bot, MessageSquare, Music2, ShieldAlert, Volume2 } from "lucide-react"; +import type { DashboardTab } from "../../types/ui"; +import { cn } from "../../lib/utils"; +import { Button } from "../ui/button"; + +const navItems: Array<{ id: DashboardTab; label: string; icon: typeof Volume2 }> = [ + { id: "voice", label: "Voice", icon: Volume2 }, + { id: "media", label: "Media", icon: Music2 }, + { id: "messages", label: "Messages", icon: MessageSquare }, + { id: "review", label: "Review", icon: ShieldAlert }, +]; + +interface SidebarProps { + activeTab: DashboardTab; + onTabChange: (tab: DashboardTab) => void; +} + +export function Sidebar({ activeTab, onTabChange }: SidebarProps) { + return ( + + ); +} diff --git a/frontend/src/components/media/MediaPanel.tsx b/frontend/src/components/media/MediaPanel.tsx new file mode 100644 index 0000000..15ee4ff --- /dev/null +++ b/frontend/src/components/media/MediaPanel.tsx @@ -0,0 +1,34 @@ +import type { MediaState } from "../../types/media"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs"; +import { MediaQueue } from "./MediaQueue"; +import { MusicPlayer } from "./MusicPlayer"; +import { ScreenShare } from "./ScreenShare"; + +interface MediaPanelProps { + state: MediaState; + loading: boolean; + onQueueMusic: (source: string) => void; + onStartScreen: (source: string) => void; + onSkip: () => void; + onStop: () => void; +} + +export function MediaPanel({ state, loading, onQueueMusic, onStartScreen, onSkip, onStop }: MediaPanelProps) { + return ( +
+ + + Music + Screen Share + + + + + + + + + +
+ ); +} diff --git a/frontend/src/components/media/MediaQueue.tsx b/frontend/src/components/media/MediaQueue.tsx new file mode 100644 index 0000000..2f68a0f --- /dev/null +++ b/frontend/src/components/media/MediaQueue.tsx @@ -0,0 +1,46 @@ +import type { MediaState } from "../../types/media"; +import { Badge } from "../ui/badge"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"; + +interface MediaQueueProps { + state: MediaState; +} + +export function MediaQueue({ state }: MediaQueueProps) { + return ( + + + Now Playing + Current item and queue state. + + + {state.current ? ( +
+
+
+
{state.current.title}
+
{state.current.source}
+
+ {state.current.mode || "music"} +
+
+ ) : ( +
No media playing.
+ )} +
+
Queue
+ {state.queue.length === 0 ? ( +
Queue is empty.
+ ) : ( + state.queue.map((item, index) => ( +
+
{item.title}
+
{item.source}
+
+ )) + )} +
+
+
+ ); +} diff --git a/frontend/src/components/media/MusicPlayer.tsx b/frontend/src/components/media/MusicPlayer.tsx new file mode 100644 index 0000000..403263b --- /dev/null +++ b/frontend/src/components/media/MusicPlayer.tsx @@ -0,0 +1,45 @@ +import { Music2 } from "lucide-react"; +import { useState } from "react"; +import { Button } from "../ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"; +import { Input } from "../ui/input"; + +interface MusicPlayerProps { + loading: boolean; + onQueue: (source: string) => void; + onSkip: () => void; + onStop: () => void; +} + +export function MusicPlayer({ loading, onQueue, onSkip, onStop }: MusicPlayerProps) { + const [source, setSource] = useState(""); + + const submit = () => { + const trimmed = source.trim(); + if (!trimmed) return; + onQueue(trimmed); + setSource(""); + }; + + return ( + + + Music Player + Play YouTube, Spotify tracks, search terms, or local files as audio. + + + setSource(event.target.value)} + onKeyDown={(event) => event.key === "Enter" && submit()} + placeholder="YouTube URL, Spotify track, or search terms" + /> +
+ + + +
+
+
+ ); +} diff --git a/frontend/src/components/media/ScreenShare.tsx b/frontend/src/components/media/ScreenShare.tsx new file mode 100644 index 0000000..2849d26 --- /dev/null +++ b/frontend/src/components/media/ScreenShare.tsx @@ -0,0 +1,45 @@ +import { MonitorUp } from "lucide-react"; +import { useState } from "react"; +import { Button } from "../ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"; +import { Input } from "../ui/input"; + +interface ScreenShareProps { + loading: boolean; + onStart: (source: string) => void; + onSkip: () => void; + onStop: () => void; +} + +export function ScreenShare({ loading, onStart, onSkip, onStop }: ScreenShareProps) { + const [source, setSource] = useState(""); + + const submit = () => { + const trimmed = source.trim(); + if (!trimmed) return; + onStart(trimmed); + setSource(""); + }; + + return ( + + + Screen Share + Start screen-share playback from a URL or local file path. + + + setSource(event.target.value)} + onKeyDown={(event) => event.key === "Enter" && submit()} + placeholder="Screen share URL or local file path" + /> +
+ + + +
+
+
+ ); +} diff --git a/frontend/src/components/messages/ImageGrid.tsx b/frontend/src/components/messages/ImageGrid.tsx new file mode 100644 index 0000000..edceb86 --- /dev/null +++ b/frontend/src/components/messages/ImageGrid.tsx @@ -0,0 +1,43 @@ +import type { MessageMetadata, MessageRecord } from "../../types/messages"; + +function parseMetadata(value: string | null): MessageMetadata { + if (!value) return {}; + try { + return JSON.parse(value) as MessageMetadata; + } catch { + return {}; + } +} + +export function ImageGrid({ messages }: { messages: MessageRecord[] }) { + const images = messages.flatMap((message) => { + const metadata = parseMetadata(message.metadata); + const attachments = metadata.attachments ?? []; + const embeds = metadata.embeds ?? []; + return [ + ...attachments + .filter((attachment) => attachment.url && (attachment.contentType?.startsWith("image/") || /\.(png|jpe?g|gif|webp)$/i.test(attachment.name))) + .map((attachment) => ({ url: attachment.url, title: attachment.name, message })), + ...embeds + .flatMap((embed) => [embed.image, embed.thumbnail].filter(Boolean).map((url) => ({ url: url as string, title: embed.title || "embed image", message }))), + ]; + }); + + if (images.length === 0) { + return
No images found.
; + } + + return ( +
+ {images.map((image, index) => ( + + {image.title} +
+
{image.title}
+
{image.message.username}
+
+
+ ))} +
+ ); +} diff --git a/frontend/src/components/messages/MessageCard.tsx b/frontend/src/components/messages/MessageCard.tsx index 6aa94e9..0ecf916 100644 --- a/frontend/src/components/messages/MessageCard.tsx +++ b/frontend/src/components/messages/MessageCard.tsx @@ -1,74 +1,51 @@ -import type { MessageRecord } from "../../api/client"; +import { RotateCw } from "lucide-react"; +import type { MessageRecord } from "../../types/messages"; +import { Badge } from "../ui/badge"; +import { Button } from "../ui/button"; export interface MessageCardProps { message: MessageRecord; onReanalyze: (id: string) => void; } -const STATUS_COLORS: Record = { - pending: "#f9e2af", - clean: "#a6e3a1", - warn: "#fab387", - flagged: "#f38ba8", - error: "#f38ba8", -}; +function aiVariant(status: string) { + if (status === "clean") return "success"; + if (status === "warn") return "warning"; + if (status === "flagged" || status === "error") return "destructive"; + return "secondary"; +} export function MessageCard({ message, onReanalyze }: MessageCardProps) { const displayContent = message.edited_content ?? message.content; const aiStatus = message.ai_status ?? "pending"; - const statusColor = STATUS_COLORS[aiStatus] ?? "#6c7086"; return ( -
- {message.username} -
-
- {message.username} - - {new Date(message.created_at).toLocaleString()} - - {message.type === "edited" && ( - edited - )} - {message.type === "deleted" && ( - deleted - )} - - {aiStatus} - -
- -

{displayContent}

- - {message.ai_analysis && ( -
{message.ai_analysis}
- )} - - {message.ai_error && ( -
{message.ai_error}
- )} - -
- +
+
+ +
+
+ {message.username || message.user_id} + {new Date(message.created_at).toLocaleString()} + {message.edited_at ? edited : null} + {message.deleted_at ? deleted : null} + {aiStatus} +
+

+ {displayContent || "(empty message)"} +

+ {message.ai_analysis ?
{message.ai_analysis}
: null} + {message.ai_error ?
AI error: {message.ai_error}
: null} +
-
+ ); } diff --git a/frontend/src/components/messages/MessageFeed.tsx b/frontend/src/components/messages/MessageFeed.tsx index 99df5e1..41fdb38 100644 --- a/frontend/src/components/messages/MessageFeed.tsx +++ b/frontend/src/components/messages/MessageFeed.tsx @@ -1,25 +1,25 @@ -import type { MessageRecord } from "../../api/client"; +import type { MessageRecord } from "../../types/messages"; +import { ScrollArea } from "../ui/scroll-area"; import { MessageCard } from "./MessageCard"; export interface MessageFeedProps { messages: MessageRecord[]; onReanalyze: (id: string) => void; + emptyText?: string; } -export function MessageFeed({ messages, onReanalyze }: MessageFeedProps) { +export function MessageFeed({ messages, onReanalyze, emptyText = "No messages found." }: MessageFeedProps) { if (messages.length === 0) { - return ( -
-

No messages yet

-
- ); + return
{emptyText}
; } return ( -
- {messages.map((msg) => ( - - ))} -
+ +
+ {messages.map((message) => ( + + ))} +
+
); } diff --git a/frontend/src/components/messages/MessagesPanel.tsx b/frontend/src/components/messages/MessagesPanel.tsx new file mode 100644 index 0000000..1d76e07 --- /dev/null +++ b/frontend/src/components/messages/MessagesPanel.tsx @@ -0,0 +1,66 @@ +import type { Channel, Guild } from "../../types/voice"; +import type { MessageRecord } from "../../types/messages"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"; +import { Select } from "../ui/select"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs"; +import { ImageGrid } from "./ImageGrid"; +import { MessageFeed } from "./MessageFeed"; + +interface MessagesPanelProps { + guilds: Guild[]; + channels: Channel[]; + selectedGuild: string; + selectedChannel: string; + messages: MessageRecord[]; + onGuildChange: (guildId: string) => void; + onChannelChange: (channelId: string) => void; + onReanalyze: (id: string) => void; +} + +export function MessagesPanel({ + guilds, + channels, + selectedGuild, + selectedChannel, + messages, + onGuildChange, + onChannelChange, + onReanalyze, +}: MessagesPanelProps) { + return ( +
+ + + Message Source + Pick a guild and channel/thread to inspect captures. + + + onChannelChange(event.target.value)} + placeholder="Select channel or thread" + options={channels.map((channel) => ({ value: channel.id, label: channel.name }))} + /> + + + + + All Messages + Images + + + + + + + + +
+ ); +} diff --git a/frontend/src/components/review/ReviewPanel.tsx b/frontend/src/components/review/ReviewPanel.tsx index e69348a..6ac0682 100644 --- a/frontend/src/components/review/ReviewPanel.tsx +++ b/frontend/src/components/review/ReviewPanel.tsx @@ -1,5 +1,6 @@ -import type { MessageRecord } from "../../api/client"; -import { MessageCard } from "../messages/MessageCard"; +import type { MessageRecord } from "../../types/messages"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"; +import { MessageFeed } from "../messages/MessageFeed"; export interface ReviewPanelProps { messages: MessageRecord[]; @@ -8,30 +9,21 @@ export interface ReviewPanelProps { export function ReviewPanel({ messages, onReanalyze }: ReviewPanelProps) { const reviewItems = messages.filter( - (m) => - m.ai_status === "warn" || - m.ai_status === "flagged" || - m.ai_status === "error", + (message) => + message.ai_status === "warn" || + message.ai_status === "flagged" || + message.ai_status === "error", ); return ( -
-
-

Needs Review

- {reviewItems.length} -
- - {reviewItems.length === 0 ? ( -
-

No items to review

-
- ) : ( -
- {reviewItems.map((msg) => ( - - ))} -
- )} -
+ + + Needs Review + {reviewItems.length} captured messages require attention. + + + + + ); -} \ No newline at end of file +} diff --git a/frontend/src/components/ui/badge.tsx b/frontend/src/components/ui/badge.tsx new file mode 100644 index 0000000..c756927 --- /dev/null +++ b/frontend/src/components/ui/badge.tsx @@ -0,0 +1,30 @@ +import type * as React from "react"; +import { cn } from "../../lib/utils"; + +type BadgeVariant = "default" | "secondary" | "destructive" | "outline" | "success" | "warning"; + +const variants: Record = { + default: "border-transparent bg-primary text-primary-foreground", + secondary: "border-transparent bg-secondary text-secondary-foreground", + destructive: "border-transparent bg-destructive text-destructive-foreground", + outline: "text-foreground", + success: "border-transparent bg-emerald-500/15 text-emerald-300", + warning: "border-transparent bg-amber-500/15 text-amber-300", +}; + +export interface BadgeProps extends React.HTMLAttributes { + variant?: BadgeVariant; +} + +export function Badge({ className, variant = "default", ...props }: BadgeProps) { + return ( +
+ ); +} diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx new file mode 100644 index 0000000..64bdf85 --- /dev/null +++ b/frontend/src/components/ui/button.tsx @@ -0,0 +1,48 @@ +import { Slot } from "@radix-ui/react-slot"; +import type * as React from "react"; +import { cn } from "../../lib/utils"; + +type ButtonVariant = "default" | "secondary" | "destructive" | "outline" | "ghost"; +type ButtonSize = "default" | "sm" | "lg" | "icon"; + +const variants: Record = { + default: "bg-primary text-primary-foreground shadow hover:bg-primary/90", + secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: "border border-border bg-background hover:bg-accent hover:text-accent-foreground", + ghost: "hover:bg-accent hover:text-accent-foreground", +}; + +const sizes: Record = { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", +}; + +export interface ButtonProps extends React.ButtonHTMLAttributes { + asChild?: boolean; + variant?: ButtonVariant; + size?: ButtonSize; +} + +export function Button({ + className, + variant = "default", + size = "default", + asChild = false, + ...props +}: ButtonProps) { + const Comp = asChild ? Slot : "button"; + return ( + + ); +} diff --git a/frontend/src/components/ui/card.tsx b/frontend/src/components/ui/card.tsx new file mode 100644 index 0000000..cda932d --- /dev/null +++ b/frontend/src/components/ui/card.tsx @@ -0,0 +1,26 @@ +import type * as React from "react"; +import { cn } from "../../lib/utils"; + +export function Card({ className, ...props }: React.HTMLAttributes) { + return
; +} + +export function CardHeader({ className, ...props }: React.HTMLAttributes) { + return
; +} + +export function CardTitle({ className, ...props }: React.HTMLAttributes) { + return

; +} + +export function CardDescription({ className, ...props }: React.HTMLAttributes) { + return

; +} + +export function CardContent({ className, ...props }: React.HTMLAttributes) { + return

; +} + +export function CardFooter({ className, ...props }: React.HTMLAttributes) { + return
; +} diff --git a/frontend/src/components/ui/input.tsx b/frontend/src/components/ui/input.tsx new file mode 100644 index 0000000..bbca089 --- /dev/null +++ b/frontend/src/components/ui/input.tsx @@ -0,0 +1,15 @@ +import type * as React from "react"; +import { cn } from "../../lib/utils"; + +export function Input({ className, type, ...props }: React.InputHTMLAttributes) { + return ( + + ); +} diff --git a/frontend/src/components/ui/scroll-area.tsx b/frontend/src/components/ui/scroll-area.tsx new file mode 100644 index 0000000..27af390 --- /dev/null +++ b/frontend/src/components/ui/scroll-area.tsx @@ -0,0 +1,30 @@ +import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"; +import type * as React from "react"; +import { cn } from "../../lib/utils"; + +export function ScrollArea({ className, children, ...props }: React.ComponentPropsWithoutRef) { + return ( + + {children} + + + + ); +} + +function ScrollBar({ className, orientation = "vertical", ...props }: React.ComponentPropsWithoutRef) { + return ( + + + + ); +} diff --git a/frontend/src/components/ui/select.tsx b/frontend/src/components/ui/select.tsx new file mode 100644 index 0000000..82fc816 --- /dev/null +++ b/frontend/src/components/ui/select.tsx @@ -0,0 +1,31 @@ +import type * as React from "react"; +import { cn } from "../../lib/utils"; + +export interface SelectOption { + value: string; + label: string; +} + +interface SelectProps extends React.SelectHTMLAttributes { + options: SelectOption[]; + placeholder?: string; +} + +export function Select({ className, options, placeholder, ...props }: SelectProps) { + return ( + + ); +} diff --git a/frontend/src/components/ui/tabs.tsx b/frontend/src/components/ui/tabs.tsx new file mode 100644 index 0000000..5681d5c --- /dev/null +++ b/frontend/src/components/ui/tabs.tsx @@ -0,0 +1,35 @@ +import * as TabsPrimitive from "@radix-ui/react-tabs"; +import type * as React from "react"; +import { cn } from "../../lib/utils"; + +export const Tabs = TabsPrimitive.Root; + +export function TabsList({ className, ...props }: React.ComponentPropsWithoutRef) { + return ( + + ); +} + +export function TabsTrigger({ className, ...props }: React.ComponentPropsWithoutRef) { + return ( + + ); +} + +export function TabsContent({ className, ...props }: React.ComponentPropsWithoutRef) { + return ( + + ); +} diff --git a/frontend/src/components/voice/ActiveSpeakers.tsx b/frontend/src/components/voice/ActiveSpeakers.tsx new file mode 100644 index 0000000..264b047 --- /dev/null +++ b/frontend/src/components/voice/ActiveSpeakers.tsx @@ -0,0 +1,35 @@ +import type { ActiveSpeaker } from "../../types/voice"; +import { Card, CardContent, CardHeader, CardTitle } from "../ui/card"; + +interface ActiveSpeakersProps { + speakers: ActiveSpeaker[]; +} + +export function ActiveSpeakers({ speakers }: ActiveSpeakersProps) { + return ( + + + Active Speakers + + + {speakers.length === 0 ? ( +
+ No active speakers. +
+ ) : ( +
+ {speakers.map((speaker, index) => ( +
+ +
+
{speaker.username}
+
Speaking
+
+
+ ))} +
+ )} +
+
+ ); +} diff --git a/frontend/src/components/voice/AudioVisualizer.tsx b/frontend/src/components/voice/AudioVisualizer.tsx new file mode 100644 index 0000000..4ef1b1d --- /dev/null +++ b/frontend/src/components/voice/AudioVisualizer.tsx @@ -0,0 +1,18 @@ +interface AudioVisualizerProps { + levels: number[]; +} + +export function AudioVisualizer({ levels }: AudioVisualizerProps) { + const bars = levels.length ? levels : Array.from({ length: 32 }, () => 0.04); + return ( +
+ {bars.map((level, index) => ( +
+ ))} +
+ ); +} diff --git a/frontend/src/components/voice/VoiceControl.tsx b/frontend/src/components/voice/VoiceControl.tsx new file mode 100644 index 0000000..7555230 --- /dev/null +++ b/frontend/src/components/voice/VoiceControl.tsx @@ -0,0 +1,76 @@ +import type { Channel, Guild, VoiceStatus } from "../../types/voice"; +import { Button } from "../ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"; +import { Select } from "../ui/select"; + +interface VoiceControlProps { + guilds: Guild[]; + channels: Channel[]; + selectedGuild: string; + selectedChannel: string; + status: VoiceStatus; + loading: boolean; + onGuildChange: (guildId: string) => void; + onChannelChange: (channelId: string) => void; + onJoin: () => void; + onDisconnect: () => void; + onListenToggle: () => void; + isListening: boolean; +} + +export function VoiceControl({ + guilds, + channels, + selectedGuild, + selectedChannel, + status, + loading, + onGuildChange, + onChannelChange, + onJoin, + onDisconnect, + onListenToggle, + isListening, +}: VoiceControlProps) { + return ( + + + Voice Bridge + Join a Discord voice channel and monitor audio in real time. + + +
+
+ + onChannelChange(event.target.value)} + placeholder="Select voice channel" + options={channels.map((channel) => ({ value: channel.id, label: channel.name }))} + /> +
+
+
+ + + +
+
+
+ ); +} diff --git a/frontend/src/components/voice/VoicePanel.tsx b/frontend/src/components/voice/VoicePanel.tsx new file mode 100644 index 0000000..7188345 --- /dev/null +++ b/frontend/src/components/voice/VoicePanel.tsx @@ -0,0 +1,41 @@ +import type { ActiveSpeaker, Channel, Guild, VoiceStatus } from "../../types/voice"; +import { AudioVisualizer } from "./AudioVisualizer"; +import { ActiveSpeakers } from "./ActiveSpeakers"; +import { VoiceControl } from "./VoiceControl"; +import { Card, CardContent, CardHeader, CardTitle } from "../ui/card"; + +interface VoicePanelProps { + guilds: Guild[]; + channels: Channel[]; + selectedGuild: string; + selectedChannel: string; + status: VoiceStatus; + loading: boolean; + activeSpeakers: ActiveSpeaker[]; + levels: number[]; + isListening: boolean; + onGuildChange: (guildId: string) => void; + onChannelChange: (channelId: string) => void; + onJoin: () => void; + onDisconnect: () => void; + onListenToggle: () => void; +} + +export function VoicePanel(props: VoicePanelProps) { + return ( +
+ +
+ + + Live Audio Visualizer + + + + + + +
+
+ ); +} diff --git a/frontend/src/hooks/useDashboardSocket.ts b/frontend/src/hooks/useDashboardSocket.ts new file mode 100644 index 0000000..52f3b4a --- /dev/null +++ b/frontend/src/hooks/useDashboardSocket.ts @@ -0,0 +1,96 @@ +import { useEffect, useRef, useState } from "react"; +import type { MessageRecord } from "../types/messages"; +import type { MediaState } from "../types/media"; +import type { UIState } from "../types/ui"; +import type { ActiveSpeaker } from "../types/voice"; + +export type WebSocketStatus = "connecting" | "connected" | "disconnected" | "error"; + +export interface DashboardSocketHandlers { + onUIState?: (state: UIState) => void; + onUserState?: (users: ActiveSpeaker[]) => void; + onMessageCreated?: (message: MessageRecord) => void; + onMessageUpdated?: (message: Partial & { id: string }) => void; + onMessageDeleted?: (message: { id: string }) => void; + onMessageAnalyzed?: (message: MessageRecord) => void; + onAttachmentUploaded?: () => void; + onMediaState?: (state: MediaState) => void; + onPcm?: (data: ArrayBuffer) => void; +} + +export function useDashboardSocket(handlers: DashboardSocketHandlers) { + const [status, setStatus] = useState("connecting"); + const handlersRef = useRef(handlers); + const socketRef = useRef(null); + + handlersRef.current = handlers; + + useEffect(() => { + let closed = false; + let reconnectTimer: number | null = null; + + const connect = () => { + const protocol = location.protocol === "https:" ? "wss:" : "ws:"; + const socket = new WebSocket(`${protocol}//${location.host}/ws`); + socket.binaryType = "arraybuffer"; + socketRef.current = socket; + setStatus("connecting"); + + socket.addEventListener("open", () => setStatus("connected")); + socket.addEventListener("error", () => setStatus("error")); + socket.addEventListener("close", () => { + setStatus("disconnected"); + if (!closed) reconnectTimer = window.setTimeout(connect, 2500); + }); + socket.addEventListener("message", (event) => { + if (event.data instanceof ArrayBuffer) { + handlersRef.current.onPcm?.(event.data); + return; + } + if (typeof event.data !== "string") return; + try { + const message = JSON.parse(event.data); + switch (message.type) { + case "ui_state": + handlersRef.current.onUIState?.(message.state); + break; + case "user_state": + handlersRef.current.onUserState?.(message.users || []); + break; + case "message_created": + handlersRef.current.onMessageCreated?.(message.data); + break; + case "message_updated": + handlersRef.current.onMessageUpdated?.(message.data); + break; + case "message_deleted": + handlersRef.current.onMessageDeleted?.(message.data); + break; + case "message_analyzed": + handlersRef.current.onMessageAnalyzed?.(message.data); + break; + case "attachment_uploaded": + handlersRef.current.onAttachmentUploaded?.(); + break; + case "media_state": + handlersRef.current.onMediaState?.(message.state); + break; + } + } catch { + // ignore malformed socket messages + } + }); + }; + + connect(); + + return () => { + closed = true; + if (reconnectTimer) window.clearTimeout(reconnectTimer); + socketRef.current?.close(); + socketRef.current = null; + }; + }, []); + + return { status, socketRef }; +} diff --git a/frontend/src/hooks/useMediaControl.ts b/frontend/src/hooks/useMediaControl.ts new file mode 100644 index 0000000..7719bb1 --- /dev/null +++ b/frontend/src/hooks/useMediaControl.ts @@ -0,0 +1,63 @@ +import { useCallback, useEffect, useState } from "react"; +import { getMediaStatus, queueMedia, skipMedia, stopMedia } from "../api/media"; +import type { MediaMode, MediaState } from "../types/media"; + +const emptyMediaState: MediaState = { playing: false, current: null, queue: [] }; + +export function useMediaControl() { + const [mediaState, setMediaState] = useState(emptyMediaState); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const refreshMedia = useCallback(async () => { + const state = await getMediaStatus(); + setMediaState(state); + return state; + }, []); + + const enqueue = useCallback(async (source: string, mode: MediaMode) => { + setLoading(true); + setError(null); + try { + const state = await queueMedia(source, mode); + setMediaState(state); + return state; + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + setError(message); + throw err; + } finally { + setLoading(false); + } + }, []); + + const skip = useCallback(async () => { + setLoading(true); + setError(null); + try { + const state = await skipMedia(); + setMediaState(state); + return state; + } finally { + setLoading(false); + } + }, []); + + const stop = useCallback(async () => { + setLoading(true); + setError(null); + try { + const state = await stopMedia(); + setMediaState(state); + return state; + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + refreshMedia().catch((err) => setError(err instanceof Error ? err.message : String(err))); + }, [refreshMedia]); + + return { mediaState, setMediaState, loading, error, refreshMedia, enqueue, skip, stop }; +} diff --git a/frontend/src/hooks/useMessages.ts b/frontend/src/hooks/useMessages.ts new file mode 100644 index 0000000..c56cf1a --- /dev/null +++ b/frontend/src/hooks/useMessages.ts @@ -0,0 +1,54 @@ +import { useCallback, useEffect, useState } from "react"; +import { listMessages, reanalyzeMessage } from "../api/messages"; +import type { MessageRecord } from "../types/messages"; + +export function mergeMessages(current: MessageRecord[], incoming: MessageRecord[]): MessageRecord[] { + const byId = new Map(current.map((message) => [message.id, message])); + for (const message of incoming) { + byId.set(message.id, { ...byId.get(message.id), ...message }); + } + return Array.from(byId.values()) + .sort((a, b) => b.created_at - a.created_at || b.id.localeCompare(a.id)) + .slice(0, 200); +} + +export function useMessages() { + const [messages, setMessages] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const fetchMessages = useCallback(async (channelId?: string) => { + setLoading(true); + setError(null); + try { + const params = new URLSearchParams({ limit: "80" }); + if (channelId) params.set("channel", channelId); + const result = await listMessages(params); + setMessages(result.data); + return result.data; + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + setError(message); + throw err; + } finally { + setLoading(false); + } + }, []); + + const reanalyze = useCallback(async (id: string) => { + setMessages((prev) => + prev.map((message) => + message.id === id + ? { ...message, ai_status: "pending", ai_error: null, ai_analysis: null } + : message, + ), + ); + await reanalyzeMessage(id); + }, []); + + useEffect(() => { + fetchMessages().catch(() => undefined); + }, [fetchMessages]); + + return { messages, setMessages, loading, error, fetchMessages, reanalyze }; +} diff --git a/frontend/src/hooks/useUIState.ts b/frontend/src/hooks/useUIState.ts new file mode 100644 index 0000000..5c0081f --- /dev/null +++ b/frontend/src/hooks/useUIState.ts @@ -0,0 +1,35 @@ +import { useCallback, useEffect, useState } from "react"; +import { getUIState, updateUIState } from "../api/uiState"; +import type { UIState } from "../types/ui"; + +export function useUIState() { + const [uiState, setUIState] = useState({ activeTab: "voice" }); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + let cancelled = false; + getUIState() + .then((state) => { + if (!cancelled) setUIState({ activeTab: "voice", ...state }); + }) + .catch((err) => { + if (!cancelled) setError(err instanceof Error ? err.message : String(err)); + }) + .finally(() => { + if (!cancelled) setLoading(false); + }); + return () => { + cancelled = true; + }; + }, []); + + const patchUIState = useCallback(async (patch: Partial) => { + setUIState((prev) => ({ ...prev, ...patch })); + const next = await updateUIState(patch); + setUIState((prev) => ({ ...prev, ...next })); + return next; + }, []); + + return { uiState, setUIState, patchUIState, loading, error }; +} diff --git a/frontend/src/hooks/useVoiceControl.ts b/frontend/src/hooks/useVoiceControl.ts new file mode 100644 index 0000000..556acc2 --- /dev/null +++ b/frontend/src/hooks/useVoiceControl.ts @@ -0,0 +1,109 @@ +import { useCallback, useEffect, useState } from "react"; +import { + connectVoice, + disconnectVoice, + getGuilds, + getTextChannels, + getThreads, + getVoiceChannels, + getVoiceStatus, +} from "../api/voice"; +import type { Channel, Guild, VoiceStatus } from "../types/voice"; + +export function useVoiceControl() { + const [guilds, setGuilds] = useState([]); + const [voiceChannels, setVoiceChannels] = useState([]); + const [textChannels, setTextChannels] = useState([]); + const [voiceStatus, setVoiceStatus] = useState({ connected: false }); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const refreshGuilds = useCallback(async () => { + setError(null); + const nextGuilds = await getGuilds(); + setGuilds(nextGuilds); + return nextGuilds; + }, []); + + const refreshVoiceStatus = useCallback(async () => { + const status = await getVoiceStatus(); + setVoiceStatus(status); + return status; + }, []); + + const loadVoiceChannels = useCallback(async (guildId: string) => { + if (!guildId) { + setVoiceChannels([]); + return []; + } + const channels = await getVoiceChannels(guildId); + setVoiceChannels(channels); + return channels; + }, []); + + const loadTextTargets = useCallback(async (guildId: string) => { + if (!guildId) { + setTextChannels([]); + return []; + } + const [channels, threads] = await Promise.all([ + getTextChannels(guildId), + getThreads(guildId).catch(() => []), + ]); + const combined = [...channels, ...threads]; + setTextChannels(combined); + return combined; + }, []); + + const joinVoice = useCallback(async (guildId: string, channelId: string) => { + setLoading(true); + setError(null); + try { + const status = await connectVoice(guildId, channelId); + setVoiceStatus(status); + return status; + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + setError(message); + throw err; + } finally { + setLoading(false); + } + }, []); + + const leaveVoice = useCallback(async () => { + setLoading(true); + setError(null); + try { + const status = await disconnectVoice(); + setVoiceStatus(status); + return status; + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + setError(message); + throw err; + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + refreshGuilds().catch((err) => setError(err instanceof Error ? err.message : String(err))); + refreshVoiceStatus().catch((err) => setError(err instanceof Error ? err.message : String(err))); + }, [refreshGuilds, refreshVoiceStatus]); + + return { + guilds, + voiceChannels, + textChannels, + voiceStatus, + loading, + error, + refreshGuilds, + refreshVoiceStatus, + loadVoiceChannels, + loadTextTargets, + joinVoice, + leaveVoice, + }; +} diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts new file mode 100644 index 0000000..a5ef193 --- /dev/null +++ b/frontend/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/frontend/src/styles.css b/frontend/src/styles.css index a1234dc..190f524 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -1,341 +1,41 @@ -* { - box-sizing: border-box; - margin: 0; - padding: 0; -} +@tailwind base; +@tailwind components; +@tailwind utilities; -body { - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; - background: #1e1e2e; - color: #cdd6f4; - height: 100vh; - overflow: hidden; -} +@layer base { + :root { + --background: 222 47% 7%; + --foreground: 210 40% 98%; + --card: 222 47% 10%; + --card-foreground: 210 40% 98%; + --primary: 199 89% 48%; + --primary-foreground: 210 40% 98%; + --secondary: 217 33% 17%; + --secondary-foreground: 210 40% 98%; + --muted: 217 33% 17%; + --muted-foreground: 215 20% 65%; + --accent: 217 33% 17%; + --accent-foreground: 210 40% 98%; + --destructive: 0 72% 51%; + --destructive-foreground: 210 40% 98%; + --border: 217 33% 20%; + --input: 217 33% 20%; + --ring: 199 89% 48%; + --radius: 0.85rem; + } -#root { - height: 100%; -} + * { + @apply border-border; + } -.app { - display: flex; - height: 100vh; -} + body { + @apply bg-background text-foreground antialiased; + font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + } -.sidebar { - width: 220px; - background: #181825; - border-right: 1px solid #313244; - display: flex; - flex-direction: column; - flex-shrink: 0; + html, + body, + #root { + min-height: 100%; + } } - -.sidebar-header { - padding: 16px; - font-size: 14px; - font-weight: 700; - color: #cdd6f4; - border-bottom: 1px solid #313244; -} - -.sidebar-placeholder { - padding: 16px; - font-size: 12px; - color: #6c7086; - flex: 1; -} - -.main { - flex: 1; - display: flex; - flex-direction: column; - overflow: hidden; -} - -.header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 12px 20px; - border-bottom: 1px solid #313244; - background: #181825; - flex-shrink: 0; -} - -.header h1 { - font-size: 16px; - font-weight: 600; - color: #cdd6f4; -} - -.ws-status { - font-size: 11px; - padding: 3px 8px; - border-radius: 4px; - font-weight: 500; -} - -.ws-status[data-status="connected"] { - background: #a6e3a1; - color: #1e1e2e; -} - -.ws-status[data-status="disconnected"], -.ws-status[data-status="error"] { - background: #f38ba8; - color: #1e1e2e; -} - -.ws-status[data-status="connecting"] { - background: #f9e2af; - color: #1e1e2e; -} - -.content { - flex: 1; - overflow-y: auto; - padding: 16px; -} - -.message-list { - display: flex; - flex-direction: column; - gap: 8px; -} - -.empty-state { - text-align: center; - color: #6c7086; - padding: 40px; -} - -.message-item { - display: flex; - gap: 12px; - padding: 10px 12px; - background: #181825; - border-radius: 6px; - border: 1px solid #313244; -} - -.message-item.type-deleted { - opacity: 0.6; -} - -.message-avatar { - width: 32px; - height: 32px; - border-radius: 50%; - flex-shrink: 0; -} - -.message-body { - flex: 1; - min-width: 0; -} - -.message-username { - font-weight: 600; - color: #89b4fa; - margin-right: 8px; -} - -.message-time { - font-size: 11px; - color: #6c7086; -} - -.message-deleted { - font-size: 11px; - color: #f38ba8; - margin-left: 8px; - font-style: italic; -} - -.message-content { - margin-top: 4px; - font-size: 13px; - color: #bac2de; - white-space: pre-wrap; - word-break: break-word; -} - -.review-panel { - border-top: 1px solid #313244; - background: #181825; - flex-shrink: 0; -} - -/* Message Card */ -.message-card { - display: flex; - gap: 12px; - padding: 12px; - background: #181825; - border-radius: 6px; - border: 1px solid #313244; -} - -.message-card.type-deleted { - opacity: 0.6; -} - -.message-card-avatar { - width: 32px; - height: 32px; - border-radius: 50%; - flex-shrink: 0; -} - -.message-card-body { - flex: 1; - min-width: 0; -} - -.message-card-meta { - display: flex; - align-items: center; - flex-wrap: wrap; - gap: 8px; -} - -.message-card-username { - font-weight: 600; - color: #89b4fa; -} - -.message-card-time { - font-size: 11px; - color: #6c7086; -} - -.badge { - font-size: 10px; - padding: 2px 6px; - border-radius: 4px; - font-weight: 500; - color: #1e1e2e; -} - -.badge-edited { - background: #fab387; - color: #1e1e2e; -} - -.badge-deleted { - background: #f38ba8; - color: #1e1e2e; -} - -.message-card-content { - margin-top: 6px; - font-size: 13px; - color: #bac2de; - white-space: pre-wrap; - word-break: break-word; -} - -.message-card-analysis { - margin-top: 8px; - padding: 8px; - background: #1e1e2e; - border-radius: 4px; - font-size: 12px; - color: #a6e3a1; - white-space: pre-wrap; -} - -.message-card-error { - margin-top: 8px; - padding: 8px; - background: #1e1e2e; - border-radius: 4px; - font-size: 12px; - color: #f38ba8; -} - -.message-card-actions { - margin-top: 8px; -} - -.btn-reanalyze { - padding: 4px 10px; - font-size: 11px; - border: none; - border-radius: 4px; - background: #313244; - color: #cdd6f4; - cursor: pointer; -} - -.btn-reanalyze:hover:not(:disabled) { - background: #45475a; -} - -.btn-reanalyze:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -/* Message Feed */ -.message-feed { - display: flex; - flex-direction: column; - gap: 8px; -} - -/* Review Panel */ -.review-panel { - border-top: 1px solid #313244; - background: #181825; - flex-shrink: 0; - max-height: 250px; - overflow-y: auto; -} - -.review-header { - display: flex; - align-items: center; - gap: 8px; - padding: 12px 16px; - border-bottom: 1px solid #313244; -} - -.review-header h2 { - font-size: 13px; - font-weight: 600; - color: #cdd6f4; -} - -.review-count { - font-size: 11px; - padding: 2px 6px; - border-radius: 10px; - background: #f38ba8; - color: #1e1e2e; - font-weight: 600; -} - -.review-list { - display: flex; - flex-direction: column; - gap: 8px; - padding: 12px; -} - -.review-placeholder { - padding: 16px; - font-size: 12px; - color: #6c7086; -} - -.review-panel .empty-state { - padding: 16px; -} - -/* Keep existing styles */ -.review-placeholder { - padding: 16px; - font-size: 12px; - color: #6c7086; -} \ No newline at end of file diff --git a/frontend/src/types/browser.ts b/frontend/src/types/browser.ts new file mode 100644 index 0000000..082a282 --- /dev/null +++ b/frontend/src/types/browser.ts @@ -0,0 +1,7 @@ +export {}; + +declare global { + interface Window { + webkitAudioContext?: typeof AudioContext; + } +} diff --git a/frontend/src/types/media.ts b/frontend/src/types/media.ts new file mode 100644 index 0000000..134dc72 --- /dev/null +++ b/frontend/src/types/media.ts @@ -0,0 +1,16 @@ +export type MediaMode = "music" | "screen"; + +export interface MediaItem { + id?: string; + source: string; + title: string; + mode?: MediaMode; + durationMs?: number | null; + thumbnailUrl?: string | null; +} + +export interface MediaState { + playing: boolean; + current: MediaItem | null; + queue: MediaItem[]; +} diff --git a/frontend/src/types/messages.ts b/frontend/src/types/messages.ts new file mode 100644 index 0000000..7ebed63 --- /dev/null +++ b/frontend/src/types/messages.ts @@ -0,0 +1,29 @@ +export type { AIStatus, MessageRecord, PageResult } from "../api/client"; + +export interface MessageMetadataAttachment { + name: string; + url: string; + size: number; + contentType?: string | null; +} + +export interface MessageMetadataEmbed { + title?: string; + description?: string; + url?: string; + image?: string; + thumbnail?: string; +} + +export interface MessageMetadataSticker { + name: string; + url: string; +} + +export interface MessageMetadata { + attachments?: MessageMetadataAttachment[]; + embeds?: MessageMetadataEmbed[]; + stickers?: MessageMetadataSticker[]; + reference?: { messageId?: string }; + channel?: { threadName?: string }; +} diff --git a/frontend/src/types/ui.ts b/frontend/src/types/ui.ts new file mode 100644 index 0000000..3297d57 --- /dev/null +++ b/frontend/src/types/ui.ts @@ -0,0 +1,12 @@ +export type DashboardTab = "voice" | "media" | "messages" | "review"; + +export interface UIState { + selectedGuild?: string; + selectedVoiceGuild?: string; + selectedVoiceChannel?: string; + selectedTextGuild?: string; + selectedTextChannel?: string; + activeTab?: DashboardTab; + isListening?: boolean; + isStreaming?: boolean; +} diff --git a/frontend/src/types/voice.ts b/frontend/src/types/voice.ts new file mode 100644 index 0000000..cd9c9b9 --- /dev/null +++ b/frontend/src/types/voice.ts @@ -0,0 +1,27 @@ +export interface Guild { + id: string; + name: string; + icon?: string | null; +} + +export interface Channel { + id: string; + name: string; + type?: string; + parentId?: string | null; +} + +export interface VoiceStatus { + connected: boolean; + activeGuildId?: string | null; + activeChannelId?: string | null; + activeChannelName?: string | null; +} + +export interface ActiveSpeaker { + id?: string; + userId?: string; + username: string; + avatar: string; + speaking: boolean; +} diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js new file mode 100644 index 0000000..085792f --- /dev/null +++ b/frontend/tailwind.config.js @@ -0,0 +1,27 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ["./index.html", "./src/**/*.{ts,tsx}"], + theme: { + extend: { + colors: { + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + primary: { DEFAULT: "hsl(var(--primary))", foreground: "hsl(var(--primary-foreground))" }, + secondary: { DEFAULT: "hsl(var(--secondary))", foreground: "hsl(var(--secondary-foreground))" }, + muted: { DEFAULT: "hsl(var(--muted))", foreground: "hsl(var(--muted-foreground))" }, + accent: { DEFAULT: "hsl(var(--accent))", foreground: "hsl(var(--accent-foreground))" }, + destructive: { DEFAULT: "hsl(var(--destructive))", foreground: "hsl(var(--destructive-foreground))" }, + card: { DEFAULT: "hsl(var(--card))", foreground: "hsl(var(--card-foreground))" }, + }, + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + }, + }, + plugins: [], +}; diff --git a/package.json b/package.json index 4c3a997..948bf2e 100644 --- a/package.json +++ b/package.json @@ -26,16 +26,22 @@ "@dank074/discord-video-stream": "workspace:*", "@discordjs/opus": "^0.10.0", "@discordjs/voice": "^0.19.1", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-tabs": "^1.1.13", "@snazzah/davey": "^0.1.10", "@types/pg": "^8.20.0", "@vitejs/plugin-react": "^6.0.2", "better-sqlite3": "^12.10.0", + "clsx": "^2.1.1", "discord.js-selfbot-v13": "workspace:*", "dotenv": "^17.4.2", "drizzle-orm": "^0.45.2", "express": "^5.2.1", "helmet": "^8.1.0", "libsodium-wrappers": "^0.8.2", + "lucide-react": "^1.16.0", "p-retry": "^8.0.0", "pg": "^8.20.0", "pino": "^10.3.1", @@ -46,6 +52,7 @@ "react": "^19.2.6", "react-dom": "^19.2.6", "sodium-native": "^5.1.0", + "tailwind-merge": "^3.6.0", "vite": "^8.0.13", "ws": "^8.20.1", "zod": "^4.4.3" @@ -58,8 +65,11 @@ "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@types/ws": "^8.18.1", + "autoprefixer": "^10.5.0", "drizzle-kit": "^0.31.10", "pino-pretty": "^13.1.3", + "postcss": "^8.5.14", + "tailwindcss": "^3.4.17", "tsx": "^4.22.0", "vitest": "latest" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 02c7ee7..f92bb53 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,18 @@ importers: '@discordjs/voice': specifier: ^0.19.1 version: 0.19.2(@discordjs/opus@0.10.0)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@radix-ui/react-scroll-area': + specifier: ^1.2.10 + version: 1.2.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-select': + specifier: ^2.2.6 + version: 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-slot': + specifier: ^1.2.4 + version: 1.2.4(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-tabs': + specifier: ^1.1.13 + version: 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@snazzah/davey': specifier: ^0.1.10 version: 0.1.11(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) @@ -25,10 +37,13 @@ importers: version: 8.20.0 '@vitejs/plugin-react': specifier: ^6.0.2 - version: 6.0.2(vite@8.0.13(@types/node@25.8.0)(esbuild@0.28.0)(tsx@4.22.0)(yaml@2.9.0)) + version: 6.0.2(vite@8.0.13(@types/node@25.8.0)(esbuild@0.28.0)(jiti@1.21.7)(tsx@4.22.0)(yaml@2.9.0)) better-sqlite3: specifier: ^12.10.0 version: 12.10.0 + clsx: + specifier: ^2.1.1 + version: 2.1.1 discord.js-selfbot-v13: specifier: workspace:* version: link:vendor/discord.js-selfbot-v13 @@ -47,6 +62,9 @@ importers: libsodium-wrappers: specifier: ^0.8.2 version: 0.8.4 + lucide-react: + specifier: ^1.16.0 + version: 1.16.0(react@19.2.6) p-retry: specifier: ^8.0.0 version: 8.0.0 @@ -77,9 +95,12 @@ importers: sodium-native: specifier: ^5.1.0 version: 5.1.0(bare-events@2.8.3) + tailwind-merge: + specifier: ^3.6.0 + version: 3.6.0 vite: specifier: ^8.0.13 - version: 8.0.13(@types/node@25.8.0)(esbuild@0.28.0)(tsx@4.22.0)(yaml@2.9.0) + version: 8.0.13(@types/node@25.8.0)(esbuild@0.28.0)(jiti@1.21.7)(tsx@4.22.0)(yaml@2.9.0) ws: specifier: ^8.20.1 version: 8.20.1 @@ -108,18 +129,27 @@ importers: '@types/ws': specifier: ^8.18.1 version: 8.18.1 + autoprefixer: + specifier: ^10.5.0 + version: 10.5.0(postcss@8.5.14) drizzle-kit: specifier: ^0.31.10 version: 0.31.10 pino-pretty: specifier: ^13.1.3 version: 13.1.3 + postcss: + specifier: ^8.5.14 + version: 8.5.14 + tailwindcss: + specifier: ^3.4.17 + version: 3.4.17 tsx: specifier: ^4.22.0 version: 4.22.0 vitest: specifier: latest - version: 4.1.6(@opentelemetry/api@1.9.1)(@types/node@25.8.0)(vite@8.0.13(@types/node@25.8.0)(esbuild@0.28.0)(tsx@4.22.0)(yaml@2.9.0)) + version: 4.1.6(@opentelemetry/api@1.9.1)(@types/node@25.8.0)(vite@8.0.13(@types/node@25.8.0)(esbuild@0.28.0)(jiti@1.21.7)(tsx@4.22.0)(yaml@2.9.0)) vendor/Discord-video-stream: dependencies: @@ -254,6 +284,10 @@ packages: '@actions/io@1.1.3': resolution: {integrity: sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==} + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + '@babel/code-frame@7.29.0': resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} @@ -894,6 +928,21 @@ packages: resolution: {integrity: sha512-nwc2iesjyc9hkuzcrMCBXQRn653XuAUKorfWM8PZyJawiy1QzLj4vahwzaI25+pfpwOLvMzbJ0uKpWLDNmo16w==} engines: {node: '>= 8'} + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/react-dom@2.1.8': + resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + '@img/colour@1.1.0': resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} @@ -1035,9 +1084,19 @@ packages: resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@jsdevtools/ez-spawn@3.0.4': resolution: {integrity: sha512-f5DRIOZf7wxogefH03RjMPMdBF7ADTWUMoOs9kaJo06EfwF+aFhMZMDZxHg/Xe12hptN9xoZjGso2fdjapBRIA==} engines: {node: '>=10'} @@ -1199,6 +1258,319 @@ packages: '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + '@radix-ui/number@1.1.1': + resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} + + '@radix-ui/primitive@1.1.3': + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + + '@radix-ui/react-arrow@1.1.7': + resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} + 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 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collection@1.1.7': + resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} + 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 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context@1.1.2': + resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-direction@1.1.1': + resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.11': + resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==} + 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 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-guards@1.1.3': + resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.1.7': + resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} + 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 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-id@1.1.1': + resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-popper@1.2.8': + resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==} + 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 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.1.9': + resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} + 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 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.5': + resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} + 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 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + 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 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-roving-focus@1.1.11': + resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} + 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 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-scroll-area@1.2.10': + resolution: {integrity: sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==} + 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 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-select@2.2.6': + resolution: {integrity: sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==} + 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 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-slot@1.2.4': + resolution: {integrity: sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-tabs@1.1.13': + resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==} + 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 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.1': + resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.2': + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.2': + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.1': + resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-previous@1.1.1': + resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.1.1': + resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.1': + resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-visually-hidden@1.2.3': + resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==} + 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 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/rect@1.1.1': + resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + '@rolldown/binding-android-arm64@1.0.1': resolution: {integrity: sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1652,6 +2024,13 @@ packages: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} engines: {node: '>=10'} + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + aproba@2.1.0: resolution: {integrity: sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==} @@ -1660,12 +2039,19 @@ packages: engines: {node: '>=10'} deprecated: This package is no longer supported. + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + array-back@1.0.4: resolution: {integrity: sha512-1WxbZvrmyhkNoeYcizokbmh5oiOCIfyvGtcqbK3Ls1v1fKcquzxnQSceOx6tzq7jmai2kFLWIpGND2cLhH6TPw==} engines: {node: '>=0.12.0'} @@ -1710,6 +2096,13 @@ packages: resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} engines: {node: '>=8.0.0'} + autoprefixer@10.5.0: + resolution: {integrity: sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + b4a@1.8.1: resolution: {integrity: sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==} peerDependencies: @@ -1784,6 +2177,11 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + baseline-browser-mapping@2.10.29: + resolution: {integrity: sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==} + engines: {node: '>=6.0.0'} + hasBin: true + before-after-hook@2.2.3: resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==} @@ -1791,6 +2189,10 @@ packages: resolution: {integrity: sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ==} engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x || 26.x} + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + bindings@1.5.0: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} @@ -1814,6 +2216,11 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + buffer-crc32@1.0.0: resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} engines: {node: '>=8.0.0'} @@ -1854,6 +2261,10 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} + camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + camelcase-keys@6.2.2: resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==} engines: {node: '>=8'} @@ -1862,6 +2273,9 @@ packages: resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} engines: {node: '>=6'} + caniuse-lite@1.0.30001792: + resolution: {integrity: sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==} + catharsis@0.9.0: resolution: {integrity: sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==} engines: {node: '>= 10'} @@ -1881,6 +2295,10 @@ packages: chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} @@ -1906,6 +2324,10 @@ packages: cliui@6.0.0: resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + cmake-ts@1.0.2: resolution: {integrity: sha512-5l++JHE7MxFuyV/OwJf3ek7ZZN1aGPFPM5oUz6AnK5inQAPe4TFXRMz5sA2qg2FRgByPWdqO+gSfIPo8GzoKNQ==} hasBin: true @@ -1950,6 +2372,10 @@ packages: resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} engines: {node: '>=20'} + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + common-sequence@2.0.2: resolution: {integrity: sha512-jAg09gkdkrDO9EWTdXfv80WWH3yeZl5oT69fGfedBNS9pXUKYInVJ1bJ+/ht2+Moeei48TmSbQDYMc8EOx9G0g==} engines: {node: '>=8'} @@ -2000,6 +2426,11 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} @@ -2073,6 +2504,12 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + + didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + diff-sequences@29.6.3: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2087,6 +2524,9 @@ packages: discord-api-types@0.38.47: resolution: {integrity: sha512-XgXQodHQBAE6kfD7kMvVo30863iHX1LHSqNq6MGUTDwIFCCvHva13+rwxyxVXDqudyApMNAd32PGjgVETi5rjA==} + dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + dmd@4.0.6: resolution: {integrity: sha512-7ZYAnFQ6jGm4SICArwqNPylJ83PaOdPTAkds3Z/s1ueFqSc5ilJ2F0b7uP+35W1PUbemH++gn5/VlC3KwEgiHQ==} engines: {node: '>=8'} @@ -2212,6 +2652,9 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + electron-to-chromium@1.5.357: + resolution: {integrity: sha512-NHlTIQDK8fmVwHwuIzmXYEJ1Ewq3D9wDNc0cWXxDGysP6Pb21giwGNkxiTifyKy/4SoPuN5l6GLP1W9Sv7zB2g==} + emoji-regex@7.0.3: resolution: {integrity: sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==} @@ -2261,6 +2704,10 @@ packages: engines: {node: '>=18'} hasBin: true + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} @@ -2463,6 +2910,9 @@ packages: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} + fraction.js@5.3.4: + resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + fresh@2.0.0: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} @@ -2517,6 +2967,10 @@ packages: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + get-proto@1.0.1: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} @@ -2535,6 +2989,10 @@ packages: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me @@ -2673,6 +3131,10 @@ packages: is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + is-core-module@2.16.2: resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} engines: {node: '>= 0.4'} @@ -2765,6 +3227,10 @@ packages: resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jiti@1.21.7: + resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} + hasBin: true + joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} @@ -2906,6 +3372,10 @@ packages: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -2948,6 +3418,11 @@ packages: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} + lucide-react@1.16.0: + resolution: {integrity: sha512-dYwyPzb4MEKpGUmNYk3WKWPnMrHs3FKM+q94kAnJrcDIqqn1hq2xY8scaS2ovsOCM5D51ey2gaRG3PBb1vgoYQ==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -3087,6 +3562,9 @@ packages: mute-stream@0.0.8: resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + nanoid@3.3.12: resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -3131,6 +3609,9 @@ packages: node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + node-releases@2.0.44: + resolution: {integrity: sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==} + nopt@5.0.0: resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} engines: {node: '>=6'} @@ -3143,6 +3624,10 @@ packages: resolution: {integrity: sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==} engines: {node: '>=10'} + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + npm-run-path@6.0.0: resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} engines: {node: '>=18'} @@ -3158,6 +3643,10 @@ packages: object-get@2.1.1: resolution: {integrity: sha512-7n4IpLMzGGcLEMiQKsNR7vCe+N5E9LORFrtNUVy4sO3dj9a3HedZCxEL2T7QuLhcHN1NBuBsMOKaOsAYI9IIvg==} + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} @@ -3333,6 +3822,10 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + pino-abstract-transport@3.0.0: resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} @@ -3350,6 +3843,10 @@ packages: resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} hasBin: true + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + pkg-pr-new@0.0.62: resolution: {integrity: sha512-K2jtf1PLCJJFDimQIpasPXDdnRTZSYPy36Ldz+QhMpLz2YN1Wi0ZQkomVt5Wi3NdBZcYuYGXekyFWfJ6fAHYjg==} hasBin: true @@ -3372,6 +3869,43 @@ packages: resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} engines: {node: '>=10.13.0'} + postcss-import@15.1.0: + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + + postcss-js@4.1.0: + resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + + postcss-load-config@4.0.2: + resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} + engines: {node: '>= 14'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + + postcss-nested@6.2.0: + resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + postcss@8.5.14: resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==} engines: {node: ^10 || ^12 || >=14} @@ -3513,10 +4047,43 @@ packages: react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.7.2: + resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + react@19.2.6: resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==} engines: {node: '>=0.10.0'} + read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + read-pkg-up@7.0.1: resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} engines: {node: '>=8'} @@ -3532,6 +4099,10 @@ packages: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + real-require@0.2.0: resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} engines: {node: '>= 12.13.0'} @@ -3867,6 +4438,11 @@ packages: resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} engines: {node: '>=14.16'} + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} @@ -3894,6 +4470,14 @@ packages: taffydb@2.6.2: resolution: {integrity: sha512-y3JaeRSplks6NYQuCOj3ZFMO3j60rTwbuKCvZxsAraGYH2epusatvZ0baZYA01WsGqJBq/Dl6vOrMUJqyMj8kA==} + tailwind-merge@3.6.0: + resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==} + + tailwindcss@3.4.17: + resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==} + engines: {node: '>=14.0.0'} + hasBin: true + tar-fs@2.1.4: resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} @@ -3936,6 +4520,13 @@ packages: text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + thirty-two@1.0.2: resolution: {integrity: sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA==} engines: {node: '>=0.2.6'} @@ -4014,6 +4605,9 @@ packages: resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==} engines: {node: '>=8'} + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + ts-mixer@6.0.4: resolution: {integrity: sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==} @@ -4138,6 +4732,12 @@ packages: unzipper@0.12.3: resolution: {integrity: sha512-PZ8hTS+AqcGxsaQntl3IRBw65QrBI6lxzqDEL7IAo/XCEqRTKGfOX56Vea5TH9SZczRVxuzk1re04z/YjuYCJA==} + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -4145,6 +4745,26 @@ packages: resolution: {integrity: sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -4420,6 +5040,8 @@ snapshots: '@actions/io@1.1.3': {} + '@alloc/quick-lru@5.2.0': {} + '@babel/code-frame@7.29.0': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -4841,6 +5463,23 @@ snapshots: '@fidm/asn1': 1.0.4 tweetnacl: 1.0.3 + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + + '@floating-ui/react-dom@2.1.8(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@floating-ui/dom': 1.7.6 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + + '@floating-ui/utils@0.2.11': {} + '@img/colour@1.1.0': {} '@img/sharp-darwin-arm64@0.34.5': @@ -4941,8 +5580,20 @@ snapshots: dependencies: '@sinclair/typebox': 0.27.10 + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + '@jridgewell/sourcemap-codec@1.5.5': {} + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + '@jsdevtools/ez-spawn@3.0.4': dependencies: call-me-maybe: 1.0.2 @@ -5178,6 +5829,291 @@ snapshots: '@pinojs/redact@0.4.0': {} + '@radix-ui/number@1.1.1': {} + + '@radix-ui/primitive@1.1.3': {} + + '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.14)(react@19.2.6)': + dependencies: + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-context@1.1.2(@types/react@19.2.14)(react@19.2.6)': + dependencies: + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-direction@1.1.1(@types/react@19.2.14)(react@19.2.6)': + dependencies: + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.14)(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.14)(react@19.2.6)': + dependencies: + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-id@1.1.1(@types/react@19.2.14)(react@19.2.6)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.6) + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@floating-ui/react-dom': 2.1.8(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/rect': 1.1.1 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-scroll-area@1.2.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-select@2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + aria-hidden: 1.2.6 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@19.2.6)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-slot@1.2.4(@types/react@19.2.14)(react@19.2.6)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.14)(react@19.2.6)': + dependencies: + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.14)(react@19.2.6)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.6) + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.14)(react@19.2.6)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.6) + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.14)(react@19.2.6)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.6) + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.14)(react@19.2.6)': + dependencies: + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.14)(react@19.2.6)': + dependencies: + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.14)(react@19.2.6)': + dependencies: + '@radix-ui/rect': 1.1.1 + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-size@1.1.1(@types/react@19.2.14)(react@19.2.6)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.6) + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/rect@1.1.1': {} + '@rolldown/binding-android-arm64@1.0.1': optional: true @@ -5451,10 +6387,10 @@ snapshots: dependencies: '@types/node': 25.8.0 - '@vitejs/plugin-react@6.0.2(vite@8.0.13(@types/node@25.8.0)(esbuild@0.28.0)(tsx@4.22.0)(yaml@2.9.0))': + '@vitejs/plugin-react@6.0.2(vite@8.0.13(@types/node@25.8.0)(esbuild@0.28.0)(jiti@1.21.7)(tsx@4.22.0)(yaml@2.9.0))': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: 8.0.13(@types/node@25.8.0)(esbuild@0.28.0)(tsx@4.22.0)(yaml@2.9.0) + vite: 8.0.13(@types/node@25.8.0)(esbuild@0.28.0)(jiti@1.21.7)(tsx@4.22.0)(yaml@2.9.0) '@vitest/expect@4.1.6': dependencies: @@ -5465,13 +6401,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.6(vite@8.0.13(@types/node@25.8.0)(esbuild@0.28.0)(tsx@4.22.0)(yaml@2.9.0))': + '@vitest/mocker@4.1.6(vite@8.0.13(@types/node@25.8.0)(esbuild@0.28.0)(jiti@1.21.7)(tsx@4.22.0)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.1.6 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.13(@types/node@25.8.0)(esbuild@0.28.0)(tsx@4.22.0)(yaml@2.9.0) + vite: 8.0.13(@types/node@25.8.0)(esbuild@0.28.0)(jiti@1.21.7)(tsx@4.22.0)(yaml@2.9.0) '@vitest/pretty-format@4.1.6': dependencies: @@ -5551,6 +6487,13 @@ snapshots: ansi-styles@5.2.0: {} + any-promise@1.3.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.2 + aproba@2.1.0: {} are-we-there-yet@2.0.0: @@ -5558,12 +6501,18 @@ snapshots: delegates: 1.0.0 readable-stream: 3.6.2 + arg@5.0.2: {} + argparse@1.0.10: dependencies: sprintf-js: 1.0.3 argparse@2.0.1: {} + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + array-back@1.0.4: dependencies: typical: 2.6.1 @@ -5594,6 +6543,15 @@ snapshots: atomic-sleep@1.0.0: {} + autoprefixer@10.5.0(postcss@8.5.14): + dependencies: + browserslist: 4.28.2 + caniuse-lite: 1.0.30001792 + fraction.js: 5.3.4 + picocolors: 1.1.1 + postcss: 8.5.14 + postcss-value-parser: 4.2.0 + b4a@1.8.1: {} balanced-match@1.0.2: {} @@ -5651,6 +6609,8 @@ snapshots: base64-js@1.5.1: {} + baseline-browser-mapping@2.10.29: {} + before-after-hook@2.2.3: {} better-sqlite3@12.10.0: @@ -5658,6 +6618,8 @@ snapshots: bindings: 1.5.0 prebuild-install: 7.1.3 + binary-extensions@2.3.0: {} + bindings@1.5.0: dependencies: file-uri-to-path: 1.0.0 @@ -5695,6 +6657,14 @@ snapshots: dependencies: fill-range: 7.1.1 + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.29 + caniuse-lite: 1.0.30001792 + electron-to-chromium: 1.5.357 + node-releases: 2.0.44 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + buffer-crc32@1.0.0: {} buffer-from@1.1.2: {} @@ -5738,6 +6708,8 @@ snapshots: callsites@3.1.0: {} + camelcase-css@2.0.1: {} + camelcase-keys@6.2.2: dependencies: camelcase: 5.3.1 @@ -5746,6 +6718,8 @@ snapshots: camelcase@5.3.1: {} + caniuse-lite@1.0.30001792: {} + catharsis@0.9.0: dependencies: lodash: 4.18.1 @@ -5765,6 +6739,18 @@ snapshots: chardet@0.7.0: {} + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + chownr@1.1.4: {} chownr@2.0.0: {} @@ -5789,6 +6775,8 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 6.2.0 + clsx@2.1.1: {} + cmake-ts@1.0.2: {} collect-all@1.0.4: @@ -5836,6 +6824,8 @@ snapshots: commander@14.0.3: {} + commander@4.1.1: {} + common-sequence@2.0.2: {} concat-map@0.0.1: {} @@ -5876,6 +6866,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + cssesc@3.0.0: {} + csstype@3.2.3: {} date-fns@2.30.0: @@ -5937,6 +6929,10 @@ snapshots: detect-libc@2.1.2: {} + detect-node-es@1.1.0: {} + + didyoumean@1.2.2: {} + diff-sequences@29.6.3: {} dijkstrajs@1.0.3: {} @@ -5947,6 +6943,8 @@ snapshots: discord-api-types@0.38.47: {} + dlv@1.1.3: {} + dmd@4.0.6: dependencies: array-back: 4.0.2 @@ -6001,6 +6999,8 @@ snapshots: ee-first@1.1.1: {} + electron-to-chromium@1.5.357: {} + emoji-regex@7.0.3: {} emoji-regex@8.0.0: {} @@ -6110,6 +7110,8 @@ snapshots: '@esbuild/win32-ia32': 0.28.0 '@esbuild/win32-x64': 0.28.0 + escalade@3.2.0: {} + escape-html@1.0.3: {} escape-string-regexp@1.0.5: {} @@ -6388,6 +7390,8 @@ snapshots: forwarded@0.2.0: {} + fraction.js@5.3.4: {} + fresh@2.0.0: {} fs-constants@1.0.0: {} @@ -6450,6 +7454,8 @@ snapshots: hasown: 2.0.3 math-intrinsics: 1.1.0 + get-nonce@1.0.1: {} + get-proto@1.0.1: dependencies: dunder-proto: 1.0.1 @@ -6470,6 +7476,10 @@ snapshots: dependencies: is-glob: 4.0.3 + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + glob@7.2.3: dependencies: fs.realpath: 1.0.0 @@ -6608,6 +7618,10 @@ snapshots: is-arrayish@0.2.1: {} + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + is-core-module@2.16.2: dependencies: hasown: 2.0.3 @@ -6669,6 +7683,8 @@ snapshots: jest-get-type@29.6.3: {} + jiti@1.21.7: {} + joycon@3.1.1: {} js-tokens@4.0.0: {} @@ -6823,6 +7839,8 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 + lilconfig@3.1.3: {} + lines-and-columns@1.2.4: {} linkify-it@3.0.3: @@ -6859,6 +7877,10 @@ snapshots: dependencies: yallist: 4.0.0 + lucide-react@1.16.0(react@19.2.6): + dependencies: + react: 19.2.6 + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -6983,6 +8005,12 @@ snapshots: mute-stream@0.0.8: {} + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + nanoid@3.3.12: {} napi-build-utils@2.0.0: {} @@ -7023,6 +8051,8 @@ snapshots: node-int64@0.4.0: {} + node-releases@2.0.44: {} + nopt@5.0.0: dependencies: abbrev: 1.1.1 @@ -7041,6 +8071,8 @@ snapshots: semver: 7.8.0 validate-npm-package-license: 3.0.4 + normalize-path@3.0.0: {} + npm-run-path@6.0.0: dependencies: path-key: 4.0.0 @@ -7057,6 +8089,8 @@ snapshots: object-get@2.1.1: {} + object-hash@3.0.0: {} + object-inspect@1.13.4: {} object-keys@1.1.1: {} @@ -7216,6 +8250,8 @@ snapshots: picomatch@4.0.4: {} + pify@2.3.0: {} + pino-abstract-transport@3.0.0: dependencies: split2: 4.2.0 @@ -7259,6 +8295,8 @@ snapshots: sonic-boom: 4.2.1 thread-stream: 4.2.0 + pirates@4.0.7: {} + pkg-pr-new@0.0.62: dependencies: '@actions/core': 1.11.1 @@ -7288,6 +8326,37 @@ snapshots: pngjs@5.0.0: {} + postcss-import@15.1.0(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.12 + + postcss-js@4.1.0(postcss@8.5.14): + dependencies: + camelcase-css: 2.0.1 + postcss: 8.5.14 + + postcss-load-config@4.0.2(postcss@8.5.14): + dependencies: + lilconfig: 3.1.3 + yaml: 2.9.0 + optionalDependencies: + postcss: 8.5.14 + + postcss-nested@6.2.0(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + postcss-selector-parser: 6.1.2 + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + postcss@8.5.14: dependencies: nanoid: 3.3.12 @@ -7424,8 +8493,39 @@ snapshots: react-is@18.3.1: {} + react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.6): + dependencies: + react: 19.2.6 + react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.6) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 + + react-remove-scroll@2.7.2(@types/react@19.2.14)(react@19.2.6): + dependencies: + react: 19.2.6 + react-remove-scroll-bar: 2.3.8(@types/react@19.2.14)(react@19.2.6) + react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.6) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@19.2.14)(react@19.2.6) + use-sidecar: 1.1.3(@types/react@19.2.14)(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.14 + + react-style-singleton@2.2.3(@types/react@19.2.14)(react@19.2.6): + dependencies: + get-nonce: 1.0.1 + react: 19.2.6 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 + react@19.2.6: {} + read-cache@1.0.0: + dependencies: + pify: 2.3.0 + read-pkg-up@7.0.1: dependencies: find-up: 4.1.0 @@ -7455,6 +8555,10 @@ snapshots: string_decoder: 1.3.0 util-deprecate: 1.0.2 + readdirp@3.6.0: + dependencies: + picomatch: 2.3.2 + real-require@0.2.0: {} real-require@1.0.0: {} @@ -7832,6 +8936,16 @@ snapshots: strip-json-comments@5.0.3: {} + sucrase@3.35.1: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + tinyglobby: 0.2.16 + ts-interface-checker: 0.1.13 + supports-color@5.5.0: dependencies: has-flag: 3.0.0 @@ -7864,6 +8978,35 @@ snapshots: taffydb@2.6.2: {} + tailwind-merge@3.6.0: {} + + tailwindcss@3.4.17: + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.6.0 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.3 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.7 + lilconfig: 3.1.3 + micromatch: 4.0.8 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.14 + postcss-import: 15.1.0(postcss@8.5.14) + postcss-js: 4.1.0(postcss@8.5.14) + postcss-load-config: 4.0.2(postcss@8.5.14) + postcss-nested: 6.2.0(postcss@8.5.14) + postcss-selector-parser: 6.1.2 + resolve: 1.22.12 + sucrase: 3.35.1 + transitivePeerDependencies: + - ts-node + tar-fs@2.1.4: dependencies: chownr: 1.1.4 @@ -7924,6 +9067,14 @@ snapshots: text-table@0.2.0: {} + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + thirty-two@1.0.2: {} thread-stream@4.2.0: @@ -7983,6 +9134,8 @@ snapshots: trim-newlines@3.0.1: {} + ts-interface-checker@0.1.13: {} + ts-mixer@6.0.4: {} tsd@0.33.0: @@ -8080,12 +9233,33 @@ snapshots: graceful-fs: 4.2.11 node-int64: 0.4.0 + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + uri-js@4.4.1: dependencies: punycode: 2.3.1 url-join@5.0.0: {} + use-callback-ref@1.3.3(@types/react@19.2.14)(react@19.2.6): + dependencies: + react: 19.2.6 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 + + use-sidecar@1.1.3(@types/react@19.2.14)(react@19.2.6): + dependencies: + detect-node-es: 1.1.0 + react: 19.2.6 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 + util-deprecate@1.0.2: {} v8-compile-cache@2.4.0: {} @@ -8099,7 +9273,7 @@ snapshots: vary@1.1.2: {} - vite@8.0.13(@types/node@25.8.0)(esbuild@0.28.0)(tsx@4.22.0)(yaml@2.9.0): + vite@8.0.13(@types/node@25.8.0)(esbuild@0.28.0)(jiti@1.21.7)(tsx@4.22.0)(yaml@2.9.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -8110,13 +9284,14 @@ snapshots: '@types/node': 25.8.0 esbuild: 0.28.0 fsevents: 2.3.3 + jiti: 1.21.7 tsx: 4.22.0 yaml: 2.9.0 - vitest@4.1.6(@opentelemetry/api@1.9.1)(@types/node@25.8.0)(vite@8.0.13(@types/node@25.8.0)(esbuild@0.28.0)(tsx@4.22.0)(yaml@2.9.0)): + vitest@4.1.6(@opentelemetry/api@1.9.1)(@types/node@25.8.0)(vite@8.0.13(@types/node@25.8.0)(esbuild@0.28.0)(jiti@1.21.7)(tsx@4.22.0)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.6 - '@vitest/mocker': 4.1.6(vite@8.0.13(@types/node@25.8.0)(esbuild@0.28.0)(tsx@4.22.0)(yaml@2.9.0)) + '@vitest/mocker': 4.1.6(vite@8.0.13(@types/node@25.8.0)(esbuild@0.28.0)(jiti@1.21.7)(tsx@4.22.0)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.6 '@vitest/runner': 4.1.6 '@vitest/snapshot': 4.1.6 @@ -8133,7 +9308,7 @@ snapshots: tinyexec: 1.1.2 tinyglobby: 0.2.16 tinyrainbow: 3.1.0 - vite: 8.0.13(@types/node@25.8.0)(esbuild@0.28.0)(tsx@4.22.0)(yaml@2.9.0) + vite: 8.0.13(@types/node@25.8.0)(esbuild@0.28.0)(jiti@1.21.7)(tsx@4.22.0)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.1 diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..7738160 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,5 @@ +export default { + plugins: { + autoprefixer: {}, + }, +}; diff --git a/public/dashboard.css b/public/dashboard.css deleted file mode 100644 index d0e5c52..0000000 --- a/public/dashboard.css +++ /dev/null @@ -1,417 +0,0 @@ -:root { - --bg: #080a0f; - --panel: rgba(18, 22, 32, 0.86); - --panel-strong: #121720; - --line: rgba(255, 255, 255, 0.12); - --text: #edf4ff; - --muted: #91a0b6; - --faint: #536176; - --cyan: #00e5ff; - --green: #39ff88; - --yellow: #ffe45e; - --red: #ff4f6d; - --blue: #6275ff; - --shadow: 0 24px 70px rgba(0, 0, 0, 0.46); - } - - * { box-sizing: border-box; } - - body { - margin: 0; - min-height: 100vh; - color: var(--text); - font-family: Manrope, sans-serif; - background: - radial-gradient(circle at 12% 8%, rgba(0, 229, 255, 0.18), transparent 32rem), - radial-gradient(circle at 88% 0%, rgba(98, 117, 255, 0.2), transparent 28rem), - linear-gradient(145deg, #05060a, var(--bg)); - overflow-x: hidden; - } - - body::before { - content: ""; - position: fixed; - inset: 0; - pointer-events: none; - background-image: - linear-gradient(rgba(255,255,255,0.035) 1px, transparent 1px), - linear-gradient(90deg, rgba(255,255,255,0.035) 1px, transparent 1px); - background-size: 42px 42px; - mask-image: linear-gradient(to bottom, black, transparent 84%); - } - - button, select { font: inherit; } - - .shell { - width: min(1440px, calc(100% - 32px)); - margin: 0 auto; - padding: 28px 0 44px; - } - - .hero { - display: grid; - grid-template-columns: 1.2fr 0.8fr; - gap: 18px; - align-items: stretch; - margin-bottom: 18px; - } - - .brand-card, - .status-card, - .tab-panel, - .content-card { - border: 1px solid var(--line); - background: var(--panel); - backdrop-filter: blur(18px); - box-shadow: var(--shadow); - } - - .brand-card { - position: relative; - padding: 28px; - border-radius: 28px; - overflow: hidden; - min-height: 190px; - } - - .brand-card::after { - content: "WATCHER"; - position: absolute; - right: -14px; - bottom: -20px; - font-family: "Archivo Black", sans-serif; - font-size: clamp(58px, 9vw, 132px); - letter-spacing: -0.08em; - color: rgba(255,255,255,0.035); - line-height: 0.78; - } - - .eyebrow { - display: inline-flex; - align-items: center; - gap: 10px; - margin-bottom: 18px; - color: var(--cyan); - font: 700 12px/1 "JetBrains Mono", monospace; - text-transform: uppercase; - letter-spacing: 0.18em; - } - - .pulse { - width: 9px; - height: 9px; - border-radius: 999px; - background: var(--green); - box-shadow: 0 0 20px var(--green); - } - - h1 { - margin: 0; - max-width: 840px; - font-family: "Archivo Black", sans-serif; - font-size: clamp(40px, 6vw, 82px); - line-height: 0.88; - letter-spacing: -0.06em; - text-transform: uppercase; - } - - .subtitle { - margin: 18px 0 0; - max-width: 720px; - color: var(--muted); - font-size: 15px; - line-height: 1.7; - } - - .status-card { - border-radius: 28px; - padding: 24px; - display: grid; - gap: 14px; - } - - .status-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - padding: 14px 0; - border-bottom: 1px solid var(--line); - } - - .status-row:last-child { border-bottom: 0; } - - .status-label { - color: var(--muted); - font: 700 12px/1 "JetBrains Mono", monospace; - text-transform: uppercase; - letter-spacing: 0.12em; - } - - .status-value { - display: inline-flex; - align-items: center; - gap: 8px; - color: var(--text); - font: 700 13px/1 "JetBrains Mono", monospace; - } - - .dot { - width: 9px; - height: 9px; - border-radius: 99px; - background: var(--faint); - } - - .dot.on { background: var(--green); box-shadow: 0 0 16px var(--green); } - .dot.warn { background: var(--yellow); box-shadow: 0 0 16px var(--yellow); } - - .tab-panel { - position: sticky; - top: 14px; - z-index: 5; - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - padding: 12px; - border-radius: 24px; - margin-bottom: 18px; - } - - .tabs { - display: flex; - gap: 8px; - flex-wrap: wrap; - } - - .tab-btn { - border: 1px solid transparent; - color: var(--muted); - background: transparent; - border-radius: 16px; - padding: 12px 16px; - cursor: pointer; - font-weight: 800; - transition: 160ms ease; - } - - .tab-btn:hover { color: var(--text); background: rgba(255,255,255,0.06); } - .tab-btn.active { - color: #061014; - background: linear-gradient(135deg, var(--cyan), var(--green)); - box-shadow: 0 12px 28px rgba(0,229,255,0.18); - } - - .filter-row { - display: flex; - align-items: center; - gap: 10px; - color: var(--muted); - font-size: 13px; - } - - select { - min-width: 240px; - color: var(--text); - background: rgba(5,8,14,0.78); - border: 1px solid var(--line); - border-radius: 14px; - padding: 11px 14px; - outline: none; - } - - .grid { - display: grid; - grid-template-columns: minmax(0, 1fr); - gap: 18px; - } - - .tab-content { display: none; } - .tab-content.active { display: block; } - - .voice-layout { - display: grid; - grid-template-columns: 380px 1fr; - gap: 18px; - } - - .content-card { - border-radius: 28px; - padding: 22px; - } - - .card-title { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - margin-bottom: 18px; - } - - .card-title h2 { - margin: 0; - font-family: "Archivo Black", sans-serif; - font-size: 26px; - letter-spacing: -0.04em; - text-transform: uppercase; - } - - .mini { - color: var(--faint); - font: 700 11px/1 "JetBrains Mono", monospace; - text-transform: uppercase; - letter-spacing: 0.12em; - } - - .field-group { display: grid; gap: 8px; margin-bottom: 14px; } - .field-group label { color: var(--muted); font-size: 13px; font-weight: 800; } - - .button-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } - - .btn { - border: 0; - border-radius: 16px; - padding: 13px 16px; - color: #061014; - cursor: pointer; - font-weight: 900; - transition: transform 140ms ease, filter 140ms ease; - } - - .btn:hover { transform: translateY(-1px); filter: brightness(1.08); } - .btn-primary { background: linear-gradient(135deg, var(--cyan), var(--blue)); color: white; } - .btn-success { background: linear-gradient(135deg, var(--green), var(--cyan)); } - .btn-danger { background: linear-gradient(135deg, var(--red), #ff9a6b); color: white; } - - .voice-status { color: var(--muted); font-size: 13px; margin-top: 12px; min-height: 20px; } - - .visualizer { - display: flex; - align-items: flex-end; - gap: 5px; - height: 130px; - padding: 16px; - border: 1px solid var(--line); - border-radius: 20px; - background: rgba(0,0,0,0.22); - overflow: hidden; - } - - .bar { - flex: 1; - min-width: 5px; - border-radius: 999px; - height: 3px; - background: linear-gradient(to top, var(--blue), var(--cyan), var(--green)); - box-shadow: 0 0 18px rgba(0,229,255,0.16); - } - - .participants { - display: grid; - gap: 10px; - } - - .user-item { - display: flex; - align-items: center; - gap: 12px; - padding: 12px; - border: 1px solid var(--line); - border-radius: 18px; - background: rgba(255,255,255,0.035); - } - - .user-item.speaking { border-color: rgba(57,255,136,0.55); background: rgba(57,255,136,0.08); } - .user-item img { width: 34px; height: 34px; border-radius: 999px; } - - .feed { - display: grid; - gap: 12px; - } - - .event-card { - display: grid; - gap: 10px; - padding: 16px; - border: 1px solid var(--line); - border-radius: 20px; - background: rgba(255,255,255,0.035); - } - - .event-head { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - } - - .author { - display: flex; - align-items: center; - gap: 10px; - min-width: 0; - } - - .avatar { - width: 34px; - height: 34px; - border-radius: 999px; - background: linear-gradient(135deg, var(--blue), var(--cyan)); - flex: 0 0 auto; - } - - .avatar img { width: 100%; height: 100%; border-radius: inherit; object-fit: cover; } - - .name { font-weight: 900; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } - .time { color: var(--faint); font: 600 11px/1 "JetBrains Mono", monospace; white-space: nowrap; } - .message-text { color: #dbe6f7; line-height: 1.6; white-space: pre-wrap; word-break: break-word; } - - .sticker-strip, .attachment-strip { display: flex; gap: 10px; flex-wrap: wrap; } - .sticker-img { width: 96px; height: 96px; object-fit: contain; border-radius: 16px; background: rgba(0,0,0,0.22); border: 1px solid var(--line); padding: 8px; } - .attachment-chip { color: var(--cyan); text-decoration: none; border: 1px solid var(--line); border-radius: 14px; padding: 8px 10px; font: 700 12px/1 "JetBrains Mono", monospace; background: rgba(0,229,255,0.06); } - .embed-card { border-left: 4px solid var(--blue); border-radius: 16px; padding: 12px; background: rgba(98,117,255,0.08); display: grid; gap: 8px; } - .embed-title { font-weight: 900; color: var(--text); } - .embed-description { color: var(--muted); line-height: 1.5; white-space: pre-wrap; } - .embed-image { max-width: 360px; width: 100%; border-radius: 14px; border: 1px solid var(--line); } - - .badges { display: flex; gap: 8px; flex-wrap: wrap; } - .badge { - border: 1px solid var(--line); - border-radius: 999px; - padding: 5px 9px; - color: var(--muted); - font: 700 10px/1 "JetBrains Mono", monospace; - text-transform: uppercase; - } - .badge.edit { color: var(--yellow); border-color: rgba(255,228,94,0.36); } - .badge.delete { color: var(--red); border-color: rgba(255,79,109,0.42); } - - .filename { font-size: 13px; font-weight: 900; word-break: break-word; } - .link { color: var(--cyan); text-decoration: none; font-weight: 900; } - .link:hover { text-decoration: underline; } - - .empty { - padding: 34px; - text-align: center; - color: var(--faint); - border: 1px dashed var(--line); - border-radius: 22px; - } - - .error { - display: none; - margin-bottom: 14px; - padding: 12px 14px; - color: #ffd8df; - border: 1px solid rgba(255,79,109,0.5); - border-radius: 16px; - background: rgba(255,79,109,0.12); - } - - @media (max-width: 980px) { - .hero, .voice-layout { grid-template-columns: 1fr; } - .tab-panel { align-items: stretch; flex-direction: column; } - .filter-row { align-items: stretch; flex-direction: column; } - select { width: 100%; min-width: 0; } - } diff --git a/public/index.html b/public/index.html deleted file mode 100644 index 0987cc3..0000000 --- a/public/index.html +++ /dev/null @@ -1,218 +0,0 @@ - - - - - - Discord Moderation Watcher - - - - -
-
-
-
Discord moderation watcher
-

Voice. Text. One Watch Floor.

-

Static shared-state dashboard with legacy working voice bridge and captured Discord messages.

-
-
-
WebSocketConnecting
-
Voice LinkNot connected
-
Active TabVoice
-
-
- - - -
- -
-
-
-

Voice Control

bridge
-
-
-
-
Idle
-
-
-

Live Audio

Speaker Off
-
-
-
-
-

Media

Idle
-
- -
- - -
-
-
-
-
-
- -
No media queued
-
-
-

Participants

speaking now
-
- -

All Messages

all captures
Select channel to view text captures

Needs Review

warn + flagged
No warned or flagged messages
-
- - - - diff --git a/src/webserver.ts b/src/webserver.ts index d28ad27..8cada7d 100644 --- a/src/webserver.ts +++ b/src/webserver.ts @@ -5,7 +5,11 @@ import { fileURLToPath } from "node:url"; import { Streamer } from "@dank074/discord-video-stream"; import { AudioPlayerStatus } from "@discordjs/voice"; import type { Client } from "discord.js-selfbot-v13"; -import express from "express"; +import express, { + type NextFunction, + type Request, + type Response, +} from "express"; import helmet from "helmet"; import * as prism from "prism-media"; import { WebSocketServer } from "ws"; @@ -74,11 +78,11 @@ let sharedUIState: SharedUIState = { ...defaultSharedUIState }; export function normalizeSharedUIState( value: SharedUIStatePatch, ): SharedUIState { - const legacyGuild = value.selectedGuild ?? ""; + const guild = value.selectedGuild ?? ""; return { - selectedVoiceGuild: value.selectedVoiceGuild ?? legacyGuild, + selectedVoiceGuild: value.selectedVoiceGuild ?? guild, selectedVoiceChannel: value.selectedVoiceChannel ?? "", - selectedTextGuild: value.selectedTextGuild ?? legacyGuild, + selectedTextGuild: value.selectedTextGuild ?? guild, selectedTextChannel: value.selectedTextChannel ?? "", activeTab: value.activeTab === "text" ? "text" : "voice", isListening: value.isListening ?? false, @@ -128,13 +132,15 @@ function patchSharedUIState(patch: SharedUIStatePatch) { // Upsample 24kHz mono s16le → 48kHz stereo s16le (pure JS) function upsample(mono24k: Buffer): Buffer { - const out = Buffer.alloc(mono24k.length * 4); - for (let i = 0; i < mono24k.length / 2; i++) { + const numSamples = mono24k.length / 2; + const out = Buffer.alloc(numSamples * 8); + for (let i = 0; i < numSamples; i++) { const s = mono24k.readInt16LE(i * 2); - out.writeInt16LE(s, i * 8); - out.writeInt16LE(s, i * 8 + 2); - out.writeInt16LE(s, i * 8 + 4); - out.writeInt16LE(s, i * 8 + 6); + const base = i * 8; + out.writeInt16LE(s, base); + out.writeInt16LE(s, base + 2); + out.writeInt16LE(s, base + 4); + out.writeInt16LE(s, base + 6); } return out; } @@ -148,7 +154,7 @@ function rmsDb(pcm: Buffer): number { sum += s * s; } const rms = Math.sqrt(sum / samples); - return 20 * Math.log10(Math.max(rms, 1e-10)); + return 20 * Math.log10(rms); } export async function startWebserver( @@ -189,7 +195,7 @@ export async function startWebserver( }), ); - app.use((req, res, next) => { + app.use((req: Request, res: Response, next: NextFunction) => { if (req.path.startsWith("/api/")) { res.set("Cache-Control", "no-store"); } @@ -213,17 +219,19 @@ export async function startWebserver( app.use(express.static(path.join(__dirname, "../public"))); - app.get("/", (_req, res) => { + app.get("/", (_req: Request, res: Response) => { const reactIndex = path.join(__dirname, "../public/app/index.html"); if (fs.existsSync(reactIndex)) { res.sendFile(reactIndex); - } else { - res.sendFile(path.join(__dirname, "../public/index.html")); + return; } + res + .status(503) + .send("React dashboard is not built. Run pnpm run build:web."); }); // Health check endpoint - app.get("/health", (_req, res) => { + app.get("/health", (_req: Request, res: Response) => { res.json({ status: "ok", timestamp: new Date().toISOString(), @@ -234,7 +242,7 @@ export async function startWebserver( }); // Metrics endpoint - app.get("/metrics", async (_req, res) => { + app.get("/metrics", async (_req: Request, res: Response) => { res.set("Content-Type", "text/plain"); uptimeGauge.set(process.uptime()); res.send(await getMetrics()); diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..59973ba --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,27 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ["./frontend/index.html", "./frontend/src/**/*.{ts,tsx}"], + theme: { + extend: { + colors: { + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + primary: { DEFAULT: "hsl(var(--primary))", foreground: "hsl(var(--primary-foreground))" }, + secondary: { DEFAULT: "hsl(var(--secondary))", foreground: "hsl(var(--secondary-foreground))" }, + muted: { DEFAULT: "hsl(var(--muted))", foreground: "hsl(var(--muted-foreground))" }, + accent: { DEFAULT: "hsl(var(--accent))", foreground: "hsl(var(--accent-foreground))" }, + destructive: { DEFAULT: "hsl(var(--destructive))", foreground: "hsl(var(--destructive-foreground))" }, + card: { DEFAULT: "hsl(var(--card))", foreground: "hsl(var(--card-foreground))" }, + }, + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + }, + }, + plugins: [], +};