feat: init commit for dashboard

This commit is contained in:
TheodorTomas
2026-01-28 14:45:25 +04:00
parent e4bd9a839a
commit ca7e2c64de
29 changed files with 1587 additions and 14 deletions

View File

@@ -0,0 +1,196 @@
"use client";
import {
ActivityIcon,
AreaChartIcon,
BarChart3Icon,
LineChartIcon,
MapIcon,
PieChartIcon,
ScatterChart,
SearchIcon,
TableIcon,
} from "lucide-react";
import { useState } from "react";
import { cn } from "@/lib/cn";
import { Button } from "@/modules/ui/components/button";
import { Input } from "@/modules/ui/components/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/modules/ui/components/select";
// --- Mock Data ---
const DATASETS = ["FCC 2018 Survey", "users_channels", "messages", "new_members_daily", "users"];
const CHART_TYPES = [
{ id: "area", name: "Area Chart", icon: AreaChartIcon },
{ id: "bar", name: "Bar Chart", icon: BarChart3Icon },
{ id: "line", name: "Line Chart", icon: LineChartIcon },
{ id: "pie", name: "Pie Chart", icon: PieChartIcon },
{ id: "table", name: "Table", icon: TableIcon },
{ id: "big_number", name: "Big Number", icon: ActivityIcon }, // Fallback icon
{ id: "scatter", name: "Scatter Plot", icon: ScatterChart }, // Fallback icon
{ id: "map", name: "World Map", icon: MapIcon },
];
export default function ChartBuilderPage() {
const [selectedDataset, setSelectedDataset] = useState<string>("");
const [selectedChartType, setSelectedChartType] = useState<string>("");
const [searchQuery, setSearchQuery] = useState("");
const [userQuery, setUserQuery] = useState("");
const filteredChartTypes = CHART_TYPES.filter((type) =>
type.name.toLowerCase().includes(searchQuery.toLowerCase())
);
return (
<div className="mx-auto max-w-5xl p-6">
<div className="mb-8">
<h1 className="text-2xl font-semibold text-gray-900">Create a new chart</h1>
</div>
<div className="grid gap-8">
{/* Option 1: Ask AI */}
<div className="space-y-4 rounded-lg border border-gray-200 bg-white p-6 shadow-sm">
<div className="mb-4 flex items-center gap-2">
<div className="bg-brand-dark/10 flex h-8 w-8 items-center justify-center rounded-full">
<ActivityIcon className="text-brand-dark h-5 w-5" />
</div>
<div>
<h2 className="font-semibold text-gray-900">Ask your data</h2>
<p className="text-sm text-gray-500">
Describe what you want to see and let AI build the chart.
</p>
</div>
</div>
<div className="flex gap-4">
<Input
placeholder="e.g. How many users signed up last week?"
value={userQuery}
onChange={(e) => setUserQuery(e.target.value)}
className="flex-1"
/>
<Button
disabled={!userQuery}
className="bg-brand-dark hover:bg-brand-dark/90"
onClick={async () => {
const response = await fetch("/api/analytics/query", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt: userQuery }),
});
const data = await response.json();
if (data.data) {
console.log("Chart Data:", data);
alert("Chart generated! Check console for data. (Visualization implementation pending)");
}
}}>
Generate
</Button>
</div>
</div>
<div className="relative">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="w-full border-t border-gray-200" />
</div>
<div className="relative flex justify-center">
<span className="bg-gray-50 px-2 text-sm text-gray-500">OR</span>
</div>
</div>
{/* Option 2: Build Manually */}
<div className="space-y-8 opacity-75 transition-opacity hover:opacity-100">
<div className="space-y-4">
<div className="flex items-center gap-2">
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-gray-200 text-xs font-bold text-gray-600">
1
</span>
<h2 className="font-medium text-gray-900">Choose a dataset</h2>
</div>
<div className="ml-8 max-w-md">
<Select value={selectedDataset} onValueChange={setSelectedDataset}>
<SelectTrigger className="bg-white">
<SelectValue placeholder="Choose a dataset" />
</SelectTrigger>
<SelectContent>
{DATASETS.map((ds) => (
<SelectItem key={ds} value={ds}>
{ds}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-4">
<div className="flex items-center gap-2">
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-gray-200 text-xs font-bold text-gray-600">
2
</span>
<h2 className="font-medium text-gray-900">Choose chart type</h2>
</div>
<div className="ml-8 rounded-lg border border-gray-200 bg-white p-4">
<div className="relative mb-4">
<SearchIcon className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" />
<Input
placeholder="Search all charts"
className="pl-9"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6">
{filteredChartTypes.map((chart) => {
const isSelected = selectedChartType === chart.id;
return (
<div
key={chart.id}
role="button"
tabIndex={0}
onClick={() => setSelectedChartType(chart.id)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") setSelectedChartType(chart.id);
}}
className={cn(
"focus:ring-brand-dark cursor-pointer rounded-md border p-4 text-center transition-all hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2",
isSelected
? "border-brand-dark ring-brand-dark bg-brand-dark/5 ring-1"
: "border-gray-200 hover:border-gray-300"
)}>
<div className="mx-auto mb-3 flex h-12 w-12 items-center justify-center rounded bg-gray-100">
<chart.icon className="h-6 w-6 text-gray-600" strokeWidth={1.5} />
</div>
<span className="text-xs font-medium text-gray-700">{chart.name}</span>
</div>
);
})}
</div>
</div>
</div>
<div className="flex justify-end pt-2">
<Button
disabled={!selectedDataset || !selectedChartType}
variant="outline"
onClick={() => {
alert("Manual chart creation triggered");
}}>
Create Manually
</Button>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,109 @@
"use client";
import { formatDistanceToNow } from "date-fns";
import { Edit2Icon, PlusIcon, SearchIcon, TrashIcon } from "lucide-react";
import Link from "next/link";
import { use } from "react";
import { Button } from "@/modules/ui/components/button";
import { Input } from "@/modules/ui/components/input";
import { useAnalysisStore } from "../store";
export default function ChartsListPage({ params }: { params: Promise<{ environmentId: string }> }) {
use(params);
const { charts, dashboards } = useAnalysisStore();
// Helper to find dashboard names
const getDashboardNames = (dashboardIds: string[]) => {
return dashboardIds
.map((id) => dashboards.find((d) => d.id === id)?.name)
.filter(Boolean)
.join(", ");
};
return (
<div className="flex h-full flex-col">
{/* Header / Actions */}
<div className="flex flex-col gap-4 border-b border-gray-200 bg-white px-6 py-4">
<div className="flex items-center justify-between">
<h1 className="text-xl font-semibold text-gray-900">Charts</h1>
<div className="flex items-center gap-2">
<Link href="chart-builder">
<Button size="sm">
<PlusIcon className="mr-2 h-4 w-4" />
Chart
</Button>
</Link>
</div>
</div>
<div className="flex items-center gap-4">
<div className="relative">
<SearchIcon className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" />
<Input placeholder="Type a value" className="w-[300px] pl-9" />
</div>
{/* Filter Dropdowns */}
<div className="no-scrollbar flex items-center gap-2 overflow-x-auto">
{["Type", "Dataset", "Owner", "Dashboard", "Favorite", "Certified", "Modified by"].map(
(filter) => (
<Button key={filter} variant="outline" className="whitespace-nowrap text-gray-500" size="sm">
{filter} <span className="ml-2 text-xs"></span>
</Button>
)
)}
</div>
</div>
</div>
{/* Table Content */}
<div className="flex-1 overflow-auto bg-gray-50 p-6">
<div className="rounded-lg border border-gray-200 bg-white shadow-sm">
<table className="w-full text-left text-sm text-gray-600">
<thead className="bg-gray-50 text-xs font-semibold uppercase text-gray-500">
<tr>
<th className="border-b border-gray-200 px-6 py-3">Name</th>
<th className="border-b border-gray-200 px-6 py-3">Type</th>
<th className="border-b border-gray-200 px-6 py-3">Dataset</th>
<th className="border-b border-gray-200 px-6 py-3">On dashboards</th>
{/* Hiding owners to save space if needed, mirroring Superset compact view */}
<th className="border-b border-gray-200 px-6 py-3">Last Modified</th>
<th className="border-b border-gray-200 px-6 py-3 text-right">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
{charts.map((chart) => (
<tr key={chart.id} className="group transition-colors hover:bg-gray-50">
<td className="text-brand-dark px-6 py-4 font-medium">
<span className="cursor-pointer hover:underline">{chart.name}</span>
</td>
<td className="px-6 py-4 text-gray-900">
{chart.type.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase())}
</td>
<td className="text-brand-dark cursor-pointer px-6 py-4 hover:underline">
{chart.dataset}
</td>
<td className="text-brand-dark px-6 py-4">
{getDashboardNames(chart.dashboardIds) || "-"}
</td>
<td className="px-6 py-4">
{formatDistanceToNow(new Date(chart.lastModified), { addSuffix: true })}
</td>
<td className="px-6 py-4 text-right">
<div className="flex items-center justify-end gap-2 opacity-0 transition-opacity group-hover:opacity-100">
<Button variant="ghost" size="icon" className="h-8 w-8 hover:bg-gray-100">
<TrashIcon className="h-4 w-4 text-gray-500" />
</Button>
<Button variant="ghost" size="icon" className="h-8 w-8 hover:bg-gray-100">
<Edit2Icon className="h-4 w-4 text-gray-500" />
</Button>
</div>
</td>
</tr>
))}
</tbody>
</table>
{charts.length === 0 && <div className="p-12 text-center text-gray-500">No charts found.</div>}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,183 @@
"use client";
import { MoreHorizontalIcon, PlusIcon } from "lucide-react";
import Link from "next/link";
import { notFound } from "next/navigation";
import { use } from "react";
import { Bar, BarChart, CartesianGrid, Bar as RechartsBar, XAxis } from "recharts";
import { Button } from "@/modules/ui/components/button";
import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/modules/ui/components/chart";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/modules/ui/components/dropdown-menu";
import { useAnalysisStore } from "../../store";
// --- Mock Chart Components for Widget Content ---
const MockBarChart = () => {
const data = [
{ name: "Jan", value: 400 },
{ name: "Feb", value: 300 },
{ name: "Mar", value: 200 },
{ name: "Apr", value: 278 },
{ name: "May", value: 189 },
{ name: "Jun", value: 239 },
];
return (
<ChartContainer config={{ value: { label: "Value", color: "#6366f1" } }} className="h-full w-full">
<BarChart accessibilityLayer data={data}>
<CartesianGrid vertical={false} strokeDasharray="3 3" />
<XAxis
dataKey="name"
tickLine={false}
tickMargin={10}
axisLine={false}
tickFormatter={(value) => value.slice(0, 3)}
/>
<ChartTooltip content={<ChartTooltipContent />} />
<Bar dataKey="value" fill="var(--color-value)" radius={4} />
</BarChart>
</ChartContainer>
);
};
const MockBigNumber = ({ title, value }: { title: string; value: string }) => (
<div className="flex h-full flex-col items-center justify-center">
<div className="text-4xl font-bold text-gray-900">{value}</div>
<div className="mt-2 text-sm text-gray-500">{title}</div>
</div>
);
// --- Widget Wrapper ---
const DashboardWidget = ({
title,
children,
error,
}: {
title: string;
children: React.ReactNode;
error?: string;
}) => {
return (
<div className="flex h-full flex-col rounded-sm border border-gray-200 bg-white shadow-sm ring-1 ring-black/5">
{/* Header */}
<div className="flex items-center justify-between border-b border-gray-100 px-4 py-2">
<h3 className="text-sm font-semibold text-gray-800">{title}</h3>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-6 w-6 text-gray-400 hover:text-gray-600">
<MoreHorizontalIcon className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>Force refresh</DropdownMenuItem>
<DropdownMenuItem>View as table</DropdownMenuItem>
<DropdownMenuItem>Maximize</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
{/* Body */}
<div className="relative min-h-[300px] flex-1 p-4">
{error ? (
<div className="flex h-full w-full flex-col items-start justify-center rounded-md border border-red-100 bg-red-50 p-4">
<div className="mb-1 flex items-center gap-2 font-semibold text-red-700">
<div className="rounded-full bg-red-600 p-0.5">
<span className="block h-3 w-3 text-center text-[10px] leading-3 text-white"></span>
</div>
Data error
</div>
<p className="text-xs text-red-600">{error}</p>
</div>
) : (
children
)}
</div>
</div>
);
};
export default function DashboardPage({
params,
}: {
params: Promise<{ environmentId: string; dashboardId: string }>;
}) {
const { dashboardId } = use(params);
const { dashboards } = useAnalysisStore();
// Find dashboard
// For the purpose of the demo/port, we might need to map the ID or just grab the first one if mock data logic is simple
const dashboard = dashboards.find((d) => d.id === dashboardId) || dashboards[0]; // Fallback for demo flow
if (!dashboard) {
return notFound();
}
// Demo: Determine if empty based on ID or simple toggle mechanism
// We'll fake an empty dashboard if ID suggests it or random chance for demo
const isEmpty = dashboard.widgets.length === 0 && dashboard.id !== "d1" && dashboard.id !== "d2";
return (
<div className="min-h-[calc(100vh-120px)] bg-gray-100 p-6">
{/* Dashboard Header Context (Actions, filters) would go here */}
<div className="mb-6 flex justify-end">
<Button variant="outline" size="sm" className="text-brand-dark border-brand-dark/20 mr-2 bg-white">
Draft
</Button>
<Button size="sm">Edit dashboard</Button>
</div>
{isEmpty ? (
// Empty State
<div className="flex h-[400px] flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-white/50">
<div className="mb-4 rounded-full bg-gray-100 p-4">
<div className="h-12 w-12 rounded-md bg-gray-300 opacity-20" /> {/* Abstract File Icon */}
</div>
<h3 className="text-lg font-medium text-gray-900">No Data</h3>
<p className="mt-2 max-w-sm text-center text-gray-500">
There is currently no information to display. Add charts to build your dashboard.
</p>
<Link href="../chart-builder">
<Button className="mt-6" variant="default">
<PlusIcon className="mr-2 h-4 w-4" />
Create Chart
</Button>
</Link>
</div>
) : (
// Grid Layout
<div className="grid grid-cols-12 gap-6">
{/* Row 1 */}
<div className="col-span-12 md:col-span-3">
<DashboardWidget
title="Average NPS Score"
error="Error: Datetime column not provided as part table configuration and is required by this type of chart">
<MockBigNumber title="NPS" value="72" />
</DashboardWidget>
</div>
<div className="col-span-12 md:col-span-9">
<DashboardWidget title="NPS Trend">
<MockBarChart />
</DashboardWidget>
</div>
{/* Row 2 */}
<div className="col-span-12 md:col-span-4">
<DashboardWidget title="Total Responses">
<MockBigNumber title="Total" value="1,293" />
</DashboardWidget>
</div>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,133 @@
"use client";
import { formatDistanceToNow } from "date-fns";
import { MoreHorizontalIcon, PlusIcon, SearchIcon, StarIcon } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { use, useState } from "react";
import { Button } from "@/modules/ui/components/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/modules/ui/components/dropdown-menu";
import { Input } from "@/modules/ui/components/input";
import { useAnalysisStore } from "../store";
export default function DashboardsListPage({ params }: { params: Promise<{ environmentId: string }> }) {
use(params); // Unwrap params to satisfy Next.js, even if unused
const { dashboards, addDashboard } = useAnalysisStore();
const router = useRouter();
const [searchQuery, setSearchQuery] = useState("");
const filteredDashboards = dashboards.filter((dashboard) =>
dashboard.name.toLowerCase().includes(searchQuery.toLowerCase())
);
const handleCreateDashboard = () => {
const newId = `d-${Date.now()}`;
addDashboard({
id: newId,
name: "New Dashboard",
status: "draft",
owners: [{ id: "u1", name: "Admin" }], // Mock owner
lastModified: new Date().toISOString(),
isFavorite: false,
widgets: [],
});
router.push(`dashboard/${newId}`);
};
return (
<div className="flex h-full flex-col">
{/* Header / Actions */}
<div className="flex items-center justify-between border-b border-gray-200 bg-white px-6 py-4">
<div className="flex items-center gap-4">
<div className="relative">
<SearchIcon className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" />
<Input
placeholder="Search dashboards"
className="w-[300px] pl-9"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
{/* Filter Dropdowns - Visual Only for MVP port */}
<Button variant="outline" className="text-gray-500">
Owner <span className="ml-2 text-xs"></span>
</Button>
<Button variant="outline" className="text-gray-500">
Status <span className="ml-2 text-xs"></span>
</Button>
</div>
<Button onClick={handleCreateDashboard}>
<PlusIcon className="mr-2 h-4 w-4" />
Dashboard
</Button>
</div>
{/* Table Content */}
<div className="flex-1 overflow-auto bg-gray-50 p-6">
<div className="rounded-lg border border-gray-200 bg-white shadow-sm">
<table className="w-full text-left text-sm text-gray-600">
<thead className="bg-gray-50 text-xs font-semibold uppercase text-gray-500">
<tr>
<th className="border-b border-gray-200 px-6 py-3">Name</th>
<th className="border-b border-gray-200 px-6 py-3">Status</th>
<th className="border-b border-gray-200 px-6 py-3">Owners</th>
<th className="border-b border-gray-200 px-6 py-3">Last Modified</th>
<th className="border-b border-gray-200 px-6 py-3 text-right">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
{filteredDashboards.map((dashboard) => (
<tr key={dashboard.id} className="group hover:bg-gray-50">
<td className="px-6 py-4 font-medium text-gray-900">
<div className="flex items-center gap-2">
{/* Star Icon - Active state simulation */}
<StarIcon
className={`h-4 w-4 ${dashboard.isFavorite ? "fill-yellow-400 text-yellow-400" : "text-gray-300"}`}
/>
<Link href={`dashboard/${dashboard.id}`}>
<span className="cursor-pointer hover:underline">{dashboard.name}</span>
</Link>
</div>
</td>
<td className="px-6 py-4">
<span className="inline-flex items-center rounded-full bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20">
{dashboard.status === "published" ? "Published" : "Draft"}
</span>
</td>
<td className="px-6 py-4">{dashboard.owners.map((u) => u.name).join(", ")}</td>
<td className="px-6 py-4">
{formatDistanceToNow(new Date(dashboard.lastModified), { addSuffix: true })}
</td>
<td className="px-6 py-4 text-right">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 p-0 opacity-0 group-hover:opacity-100">
<MoreHorizontalIcon className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem className="text-red-600">Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</td>
</tr>
))}
</tbody>
</table>
{filteredDashboards.length === 0 && (
<div className="p-12 text-center text-gray-500">No dashboards found.</div>
)}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,57 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { use } from "react";
import { cn } from "@/lib/cn";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
import { PageHeader } from "@/modules/ui/components/page-header";
const TABS = [
{ name: "Dashboards", href: "dashboards" },
{ name: "Charts", href: "charts" },
];
export default function AnalysisLayout({
children,
params,
}: {
children: React.ReactNode;
params: Promise<{ environmentId: string }>;
}) {
const pathname = usePathname();
const { environmentId } = use(params);
return (
<PageContentWrapper>
<PageHeader pageTitle="Analysis" />
<div className="border-b border-gray-200">
<nav className="-mb-px flex space-x-8" aria-label="Tabs">
{TABS.map((tab) => {
const href = `/environments/${environmentId}/analysis/${tab.href}`;
// Check if pathname starts with the tab href to allow for sub-routes (like dashboard detail)
// But for exact match on list pages, we can be more specific if needed.
// For now, simple prefix check is good for "Charts" vs "Dashboards".
// However, "dashboards/d1" should still highlight "Dashboards".
const isActive = pathname?.includes(`/${tab.href}`);
return (
<Link
key={tab.name}
href={href}
className={cn(
isActive
? "border-brand-dark text-brand-dark"
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700",
"whitespace-nowrap border-b-2 px-1 py-4 text-sm font-medium"
)}>
{tab.name}
</Link>
);
})}
</nav>
</div>
<div className="pt-6">{children}</div>
</PageContentWrapper>
);
}

View File

@@ -0,0 +1,9 @@
import { redirect } from "next/navigation";
export default async function AnalysisPage({ params }: { params: Promise<{ environmentId: string }> }) {
const { environmentId } = await params;
if (!environmentId || environmentId === "undefined") {
redirect("/");
}
redirect(`/environments/${environmentId}/analysis/dashboards`);
}

View File

@@ -0,0 +1,182 @@
import { create } from "zustand";
// --- Types ---
export type DashboardStatus = "published" | "draft";
export interface AnalysisUser {
id: string;
name: string;
}
export interface Dashboard {
id: string;
name: string;
description?: string;
status: DashboardStatus;
owners: AnalysisUser[];
lastModified: string; // ISO Date string
isFavorite: boolean;
widgets: DashboardWidget[];
}
export interface DashboardWidget {
id: string;
type: "chart" | "markdown" | "header" | "divider";
title?: string;
chartId?: string; // If type is chart
layout: {
x: number;
y: number;
w: number;
h: number;
};
}
export type ChartType =
| "area"
| "bar"
| "line"
| "pie"
| "big_number"
| "big_number_total"
| "table"
| "funnel"
| "map";
export interface Chart {
id: string;
name: string;
type: ChartType;
dataset: string;
owners: AnalysisUser[];
lastModified: string;
dashboardIds: string[];
config: Record<string, any>; // Flexible config for specific chart props
}
interface AnalysisState {
dashboards: Dashboard[];
charts: Chart[];
activeDashboard: Dashboard | null;
layoutMode: "view" | "edit";
isLoading: boolean;
// Actions
setDashboards: (dashboards: Dashboard[]) => void;
setCharts: (charts: Chart[]) => void;
setActiveDashboard: (dashboard: Dashboard | null) => void;
setLayoutMode: (mode: "view" | "edit") => void;
addDashboard: (dashboard: Dashboard) => void;
updateDashboard: (id: string, updates: Partial<Dashboard>) => void;
}
// --- Mock Data ---
const MOCK_USERS: AnalysisUser[] = [
{ id: "u1", name: "Admin User" },
{ id: "u2", name: "Jane Doe" },
];
const MOCK_CHARTS: Chart[] = [
{
id: "c1",
name: "Gender",
type: "pie",
dataset: "FCC 2018 Survey",
owners: [MOCK_USERS[0]],
lastModified: new Date(Date.now() - 1000 * 60 * 3).toISOString(), // 3 mins ago
dashboardIds: ["d1"],
config: {},
},
{
id: "c2",
name: "Cross Channel Relationship",
type: "bar", // Using bar as approximation for chord if not available
dataset: "users_channels-uzooNNtSRO",
owners: [MOCK_USERS[0]],
lastModified: new Date(Date.now() - 1000 * 60 * 3).toISOString(),
dashboardIds: ["d2"],
config: {},
},
{
id: "c3",
name: "Weekly Messages",
type: "line",
dataset: "messages",
owners: [MOCK_USERS[0]],
lastModified: new Date(Date.now() - 1000 * 60 * 14).toISOString(), // 14 mins ago
dashboardIds: ["d2"],
config: {},
},
{
id: "c4",
name: "New Members per Month",
type: "line",
dataset: "new_members_daily",
owners: [MOCK_USERS[0]],
lastModified: new Date(Date.now() - 1000 * 60 * 14).toISOString(),
dashboardIds: ["d2"],
config: {},
},
{
id: "c5",
name: "Number of Members",
type: "big_number",
dataset: "users",
owners: [MOCK_USERS[0]],
lastModified: new Date(Date.now() - 1000 * 60 * 14).toISOString(),
dashboardIds: ["d2"],
config: {},
},
];
const MOCK_DASHBOARDS: Dashboard[] = [
{
id: "d1",
name: "FCC New Coder Survey 2018",
status: "published",
owners: [MOCK_USERS[0]],
lastModified: new Date(Date.now() - 1000 * 60 * 14).toISOString(),
isFavorite: true,
widgets: [],
},
{
id: "d2",
name: "Slack Dashboard",
status: "published",
owners: [MOCK_USERS[0]],
lastModified: new Date(Date.now() - 1000 * 60 * 14).toISOString(),
isFavorite: true,
widgets: [],
},
{
id: "d3",
name: "Sales Dashboard",
status: "published",
owners: [MOCK_USERS[0]],
lastModified: new Date(Date.now() - 1000 * 60 * 14).toISOString(),
isFavorite: true,
widgets: [],
},
];
// --- Store ---
export const useAnalysisStore = create<AnalysisState>((set) => ({
dashboards: MOCK_DASHBOARDS,
charts: MOCK_CHARTS,
activeDashboard: null,
layoutMode: "view",
isLoading: false,
setDashboards: (dashboards) => set({ dashboards }),
setCharts: (charts) => set({ charts }),
setActiveDashboard: (activeDashboard) => set({ activeDashboard }),
setLayoutMode: (layoutMode) => set({ layoutMode }),
addDashboard: (dashboard) => set((state) => ({ dashboards: [...state.dashboards, dashboard] })),
updateDashboard: (id, updates) =>
set((state) => ({
dashboards: state.dashboards.map((d) => (d.id === id ? { ...d, ...updates } : d)),
})),
}));

View File

@@ -8,6 +8,7 @@ import {
MessageCircle,
PanelLeftCloseIcon,
PanelLeftOpenIcon,
PieChart,
RocketIcon,
UserCircleIcon,
UserIcon,
@@ -105,6 +106,13 @@ export const MainNavigation = ({
isActive: pathname?.includes("/surveys"),
isHidden: false,
},
{
name: t("common.analysis"),
href: `/environments/${environment.id}/analysis`,
icon: PieChart,
isActive: pathname?.includes("/analysis"),
isHidden: false,
},
{
href: `/environments/${environment.id}/contacts`,
name: t("common.contacts"),
@@ -185,7 +193,7 @@ export const MainNavigation = ({
size="icon"
onClick={toggleSidebar}
className={cn(
"rounded-xl bg-slate-50 p-1 text-slate-600 transition-all hover:bg-slate-100 focus:ring-0 focus:ring-transparent focus:outline-none"
"rounded-xl bg-slate-50 p-1 text-slate-600 transition-all hover:bg-slate-100 focus:outline-none focus:ring-0 focus:ring-transparent"
)}>
{isCollapsed ? (
<PanelLeftOpenIcon strokeWidth={1.5} />

View File

@@ -11,6 +11,12 @@ const EnvLayout = async (props: {
children: React.ReactNode;
}) => {
const params = await props.params;
const { environmentId } = params;
if (environmentId === "undefined") {
return redirect("/");
}
const { children } = props;
// Check session first (required for userId)

View File

@@ -106,6 +106,7 @@ checksums:
common/allow: 3e39cc5940255e6bff0fea95c817dd43
common/allow_users_to_exit_by_clicking_outside_the_survey: 1c09db6e85214f1b1c3d4774c4c5cd56
common/an_unknown_error_occurred_while_deleting_table_items: 06be3fd128aeb51eed4fba9a079ecee2
common/analysis: 409bac6215382c47e59f5039cc4cdcdd
common/and: dc75b95c804b16dc617a5f16f7393bca
common/and_response_limit_of: 05be41a1d7e8dafa4aa012dcba77f5d4
common/anonymous: 77b5222e710cc1dae073dae32309f8ed

View File

@@ -133,6 +133,7 @@
"allow": "erlauben",
"allow_users_to_exit_by_clicking_outside_the_survey": "Erlaube Nutzern, die Umfrage zu verlassen, indem sie außerhalb klicken",
"an_unknown_error_occurred_while_deleting_table_items": "Beim Löschen von {type}s ist ein unbekannter Fehler aufgetreten",
"analysis": "Analyse",
"and": "und",
"and_response_limit_of": "und Antwortlimit von",
"anonymous": "Anonym",

View File

@@ -133,6 +133,7 @@
"allow": "Allow",
"allow_users_to_exit_by_clicking_outside_the_survey": "Allow users to exit by clicking outside the survey",
"an_unknown_error_occurred_while_deleting_table_items": "An unknown error occurred while deleting {type}s",
"analysis": "Analysis",
"and": "And",
"and_response_limit_of": "and response limit of",
"anonymous": "Anonymous",

View File

@@ -133,6 +133,7 @@
"allow": "Permitir",
"allow_users_to_exit_by_clicking_outside_the_survey": "Permitir a los usuarios salir haciendo clic fuera de la encuesta",
"an_unknown_error_occurred_while_deleting_table_items": "Se ha producido un error desconocido al eliminar {type}s",
"analysis": "Análisis",
"and": "Y",
"and_response_limit_of": "y límite de respuesta de",
"anonymous": "Anónimo",

View File

@@ -133,6 +133,7 @@
"allow": "Autoriser",
"allow_users_to_exit_by_clicking_outside_the_survey": "Permettre aux utilisateurs de quitter en cliquant hors de l'enquête",
"an_unknown_error_occurred_while_deleting_table_items": "Une erreur inconnue est survenue lors de la suppression des {type}s",
"analysis": "Analyse",
"and": "Et",
"and_response_limit_of": "et limite de réponse de",
"anonymous": "Anonyme",

View File

@@ -133,6 +133,7 @@
"allow": "許可",
"allow_users_to_exit_by_clicking_outside_the_survey": "フォームの外側をクリックしてユーザーが終了できるようにする",
"an_unknown_error_occurred_while_deleting_table_items": "{type}の削除中に不明なエラーが発生しました",
"analysis": "分析",
"and": "および",
"and_response_limit_of": "と回答数の上限",
"anonymous": "匿名",

View File

@@ -133,6 +133,7 @@
"allow": "Toestaan",
"allow_users_to_exit_by_clicking_outside_the_survey": "Laat gebruikers afsluiten door buiten de enquête te klikken",
"an_unknown_error_occurred_while_deleting_table_items": "Er is een onbekende fout opgetreden bij het verwijderen van {type}s",
"analysis": "Analyse",
"and": "En",
"and_response_limit_of": "en responslimiet van",
"anonymous": "Anoniem",

View File

@@ -133,6 +133,7 @@
"allow": "permitir",
"allow_users_to_exit_by_clicking_outside_the_survey": "Permitir que os usuários saiam clicando fora da pesquisa",
"an_unknown_error_occurred_while_deleting_table_items": "Ocorreu um erro desconhecido ao deletar {type}s",
"analysis": "Análise",
"and": "E",
"and_response_limit_of": "e limite de resposta de",
"anonymous": "Anônimo",

View File

@@ -133,6 +133,7 @@
"allow": "Permitir",
"allow_users_to_exit_by_clicking_outside_the_survey": "Permitir que os utilizadores saiam se clicarem 'sair do questionário'",
"an_unknown_error_occurred_while_deleting_table_items": "Ocorreu um erro desconhecido ao eliminar {type}s",
"analysis": "Análise",
"and": "E",
"and_response_limit_of": "e limite de resposta de",
"anonymous": "Anónimo",

View File

@@ -133,6 +133,7 @@
"allow": "Permite",
"allow_users_to_exit_by_clicking_outside_the_survey": "Permite utilizatorilor să iasă făcând clic în afara sondajului",
"an_unknown_error_occurred_while_deleting_table_items": "A apărut o eroare necunoscută la ștergerea elementelor de tipul {type}",
"analysis": "Analiză",
"and": "Și",
"and_response_limit_of": "și limită răspuns",
"anonymous": "Anonim",

View File

@@ -133,6 +133,7 @@
"allow": "Разрешить",
"allow_users_to_exit_by_clicking_outside_the_survey": "Разрешить пользователям выходить, кликнув вне опроса",
"an_unknown_error_occurred_while_deleting_table_items": "Произошла неизвестная ошибка при удалении {type}ов",
"analysis": "Анализ",
"and": "и",
"and_response_limit_of": "и лимит ответов",
"anonymous": "Аноним",

View File

@@ -133,6 +133,7 @@
"allow": "Tillåt",
"allow_users_to_exit_by_clicking_outside_the_survey": "Tillåt användare att avsluta genom att klicka utanför enkäten",
"an_unknown_error_occurred_while_deleting_table_items": "Ett okänt fel uppstod vid borttagning av {type}",
"analysis": "Analys",
"and": "Och",
"and_response_limit_of": "och svarsgräns på",
"anonymous": "Anonym",

View File

@@ -133,6 +133,7 @@
"allow": "允许",
"allow_users_to_exit_by_clicking_outside_the_survey": "允许 用户 通过 点击 调查 外部 退出",
"an_unknown_error_occurred_while_deleting_table_items": "删除 {type} 时发生未知错误",
"analysis": "分析",
"and": "和",
"and_response_limit_of": "和 响应限制",
"anonymous": "匿名",

View File

@@ -133,6 +133,7 @@
"allow": "允許",
"allow_users_to_exit_by_clicking_outside_the_survey": "允許使用者點擊問卷外退出",
"an_unknown_error_occurred_while_deleting_table_items": "刪除 '{'type'}' 時發生未知錯誤",
"analysis": "分析",
"and": "且",
"and_response_limit_of": "且回應上限為",
"anonymous": "匿名",

View File

@@ -0,0 +1,303 @@
"use client";
import * as React from "react";
import * as RechartsPrimitive from "recharts";
import { ResponsiveContainer, Tooltip } from "recharts";
import { cn } from "@/lib/cn";
// Format: { THEME_NAME: CSS_VARIABLE }
const THEMES = { light: "", dark: ".dark" } as const;
export type ChartConfig = {
[k in string]: {
label?: React.ReactNode;
icon?: React.ComponentType;
} & ({ color?: string; theme?: never } | { color?: never; theme: Record<keyof typeof THEMES, string> });
};
type ChartContextProps = {
config: ChartConfig;
};
const ChartContext = React.createContext<ChartContextProps | null>(null);
function useChart() {
const context = React.useContext(ChartContext);
if (!context) {
throw new Error("useChart must be used within a <ChartContainer />");
}
return context;
}
const ChartContainer = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
config: ChartConfig;
children: React.ComponentProps<typeof ResponsiveContainer>["children"];
}
>(({ id, className, children, config, ...props }, ref) => {
const uniqueId = React.useId();
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
return (
<ChartContext.Provider value={{ config }}>
<div
data-chart={chartId}
ref={ref}
className={cn(
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
className
)}
{...props}>
<ChartStyle id={chartId} config={config} />
<ResponsiveContainer>{children}</ResponsiveContainer>
</div>
</ChartContext.Provider>
);
});
ChartContainer.displayName = "ChartContainer";
const ChartTooltip = Tooltip;
const ChartTooltipContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<typeof Tooltip> &
React.ComponentProps<"div"> & {
hideLabel?: boolean;
hideIndicator?: boolean;
indicator?: "line" | "dot" | "dashed";
nameKey?: string;
labelKey?: string;
}
>(
(
{
active,
payload,
className,
indicator = "dot",
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
},
ref
) => {
const { config } = useChart();
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
return null;
}
const [item] = payload;
const key = `${labelKey || item.dataKey || item.name || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const value =
!labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label
: itemConfig?.label;
if (labelFormatter) {
return <div className={cn("font-medium", labelClassName)}>{labelFormatter(value, payload)}</div>;
}
if (!value) {
return null;
}
return <div className={cn("font-medium", labelClassName)}>{value}</div>;
}, [label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey]);
if (!active || !payload?.length) {
return null;
}
const nestLabel = payload.length === 1 && indicator !== "dot";
return (
<div
ref={ref}
className={cn(
"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
className
)}>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const indicatorColor = color || item.payload.fill || item.color;
return (
<div
key={item.dataKey}
className={cn(
"[&>svg]:text-muted-foreground flex w-full items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
indicator === "dot" && "items-center"
)}>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn("shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]", {
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent": indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
})}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center"
)}>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">{itemConfig?.label || item.name}</span>
</div>
{item.value && (
<span className="text-foreground font-mono font-medium tabular-nums">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
);
})}
</div>
</div>
);
}
);
ChartTooltipContent.displayName = "ChartTooltip";
const ChartLegend = RechartsPrimitive.Legend;
const ChartLegendContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean;
nameKey?: string;
}
>(({ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey }, ref) => {
const { config } = useChart();
if (!payload?.length) {
return null;
}
return (
<div
ref={ref}
className={cn(
"flex items-center justify-center gap-4",
verticalAlign === "top" ? "pb-3" : "pt-3",
className
)}>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
return (
<div
key={item.value}
className={cn("[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3")}>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
);
})}
</div>
);
});
ChartLegendContent.displayName = "ChartLegend";
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) {
if (typeof payload !== "object" || payload === null) {
return undefined;
}
const payloadPayload =
"payload" in payload && typeof payload.payload === "object" && payload.payload !== null
? payload.payload
: undefined;
let configLabelKey: string = key;
if (key in payload && typeof payload[key as keyof typeof payload] === "string") {
configLabelKey = payload[key as keyof typeof payload] as string;
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) {
configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string;
}
return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config];
}
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(([, config]) => config.theme || config.color);
if (!colorConfig.length) {
return null;
}
return (
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(
([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color;
return color ? ` --color-${key}: ${color};` : null;
})
.join("\n")}
}
`
)
.join("\n"),
}}
/>
);
};
export { ChartContainer, ChartTooltip, ChartTooltipContent, ChartLegend, ChartLegendContent, ChartStyle };

View File

@@ -124,6 +124,7 @@
"react-i18next": "15.7.3",
"react-turnstile": "1.1.4",
"react-use": "17.6.0",
"recharts": "3.7.0",
"redis": "4.7.0",
"sanitize-html": "2.17.0",
"server-only": "0.0.1",
@@ -137,7 +138,8 @@
"webpack": "5.99.8",
"xlsx": "file:vendor/xlsx-0.20.3.tgz",
"zod": "3.24.4",
"zod-openapi": "4.2.4"
"zod-openapi": "4.2.4",
"zustand": "5.0.10"
},
"devDependencies": {
"@formbricks/config-typescript": "workspace:*",

15
cube/cube.js Normal file
View File

@@ -0,0 +1,15 @@
module.exports = {
// Validate that the auth payload is present
checkSqlAuth: (req, auth) => {
// In dev mode with API secret, auth should be populated
if (!auth) {
// throw new Error('Authentication required');
}
},
// Rewrite queries based on security context (RLS)
queryRewrite: (query, { securityContext }) => {
console.log('Query Security Context:', securityContext);
return query;
},
};

View File

@@ -0,0 +1,44 @@
cube(`FeedbackRecords`, {
sql: `
SELECT
id,
created_at as collected_at,
(data->>'q18782jji4swm64miro9ei7e')::numeric as value_number
FROM "Response"
WHERE "surveyId" = 'clseedsurveykitchen00'
AND data->>'q18782jji4swm64miro9ei7e' IS NOT NULL
`,
measures: {
count: {
type: `count`
},
npsScore: {
type: `number`,
sql: `
CASE
WHEN COUNT(*) = 0 THEN 0
ELSE ROUND(
(
(COUNT(CASE WHEN ${CUBE}.value_number >= 9 THEN 1 END)::numeric -
COUNT(CASE WHEN ${CUBE}.value_number <= 6 THEN 1 END)::numeric)
/ COUNT(*)::numeric
) * 100,
2
)
END
`
}
},
dimensions: {
collectedAt: {
sql: `collected_at`,
type: `time`
},
value: {
sql: `value_number`,
type: `number`
}
}
});

View File

@@ -36,6 +36,24 @@ services:
volumes:
- minio-data:/data
cube:
image: cubejs/cube:latest
ports:
- 4000:4000
- 4001:4001
environment:
- CUBEJS_DB_HOST=postgres
- CUBEJS_DB_PORT=5432
- CUBEJS_DB_NAME=postgres
- CUBEJS_DB_USER=postgres
- CUBEJS_DB_PASS=postgres
- CUBEJS_DB_TYPE=postgres
- CUBEJS_API_SECRET=your-secret-key-change-in-production
- CUBEJS_DEV_MODE=true
volumes:
- ./cube/cube.js:/cube/conf/cube.js
- ./cube/schema:/cube/conf/model
volumes:
postgres:
driver: local

318
pnpm-lock.yaml generated
View File

@@ -437,6 +437,9 @@ importers:
react-use:
specifier: 17.6.0
version: 17.6.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
recharts:
specifier: 3.7.0
version: 3.7.0(@types/react@19.2.1)(react-dom@19.2.3(react@19.2.3))(react-is@17.0.2)(react@19.2.3)(redux@5.0.1)
redis:
specifier: 4.7.0
version: 4.7.0
@@ -479,9 +482,9 @@ importers:
zod-openapi:
specifier: 4.2.4
version: 4.2.4(zod@3.24.4)
zod-to-json-schema:
specifier: ^3.23.5
version: 3.25.1(zod@3.24.4)
zustand:
specifier: 5.0.10
version: 5.0.10(@types/react@19.2.1)(immer@11.1.3)(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3))
devDependencies:
'@formbricks/config-typescript':
specifier: workspace:*
@@ -4153,6 +4156,17 @@ packages:
peerDependencies:
'@redis/client': ^5.8.1
'@reduxjs/toolkit@2.11.2':
resolution: {integrity: sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==}
peerDependencies:
react: ^16.9.0 || ^17.0.0 || ^18 || ^19
react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0
peerDependenciesMeta:
react:
optional: true
react-redux:
optional: true
'@resvg/resvg-wasm@2.4.0':
resolution: {integrity: sha512-C7c51Nn4yTxXFKvgh2txJFNweaVcfUPQxwEUFw4aWsCmfiBDJsTSwviIF8EcwjQ6k8bPyMWCl1vw4BdxE569Cg==}
engines: {node: '>= 10'}
@@ -5190,6 +5204,33 @@ packages:
'@types/cors@2.8.19':
resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==}
'@types/d3-array@3.2.2':
resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==}
'@types/d3-color@3.1.3':
resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==}
'@types/d3-ease@3.0.2':
resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==}
'@types/d3-interpolate@3.0.4':
resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==}
'@types/d3-path@3.1.1':
resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==}
'@types/d3-scale@4.0.9':
resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==}
'@types/d3-shape@3.1.8':
resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==}
'@types/d3-time@3.0.4':
resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==}
'@types/d3-timer@3.0.2':
resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
'@types/deep-eql@4.0.2':
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
@@ -5315,6 +5356,9 @@ packages:
'@types/ungap__structured-clone@1.2.0':
resolution: {integrity: sha512-ZoaihZNLeZSxESbk9PUAPZOlSpcKx81I1+4emtULDVmBLkYutTcMlCj2K9VNlf9EWODxdO6gkAqEaLorXwZQVA==}
'@types/use-sync-external-store@0.0.6':
resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==}
'@types/uuid@9.0.8':
resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==}
@@ -6581,6 +6625,50 @@ packages:
csv-parse@5.6.0:
resolution: {integrity: sha512-l3nz3euub2QMg5ouu5U09Ew9Wf6/wQ8I++ch1loQ0ljmzhmfZYrH9fflS22i/PQEvsPvxCwxgz5q7UB8K1JO4Q==}
d3-array@3.2.4:
resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==}
engines: {node: '>=12'}
d3-color@3.1.0:
resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
engines: {node: '>=12'}
d3-ease@3.0.1:
resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
engines: {node: '>=12'}
d3-format@3.1.2:
resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==}
engines: {node: '>=12'}
d3-interpolate@3.0.1:
resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
engines: {node: '>=12'}
d3-path@3.1.0:
resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==}
engines: {node: '>=12'}
d3-scale@4.0.2:
resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==}
engines: {node: '>=12'}
d3-shape@3.2.0:
resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==}
engines: {node: '>=12'}
d3-time-format@4.1.0:
resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==}
engines: {node: '>=12'}
d3-time@3.1.0:
resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==}
engines: {node: '>=12'}
d3-timer@3.0.1:
resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
engines: {node: '>=12'}
damerau-levenshtein@1.0.8:
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
@@ -6657,6 +6745,9 @@ packages:
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
engines: {node: '>=0.10.0'}
decimal.js-light@2.5.1:
resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==}
decimal.js@10.6.0:
resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
@@ -6959,6 +7050,9 @@ packages:
resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==}
engines: {node: '>= 0.4'}
es-toolkit@1.44.0:
resolution: {integrity: sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==}
esbuild@0.23.1:
resolution: {integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==}
engines: {node: '>=18'}
@@ -7736,6 +7830,12 @@ packages:
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
engines: {node: '>= 4'}
immer@10.2.0:
resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==}
immer@11.1.3:
resolution: {integrity: sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q==}
import-fresh@3.3.1:
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
engines: {node: '>=6'}
@@ -7779,6 +7879,10 @@ packages:
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
engines: {node: '>= 0.4'}
internmap@2.0.3:
resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
engines: {node: '>=12'}
intl-messageformat@10.7.18:
resolution: {integrity: sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==}
@@ -9543,6 +9647,18 @@ packages:
react-is@17.0.2:
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
react-redux@9.2.0:
resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==}
peerDependencies:
'@types/react': ^18.2.25 || ^19
react: ^18.0 || ^19
redux: ^5.0.0
peerDependenciesMeta:
'@types/react':
optional: true
redux:
optional: true
react-refresh@0.14.2:
resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
engines: {node: '>=0.10.0'}
@@ -9645,6 +9761,14 @@ packages:
resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==}
engines: {node: '>= 4'}
recharts@3.7.0:
resolution: {integrity: sha512-l2VCsy3XXeraxIID9fx23eCb6iCBsxUQDnE8tWm6DFdszVAO7WVY/ChAD9wVit01y6B2PMupYiMmQwhgPHc9Ew==}
engines: {node: '>=18'}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-is: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
redent@3.0.0:
resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
engines: {node: '>=8'}
@@ -9656,6 +9780,14 @@ packages:
resolution: {integrity: sha512-RZjBKYX/qFF809x6vDcE5VA6L3MmiuT+BkbXbIyyyeU0lPD47V4z8qTzN+Z/kKFwpojwCItOfaItYuAjNs8pTQ==}
engines: {node: '>= 18'}
redux-thunk@3.1.0:
resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==}
peerDependencies:
redux: ^5.0.0
redux@5.0.1:
resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==}
reflect-metadata@0.2.2:
resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==}
@@ -9690,6 +9822,9 @@ packages:
require-main-filename@2.0.0:
resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
reselect@5.1.1:
resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==}
resize-observer-polyfill@1.5.1:
resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==}
@@ -10769,6 +10904,9 @@ packages:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'}
victory-vendor@37.3.6:
resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==}
vite-node@3.1.3:
resolution: {integrity: sha512-uHV4plJ2IxCl4u1up1FQRrqclylKAogbtBfOTwcuJ28xFi+89PZ57BRh+naIRvH70HPwxy5QHYzg1OrEaC7AbA==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
@@ -11176,14 +11314,27 @@ packages:
peerDependencies:
zod: ^3.21.4
zod-to-json-schema@3.25.1:
resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==}
peerDependencies:
zod: ^3.25 || ^4
zod@3.24.4:
resolution: {integrity: sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==}
zustand@5.0.10:
resolution: {integrity: sha512-U1AiltS1O9hSy3rul+Ub82ut2fqIAefiSuwECWt6jlMVUGejvf+5omLcRBSzqbRagSM3hQZbtzdeRc6QVScXTg==}
engines: {node: '>=12.20.0'}
peerDependencies:
'@types/react': '>=18.0.0'
immer: '>=9.0.6'
react: '>=18.0.0'
use-sync-external-store: '>=1.2.0'
peerDependenciesMeta:
'@types/react':
optional: true
immer:
optional: true
react:
optional: true
use-sync-external-store:
optional: true
snapshots:
'@acemir/cssom@0.9.29': {}
@@ -15534,6 +15685,18 @@ snapshots:
dependencies:
'@redis/client': 5.8.1
'@reduxjs/toolkit@2.11.2(react-redux@9.2.0(@types/react@19.2.1)(react@19.2.3)(redux@5.0.1))(react@19.2.3)':
dependencies:
'@standard-schema/spec': 1.1.0
'@standard-schema/utils': 0.3.0
immer: 11.1.3
redux: 5.0.1
redux-thunk: 3.1.0(redux@5.0.1)
reselect: 5.1.1
optionalDependencies:
react: 19.2.3
react-redux: 9.2.0(@types/react@19.2.1)(react@19.2.3)(redux@5.0.1)
'@resvg/resvg-wasm@2.4.0': {}
'@rolldown/pluginutils@1.0.0-beta.53': {}
@@ -16713,6 +16876,30 @@ snapshots:
dependencies:
'@types/node': 22.15.18
'@types/d3-array@3.2.2': {}
'@types/d3-color@3.1.3': {}
'@types/d3-ease@3.0.2': {}
'@types/d3-interpolate@3.0.4':
dependencies:
'@types/d3-color': 3.1.3
'@types/d3-path@3.1.1': {}
'@types/d3-scale@4.0.9':
dependencies:
'@types/d3-time': 3.0.4
'@types/d3-shape@3.1.8':
dependencies:
'@types/d3-path': 3.1.1
'@types/d3-time@3.0.4': {}
'@types/d3-timer@3.0.2': {}
'@types/deep-eql@4.0.2': {}
'@types/doctrine@0.0.9': {}
@@ -16852,6 +17039,8 @@ snapshots:
'@types/ungap__structured-clone@1.2.0': {}
'@types/use-sync-external-store@0.0.6': {}
'@types/uuid@9.0.8': {}
'@types/webidl-conversions@7.0.3': {}
@@ -18343,6 +18532,44 @@ snapshots:
csv-parse@5.6.0: {}
d3-array@3.2.4:
dependencies:
internmap: 2.0.3
d3-color@3.1.0: {}
d3-ease@3.0.1: {}
d3-format@3.1.2: {}
d3-interpolate@3.0.1:
dependencies:
d3-color: 3.1.0
d3-path@3.1.0: {}
d3-scale@4.0.2:
dependencies:
d3-array: 3.2.4
d3-format: 3.1.2
d3-interpolate: 3.0.1
d3-time: 3.1.0
d3-time-format: 4.1.0
d3-shape@3.2.0:
dependencies:
d3-path: 3.1.0
d3-time-format@4.1.0:
dependencies:
d3-time: 3.1.0
d3-time@3.1.0:
dependencies:
d3-array: 3.2.4
d3-timer@3.0.1: {}
damerau-levenshtein@1.0.8: {}
data-uri-to-buffer@4.0.1: {}
@@ -18400,6 +18627,8 @@ snapshots:
decamelize@1.2.0: {}
decimal.js-light@2.5.1: {}
decimal.js@10.6.0: {}
decompress-response@6.0.0:
@@ -18753,6 +18982,8 @@ snapshots:
is-date-object: 1.1.0
is-symbol: 1.1.1
es-toolkit@1.44.0: {}
esbuild@0.23.1:
optionalDependencies:
'@esbuild/aix-ppc64': 0.23.1
@@ -19767,6 +19998,10 @@ snapshots:
ignore@7.0.5: {}
immer@10.2.0: {}
immer@11.1.3: {}
import-fresh@3.3.1:
dependencies:
parent-module: 1.0.1
@@ -19809,6 +20044,8 @@ snapshots:
hasown: 2.0.2
side-channel: 1.1.0
internmap@2.0.3: {}
intl-messageformat@10.7.18:
dependencies:
'@formatjs/ecma402-abstract': 2.3.6
@@ -21590,6 +21827,15 @@ snapshots:
react-is@17.0.2: {}
react-redux@9.2.0(@types/react@19.2.1)(react@19.2.3)(redux@5.0.1):
dependencies:
'@types/use-sync-external-store': 0.0.6
react: 19.2.3
use-sync-external-store: 1.6.0(react@19.2.3)
optionalDependencies:
'@types/react': 19.2.1
redux: 5.0.1
react-refresh@0.14.2: {}
react-refresh@0.18.0: {}
@@ -21738,6 +21984,26 @@ snapshots:
tiny-invariant: 1.3.3
tslib: 2.8.1
recharts@3.7.0(@types/react@19.2.1)(react-dom@19.2.3(react@19.2.3))(react-is@17.0.2)(react@19.2.3)(redux@5.0.1):
dependencies:
'@reduxjs/toolkit': 2.11.2(react-redux@9.2.0(@types/react@19.2.1)(react@19.2.3)(redux@5.0.1))(react@19.2.3)
clsx: 2.1.1
decimal.js-light: 2.5.1
es-toolkit: 1.44.0
eventemitter3: 5.0.1
immer: 10.2.0
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
react-is: 17.0.2
react-redux: 9.2.0(@types/react@19.2.1)(react@19.2.3)(redux@5.0.1)
reselect: 5.1.1
tiny-invariant: 1.3.3
use-sync-external-store: 1.6.0(react@19.2.3)
victory-vendor: 37.3.6
transitivePeerDependencies:
- '@types/react'
- redux
redent@3.0.0:
dependencies:
indent-string: 4.0.0
@@ -21760,6 +22026,12 @@ snapshots:
'@redis/search': 5.8.1(@redis/client@5.8.1)
'@redis/time-series': 5.8.1(@redis/client@5.8.1)
redux-thunk@3.1.0(redux@5.0.1):
dependencies:
redux: 5.0.1
redux@5.0.1: {}
reflect-metadata@0.2.2: {}
reflect.getprototypeof@1.0.10:
@@ -21802,6 +22074,8 @@ snapshots:
require-main-filename@2.0.0: {}
reselect@5.1.1: {}
resize-observer-polyfill@1.5.1: {}
resolve-from@4.0.0: {}
@@ -23069,6 +23343,23 @@ snapshots:
vary@1.1.2: {}
victory-vendor@37.3.6:
dependencies:
'@types/d3-array': 3.2.2
'@types/d3-ease': 3.0.2
'@types/d3-interpolate': 3.0.4
'@types/d3-scale': 4.0.9
'@types/d3-shape': 3.1.8
'@types/d3-time': 3.0.4
'@types/d3-timer': 3.0.2
d3-array: 3.2.4
d3-ease: 3.0.1
d3-interpolate: 3.0.1
d3-scale: 4.0.2
d3-shape: 3.2.0
d3-time: 3.1.0
d3-timer: 3.0.1
vite-node@3.1.3(@types/node@22.15.18)(jiti@2.4.2)(lightningcss@1.30.2)(terser@5.39.1)(tsx@4.19.4)(yaml@2.8.2):
dependencies:
cac: 6.7.14
@@ -23605,8 +23896,11 @@ snapshots:
dependencies:
zod: 3.24.4
zod-to-json-schema@3.25.1(zod@3.24.4):
dependencies:
zod: 3.24.4
zod@3.24.4: {}
zustand@5.0.10(@types/react@19.2.1)(immer@11.1.3)(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3)):
optionalDependencies:
'@types/react': 19.2.1
immer: 11.1.3
react: 19.2.3
use-sync-external-store: 1.6.0(react@19.2.3)