[ENG-1794] Overview Rework (#2555)

* StorageBar implementation

* filekindstats reworked into interactive node graph

* ui changes to graph

* light/dark mode changes + ui edits

* missing dependancy in package.json and minor visual improvements

* fixed collision physics

* d3 force package

* fixed nodes going off screen

* removed onNodeDragEnd

* fix central node and add particle effect

* fixed central node and typescript errors

* bar graph/storage bar ui improvements

* changed icons

* totals

* made ui changes

* ui changes

* minor requested ui changes

* fixed spacing for ui

* Remove extraneous newline in core/src/api/libraries.rs

* Fix minor suggestions from code review

* Fix typecheck

* Auto format

* refactor file kind stat card with css grid

* ensure unidentified files never negative

* FIxing some stats

* Counting directories

* fixed error with height of bars

* updated storage bar

* total files update

---------

Co-authored-by: ameer2468 <33054370+ameer2468@users.noreply.github.com>
Co-authored-by: James Pine <ijamespine@me.com>
Co-authored-by: Lynx <141365347+iLynxcat@users.noreply.github.com>
Co-authored-by: Vítor Vasconcellos <vasconcellos.dev@gmail.com>
Co-authored-by: Lynx <ephemeral.lynx@protonmail.com>
Co-authored-by: Ericson Soares <ericson.ds999@gmail.com>
This commit is contained in:
Matthew Yung 2024-06-24 13:00:51 -07:00 committed by GitHub
parent f0bc308653
commit f7b0e3bd06
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 1217 additions and 352 deletions

View file

@ -1,8 +1,8 @@
import { useNavigation } from '@react-navigation/native';
import { useLibraryQuery } from '@sd/client';
import { DotsThree } from 'phosphor-react-native';
import React from 'react';
import { Text, View } from 'react-native';
import { uint32ArrayToBigInt, useLibraryQuery } from '@sd/client';
import { tw } from '~/lib/tailwind';
import { OverviewStackScreenProps } from '~/navigation/tabs/OverviewStack';
@ -24,12 +24,17 @@ export default function CategoriesScreen() {
style={tw`h-8 w-8 rounded-full`}
variant="gray"
>
<DotsThree weight='bold' size={18} color={'white'} />
<DotsThree weight="bold" size={18} color={'white'} />
</Button>
</View>
<View style={tw`flex-row flex-wrap gap-2`}>
{kinds.data?.statistics
?.sort((a, b) => b.count - a.count)
?.sort((a, b) => {
const aCount = uint32ArrayToBigInt(a.count);
const bCount = uint32ArrayToBigInt(b.count);
if (aCount === bCount) return 0;
return aCount > bCount ? -1 : 1;
})
.filter((i) => i.kind !== 0)
.slice(0, 6)
.map((item) => {
@ -49,7 +54,7 @@ export default function CategoriesScreen() {
kind={kind}
name={name}
icon={icon}
items={count}
items={uint32ArrayToBigInt(count)}
/>
);
})}

View file

@ -1,16 +1,16 @@
import { formatNumber } from '@sd/client';
import { useNavigation } from '@react-navigation/native';
import { Pressable, Text, View } from 'react-native';
import { ClassInput } from 'twrnc';
import { formatNumber } from '@sd/client';
import { tw, twStyle } from '~/lib/tailwind';
import { useNavigation } from '@react-navigation/native';
import { useSearchStore } from '~/stores/searchStore';
import { Icon, IconName } from '../icons/Icon';
interface CategoryItemProps {
kind: number;
name: string;
items: number;
items: bigint | number;
icon: IconName;
selected?: boolean;
onClick?: () => void;
@ -29,14 +29,18 @@ const CategoryItem = ({ name, icon, items, style, kind }: CategoryItemProps) =>
style
)}
onPress={() => {
searchStore.updateFilters('kind', {
name,
icon: icon + '20' as IconName,
id: kind
}, true);
searchStore.updateFilters(
'kind',
{
name,
icon: (icon + '20') as IconName,
id: kind
},
true
);
navigation.navigate('SearchStack', {
screen: 'Search',
})
screen: 'Search'
});
}}
>
<Icon name={icon} size={56} />

View file

@ -1,7 +1,7 @@
import { useLibraryQuery } from '@sd/client';
import { useMemo } from 'react';
import { FlatList, View } from 'react-native';
import { useDebounce } from 'use-debounce';
import { uint32ArrayToBigInt, useLibraryQuery } from '@sd/client';
import { IconName } from '~/components/icons/Icon';
import ScreenContainer from '~/components/layout/ScreenContainer';
import CategoryItem from '~/components/overview/CategoryItem';
@ -22,7 +22,14 @@ const CategoriesScreen = () => {
return (
<ScreenContainer scrollview={false} style={tw`relative px-6 py-0`}>
<FlatList
data={filteredKinds?.sort((a, b) => b.count - a.count).filter((i) => i.kind !== 0)}
data={filteredKinds
?.sort((a, b) => {
const aCount = uint32ArrayToBigInt(a.count);
const bCount = uint32ArrayToBigInt(b.count);
if (aCount === bCount) return 0;
return aCount > bCount ? -1 : 1;
})
.filter((i) => i.kind !== 0)}
numColumns={3}
contentContainerStyle={tw`py-6`}
keyExtractor={(item) => item.name}
@ -46,7 +53,7 @@ const CategoriesScreen = () => {
kind={kind}
name={name}
icon={icon}
items={count}
items={uint32ArrayToBigInt(count)}
/>
);
}}

View file

@ -6,17 +6,15 @@ use crate::{
Node,
};
use futures::StreamExt;
use prisma_client_rust::raw;
use sd_core_heavy_lifting::JobId;
use sd_file_ext::kind::ObjectKind;
use sd_p2p::RemoteIdentity;
use sd_prisma::prisma::{indexer_rule, object, statistics};
use tokio_stream::wrappers::IntervalStream;
use tracing::{info, warn};
use sd_prisma::prisma::{file_path, indexer_rule, object, statistics};
use sd_utils::{db::size_in_bytes_from_db, u64_to_frontend};
use std::{
collections::{hash_map::Entry, HashMap},
collections::{hash_map::Entry, BTreeMap, HashMap},
convert::identity,
pin::pin,
sync::Arc,
@ -25,8 +23,13 @@ use std::{
use async_channel as chan;
use directories::UserDirs;
use futures_concurrency::{future::Join, stream::Merge};
use futures::StreamExt;
use futures_concurrency::{
future::{Join, TryJoin},
stream::Merge,
};
use once_cell::sync::Lazy;
use prisma_client_rust::{and, or, raw};
use rspc::{alpha::AlphaRouter, ErrorCode};
use serde::{Deserialize, Serialize};
use specta::Type;
@ -36,7 +39,8 @@ use tokio::{
sync::Mutex,
time::{interval, Instant},
};
use tracing::{debug, error};
use tokio_stream::wrappers::IntervalStream;
use tracing::{debug, error, info, warn};
use uuid::Uuid;
use super::{utils::library, Ctx, R};
@ -122,36 +126,125 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
})
})
.procedure("kindStatistics", {
#[derive(Serialize, Deserialize, Type, Default)]
#[derive(Debug, Serialize, Deserialize, Type, Default)]
pub struct KindStatistic {
kind: i32,
name: String,
count: i32,
total_bytes: String,
count: (u32, u32),
total_bytes: (u32, u32),
}
#[derive(Serialize, Deserialize, Type, Default)]
#[derive(Debug, Serialize, Deserialize, Type, Default)]
pub struct KindStatistics {
statistics: Vec<KindStatistic>,
total_identified_files: i32,
total_unidentified_files: i32,
}
#[derive(Default)]
struct CountAndSize {
count: u64,
size: u64,
}
R.with2(library()).query(|(_, library), _: ()| async move {
let mut statistics: Vec<KindStatistic> = vec![];
for kind in ObjectKind::iter() {
let count = library
let (total_unidentified_files, total_identified_files) = (
library
.db
.file_path()
.count(vec![
file_path::is_dir::equals(Some(false)),
file_path::cas_id::equals(None),
file_path::object_id::equals(None),
])
.exec(),
library
.db
.file_path()
.count(vec![or!(
file_path::is_dir::equals(Some(true)),
and!(
file_path::cas_id::not(None),
file_path::object_id::not(None),
),
)])
.exec(),
)
.try_join()
.await?;
let mut statistics_by_kind = BTreeMap::from_iter(
ObjectKind::iter().map(|kind| (kind as i32, CountAndSize::default())),
);
let mut last_object_id = 0;
loop {
let objects = library
.db
.object()
.count(vec![object::kind::equals(Some(kind as i32))])
.find_many(vec![object::id::gt(last_object_id)])
.take(1000)
.select(
object::select!({ id kind file_paths: select { size_in_bytes_bytes } }),
)
.exec()
.await?;
statistics.push(KindStatistic {
kind: kind as i32,
name: kind.to_string(),
count: count as i32,
total_bytes: "0".to_string(),
});
if let Some(last) = objects.last() {
last_object_id = last.id;
} else {
break; // No more objects
}
for object in objects {
if let Some(kind) = object.kind {
statistics_by_kind.entry(kind).and_modify(|count_and_size| {
count_and_size.count += object.file_paths.len() as u64;
count_and_size.size += object
.file_paths
.into_iter()
.map(|file_path| {
file_path
.size_in_bytes_bytes
.map(|size| size_in_bytes_from_db(&size))
.unwrap_or(0)
})
.sum::<u64>();
});
}
}
}
Ok(KindStatistics { statistics })
// This is a workaround for the fact that we don't assign object to directories yet
if let Some(count_and_size) =
statistics_by_kind.get_mut(&(ObjectKind::Folder as i32))
{
count_and_size.count = library
.db
.file_path()
.count(vec![file_path::is_dir::equals(Some(true))])
.exec()
.await? as u64;
}
Ok(KindStatistics {
statistics: ObjectKind::iter()
.map(|kind| {
let int_kind = kind as i32;
let CountAndSize { count, size } =
statistics_by_kind.get(&int_kind).expect("can't fail");
KindStatistic {
kind: int_kind,
name: kind.to_string(),
count: u64_to_frontend(*count),
total_bytes: u64_to_frontend(*size),
}
})
.collect(),
total_identified_files: total_identified_files as i32,
total_unidentified_files: total_unidentified_files as i32,
})
})
})
.procedure("create", {

View file

@ -1,11 +1,12 @@
use crate::{api::utils::get_size, library::Library, volume::get_volumes, Node};
use sd_prisma::prisma::statistics;
use sd_utils::db::size_in_bytes_from_db;
use chrono::Utc;
use tracing::{error, info};
use super::LibraryManagerError;
use tracing::{error, info};
pub async fn update_library_statistics(
node: &Node,
@ -45,7 +46,7 @@ pub async fn update_library_statistics(
.map(|location| {
location
.size_in_bytes
.map(|bytes| bytes.iter().fold(0, |acc, &x| acc * 256 + x as u64))
.map(|size| size_in_bytes_from_db(&size))
.unwrap_or(0)
})
.sum::<u64>();

View file

@ -72,3 +72,263 @@ export const Component = () => {
</ExplorerContextProvider>
);
};
// NOTE -> this is code for the node graph. The plan is to implement this in network (moved from overview page). Jamie asked me to save the code somewhere
// so placing it here for now!
// import { getIcon } from '@sd/assets/util';
// import { useLibraryQuery } from '@sd/client';
// import React, { useEffect, useRef, useState, useCallback } from 'react';
// import { useIsDark } from '~/hooks';
// import ForceGraph2D from 'react-force-graph-2d';
// import { useNavigate } from 'react-router';
// import * as d3 from 'd3-force';
// //million-ignore
// const canvasWidth = 700
// const canvasHeight = 600;
// interface KindStatistic {
// kind: number;
// name: string;
// count: number;
// total_bytes: string;
// }
// interface Node {
// id: string | number;
// name: string;
// val: number;
// fx?: number;
// fy?: number;
// x?: number;
// y?: number;
// }
// interface Link {
// source: string | number;
// target: string | number;
// }
// interface GraphData {
// nodes: Node[];
// links: Link[];
// }
// const FileKindStatistics: React.FC = () => {
// const isDark = useIsDark();
// const navigate = useNavigate();
// const { data } = useLibraryQuery(['library.kindStatistics']);
// const [graphData, setGraphData] = useState<GraphData>({ nodes: [], links: [] });
// const iconsRef = useRef<{ [key: string]: HTMLImageElement }>({});
// const containerRef = useRef<HTMLDivElement>(null);
// const fgRef = useRef<any>(null);
// useEffect(() => {
// if (data) {
// const statistics: KindStatistic[] = data.statistics
// .filter((item: KindStatistic) => item.count != 0)
// .sort((a: KindStatistic, b: KindStatistic) => b.count - a.count)
// // TODO: eventually allow users to select and save which file kinds are shown
// .slice(0, 18); // Get the top 18 highest file kinds
// const totalFilesCount = statistics.reduce((sum, item) => sum + item.count, 0);
// const nodes = [
// { id: 'center', name: 'Total Files', val: totalFilesCount },
// ...statistics.map(item => ({
// id: item.kind,
// name: item.name,
// val: item.count,
// }))
// ];
// const links = statistics.map(item => ({
// source: 'center',
// target: item.kind,
// }));
// setGraphData({ nodes, links });
// // Preload icons, this is for rendering purposes
// statistics.forEach(item => {
// const iconName = item.name;
// if (!iconsRef.current[iconName]) {
// const img = new Image();
// img.src = getIcon(iconName, isDark);
// iconsRef.current[iconName] = img;
// }
// });
// // d3 stuff for changing physics of the nodes
// fgRef.current.d3Force('link').distance(110); // Adjust link distance to make links shorter
// fgRef.current.d3Force('charge').strength(-50); // how hard the nodes repel
// fgRef.current.d3Force('center').strength(10); // Adjust center strength for stability
// fgRef.current.d3Force('collision', d3.forceCollide().radius(25)); // Add collision force with radius. Should be a little larger than radius of nodes.
// fgRef.current.d3Force('y', d3.forceY(canvasHeight / 5).strength((1.2))); // strong force to ensure nodes don't spill out of canvas
// }
// }, [data, isDark]);
// const paintNode = useCallback((node: any, ctx: CanvasRenderingContext2D, globalScale: number) => {
// const fontSize = 0.6 / globalScale;
// ctx.font = `400 ${fontSize}em ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"`;
// ctx.textAlign = 'center';
// ctx.textBaseline = 'middle';
// const darkColor = 'rgb(34, 34, 45)';
// const lightColor = 'rgb(252, 252, 254)';
// const x = isFinite(node.x) ? node.x : 0;
// const y = isFinite(node.y) ? node.y : 0;
// if (node.name === 'Total Files') {
// const radius = 25;
// const borderWidth = 0.5;
// // Create linear gradient for light mode
// const lightGradient = ctx.createLinearGradient(x - radius, y - radius, x + radius, y + radius);
// lightGradient.addColorStop(0, 'rgb(117, 177, 249)');
// lightGradient.addColorStop(1, 'rgb(0, 76, 153)');
// // Create linear gradient for dark mode
// const darkGradient = ctx.createLinearGradient(x - radius, y - radius, x + radius, y + radius);
// darkGradient.addColorStop(0, 'rgb(255, 13, 202)');
// darkGradient.addColorStop(1, 'rgb(128, 0, 255)');
// // Draw filled circle with gradient border
// ctx.beginPath();
// ctx.arc(node.x, node.y, radius, 0, 2 * Math.PI, false);
// ctx.fillStyle = isDark ? darkGradient : lightGradient;
// ctx.fill();
// // Draw inner circle to create the border effect
// ctx.beginPath();
// ctx.arc(node.x, node.y, radius - borderWidth, 0, 2 * Math.PI, false);
// ctx.fillStyle = isDark ? darkColor : lightColor;
// ctx.fill();
// // Add inner shadow
// const shadowGradient = ctx.createRadialGradient(x, y, radius * 0.5, x, y, radius);
// shadowGradient.addColorStop(0, 'rgba(0, 0, 0, 0)');
// shadowGradient.addColorStop(1, isDark ? 'rgba(255, 93, 234, 0.1' : 'rgba(66, 97, 255, 0.05)');
// ctx.globalCompositeOperation = 'source-atop';
// ctx.beginPath();
// ctx.arc(node.x, node.y, radius, 0, 2 * Math.PI, false);
// ctx.fillStyle = shadowGradient;
// ctx.fill();
// // Draw text
// ctx.fillStyle = isDark ? 'rgba(255, 255, 255, 1)' : 'rgba(10, 10, 10, 0.8)';
// ctx.font = `bold ${fontSize * 2}em ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"`;
// ctx.fillText(node.val, node.x, node.y - fontSize * 9);
// ctx.fillStyle = isDark ? 'rgba(255, 255, 255, 0.3)' : 'rgba(10, 10, 10, 0.8)';
// ctx.font = `400 ${fontSize * 1.1}em ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"`;
// ctx.fillText(node.name, node.x, node.y + fontSize * 25);
// } else {
// const iconName = node.name;
// const iconImg = iconsRef.current[iconName];
// const iconSize = 25 / globalScale;
// const textYPos = node.y + iconSize;
// // Draw shadow
// ctx.shadowColor = isDark ? 'rgb(44, 45, 58)' : 'rgba(0, 0, 0, 0.1)';
// ctx.shadowBlur = 0.5;
// ctx.shadowOffsetX = -0.5;
// ctx.shadowOffsetY = -2;
// // Draw node circle
// const radius = 18;
// ctx.beginPath();
// ctx.arc(node.x, node.y, radius, 0, 2 * Math.PI, false);
// ctx.fillStyle = isDark ? darkColor : lightColor;
// ctx.fill();
// ctx.shadowColor = 'transparent';
// if (iconImg) {
// ctx.drawImage(iconImg, node.x - iconSize / 2, node.y - iconSize, iconSize, iconSize);
// }
// ctx.fillStyle = isDark ? 'white' : 'black';
// // Truncate node name if it is too long
// let truncatedName = node.name;
// if (node.name.length > 10) {
// truncatedName = node.name.slice(0, 6) + "...";
// }
// ctx.fillText(truncatedName, node.x, textYPos - 9);
// ctx.fillStyle = isDark ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.5)';
// ctx.fillText(node.val, node.x, textYPos - 2);
// }
// }, [isDark]);
// const handleNodeClick = useCallback((node: any) => {
// if (node.id !== 'center') {
// const path = {
// pathname: '../search',
// search: new URLSearchParams({
// filters: JSON.stringify([{ object: { kind: { in: [node.id] } } }])
// }).toString()
// };
// navigate(path);
// }
// }, [navigate]);
// const handleEngineTick = () => {
// const centerNode = graphData.nodes.find((node: any) => node.id === 'center');
// if (centerNode) {
// centerNode.fx = 0;
// centerNode.fy = 0;
// }
// }
// useEffect(() => {
// if (fgRef.current) {
// fgRef.current.d3Force('center', d3.forceCenter());
// }
// }, []);
// const paintPointerArea = useCallback((node: any, color: string, ctx: CanvasRenderingContext2D, globalScale: number) => {
// const size = 30 / globalScale;
// ctx.fillStyle = color;
// ctx.beginPath();
// ctx.arc(node.x, node.y, size, 0, 2 * Math.PI, false);
// ctx.fill();
// }, []);
// return (
// <div className="relative bottom-48 h-[200px] w-full" ref={containerRef}>
// {data ? (
// <ForceGraph2D
// ref={fgRef}
// graphData={graphData}
// nodeId="id"
// linkSource="source"
// linkTarget="target"
// width={canvasWidth}
// height={canvasHeight}
// backgroundColor="transparent"
// nodeCanvasObject={paintNode}
// linkWidth={0.5}
// nodeLabel=""
// dagMode="radialout"
// linkColor={() => isDark ? '#2C2D3A' : 'rgba(0, 0, 0, 0.2)'}
// onNodeClick={handleNodeClick}
// enableZoomInteraction={false}
// enablePanInteraction={false}
// dagLevelDistance={100}
// warmupTicks={500}
// d3VelocityDecay={0.75}
// onEngineTick={handleEngineTick}
// nodePointerAreaPaint={paintPointerArea}
// />
// ) : (
// <div>Loading...</div>
// )}
// </div>
// );
// };
// export default React.memo(FileKindStatistics);

View file

@ -1,99 +1,252 @@
import { Info } from '@phosphor-icons/react';
import { getIcon } from '@sd/assets/util';
import clsx from 'clsx';
import { motion } from 'framer-motion';
import { useRef } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { formatNumber, SearchFilterArgs, useLibraryQuery } from '@sd/client';
import { Icon } from '~/components';
import { useLocale } from '~/hooks';
import React, { MouseEventHandler, useCallback, useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router';
import { KindStatistic, uint32ArrayToBigInt, useLibraryQuery } from '@sd/client';
import { Card, Tooltip } from '@sd/ui';
import { useIsDark, useLocale } from '~/hooks';
import { translateKindName } from '../Explorer/util';
const INFO_ICON_CLASSLIST = 'inline size-3 text-ink-faint opacity-0';
const TOTAL_FILES_CLASSLIST =
'flex items-center justify-between whitespace-nowrap text-sm font-medium text-ink-dull mt-2 px-1';
const UNIDENTIFIED_FILES_CLASSLIST = 'relative flex items-center text-xs text-ink-faint';
export default () => {
const ref = useRef<HTMLDivElement>(null);
const kinds = useLibraryQuery(['library.kindStatistics']);
return (
<>
{/* This is awful, will replace icons accordingly and memo etc */}
{kinds.data?.statistics
?.sort((a, b) => b.count - a.count)
.filter((i) => i.kind !== 0)
.map(({ kind, name, count }) => {
let icon = name;
switch (name) {
case 'Code':
icon = 'Terminal';
break;
case 'Unknown':
icon = 'Undefined';
break;
}
return (
<motion.div
viewport={{
root: ref,
// WARNING: Edge breaks if the values are not postfixed with px or %
margin: '0% -120px 0% 0%'
}}
className={clsx('min-w-fit')}
key={kind}
>
<KindItem
kind={kind}
name={translateKindName(name)}
icon={icon}
items={count}
onClick={() => {}}
/>
</motion.div>
);
})}
</>
);
const mapFractionalValue = (numerator: bigint, denominator: bigint, maxValue: bigint): string => {
const result = ((numerator * maxValue) / denominator).toString();
return result;
};
interface KindItemProps {
kind: number;
name: string;
items: number;
icon: string;
selected?: boolean;
onClick?: () => void;
disabled?: boolean;
const formatNumberWithCommas = (number: number | bigint) => number.toLocaleString();
const interpolateHexColor = (color1: string, color2: string, factor: number): string => {
const hex = (color: string) => parseInt(color.slice(1), 16);
const r = Math.round((1 - factor) * (hex(color1) >> 16) + factor * (hex(color2) >> 16));
const g = Math.round(
(1 - factor) * ((hex(color1) >> 8) & 0x00ff) + factor * ((hex(color2) >> 8) & 0x00ff)
);
const b = Math.round(
(1 - factor) * (hex(color1) & 0x0000ff) + factor * (hex(color2) & 0x0000ff)
);
return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase()}`;
};
interface FileKind {
kind: string;
count: bigint;
id: number;
}
const KindItem = ({ kind, name, icon, items, selected, onClick, disabled }: KindItemProps) => {
interface FileKindStatsProps {}
const FileKindStats: React.FC<FileKindStatsProps> = () => {
const isDark = useIsDark();
const navigate = useNavigate();
const { t } = useLocale();
return (
<Link
to={{
const { data } = useLibraryQuery(['library.kindStatistics']);
const [fileKinds, setFileKinds] = useState<FileKind[]>([]);
const [cardWidth, setCardWidth] = useState<number>(0);
const containerRef = useRef<HTMLDivElement>(null);
const iconsRef = useRef<{ [key: string]: HTMLImageElement }>({});
const BAR_MAX_HEIGHT = 115n;
const BAR_COLOR_START = '#3A7ECC';
const BAR_COLOR_END = '#004C99';
const formatCount = (count: number | bigint): string => {
const bigIntCount = typeof count === 'number' ? BigInt(count) : count;
return bigIntCount >= 1000n ? `${bigIntCount / 1000n}K` : count.toString();
};
const handleResize = useCallback(() => {
if (containerRef.current) {
const factor = window.innerWidth > 1500 ? 0.35 : 0.4;
setCardWidth(window.innerWidth * factor);
}
}, []);
useEffect(() => {
window.addEventListener('resize', handleResize);
handleResize();
const containerElement = containerRef.current;
if (containerElement) {
const observer = new MutationObserver(handleResize);
observer.observe(containerElement, {
attributes: true,
childList: true,
subtree: true,
attributeFilter: ['style']
});
return () => {
observer.disconnect();
};
}
return () => {
window.removeEventListener('resize', handleResize);
};
}, [handleResize]);
useEffect(() => {
if (data) {
const statistics: KindStatistic[] = data.statistics
.filter(
(item: { kind: number; count: any }) => uint32ArrayToBigInt(item.count) !== 0n
)
.sort((a: { count: any }, b: { count: any }) => {
const aCount = uint32ArrayToBigInt(a.count);
const bCount = uint32ArrayToBigInt(b.count);
if (aCount === bCount) return 0;
return aCount > bCount ? -1 : 1;
});
setFileKinds(
statistics.map((item) => ({
kind: item.name,
count: uint32ArrayToBigInt(item.count),
id: item.kind
}))
);
statistics.forEach((item) => {
const iconName = item.name;
if (!iconsRef.current[iconName]) {
const img = new Image();
img.src = getIcon(iconName + '20', isDark);
iconsRef.current[iconName] = img;
}
});
}
}, [data, isDark]);
const sortedFileKinds = [...fileKinds].sort((a, b) => {
if (a.count === b.count) return 0;
return a.count > b.count ? -1 : 1;
});
const maxFileCount = sortedFileKinds && sortedFileKinds[0] ? sortedFileKinds[0].count : 0n;
const barGap = 12;
const barCount = sortedFileKinds.length;
const totalGapWidth = barGap * (barCount - 5);
const makeBarClickHandler =
(fileKind: FileKind): MouseEventHandler<HTMLDivElement> | undefined =>
() => {
const path = {
pathname: '../search',
search: new URLSearchParams({
filters: JSON.stringify([
{ object: { kind: { in: [kind] } } }
] as SearchFilterArgs[])
filters: JSON.stringify([{ object: { kind: { in: [fileKind.id] } } }])
}).toString()
}}
>
<div
onClick={onClick}
className={clsx(
'flex shrink-0 items-center rounded-lg py-1 text-sm outline-none focus:bg-app-selectedItem/50',
selected && 'bg-app-selectedItem',
disabled && 'cursor-not-allowed opacity-30'
)}
};
navigate(path);
};
return (
<div className="flex justify-center">
<Card
ref={containerRef}
className="max-w-1/2 group mx-1 flex h-[220px] w-full min-w-[400px] shrink-0 flex-col gap-2 bg-app-box/50"
>
<Icon name={icon as any} className="mr-3 size-12" />
<div className="pr-5">
<h2 className="text-sm font-medium">{name}</h2>
{items !== undefined && (
<p className="text-xs text-ink-faint">
{t('item_with_count', { count: items })}
</p>
)}
<div className={TOTAL_FILES_CLASSLIST}>
<Tooltip className="flex items-center" label={t('bar_graph_info')}>
<div className="flex items-center gap-2">
<span
className={clsx(
'text-xl font-black',
isDark ? 'text-white' : 'text-black'
)}
>
{data?.total_identified_files
? formatNumberWithCommas(data.total_identified_files)
: '0'}{' '}
</span>
<div className="flex items-center">
{t('total_files')}
<Info
weight="fill"
className={`ml-1 ${INFO_ICON_CLASSLIST} opacity-0 transition-opacity duration-300 group-hover:opacity-70`}
/>
</div>
</div>
</Tooltip>
<div className={UNIDENTIFIED_FILES_CLASSLIST}>
<Tooltip label={t('unidentified_files_info')}>
<span>
{data?.total_unidentified_files
? formatNumberWithCommas(data.total_unidentified_files)
: '0'}{' '}
unidentified files
</span>
</Tooltip>
</div>
</div>
</div>
</Link>
<div className="relative mx-2.5 grid grow grid-cols-[repeat(auto-fit,_minmax(0,_1fr))] grid-rows-[136px_12px] items-end justify-items-center gap-x-1.5 gap-y-1 self-stretch">
{sortedFileKinds.map((fileKind, index) => {
const iconImage = iconsRef.current[fileKind.kind];
const barColor = interpolateHexColor(
BAR_COLOR_START,
BAR_COLOR_END,
index / (barCount - 1)
);
const barHeight =
mapFractionalValue(fileKind.count, maxFileCount, BAR_MAX_HEIGHT) + 'px';
return (
<>
<Tooltip
asChild
key={fileKind.kind}
label={
formatNumberWithCommas(fileKind.count) +
' ' +
fileKind.kind +
's'
}
position="left"
>
<div
className="relative flex w-full min-w-8 max-w-10 grow cursor-pointer flex-col items-center"
onDoubleClick={makeBarClickHandler(fileKind)}
>
{iconImage && (
<img
src={iconImage.src}
alt={fileKind.kind}
className="relative mb-1 size-4 duration-500"
/>
)}
<motion.div
className="flex w-full flex-col items-center rounded transition-all duration-500"
initial={{ height: 0 }}
animate={{ height: barHeight }}
transition={{ duration: 0.4, ease: [0.42, 0, 0.58, 1] }}
style={{
height: barHeight,
minHeight: '2px',
backgroundColor: barColor
}}
></motion.div>
</div>
</Tooltip>
<div
className="sm col-span-1 row-start-2 row-end-auto text-center text-[10px] font-medium text-ink-faint"
style={{
borderRadius: '3px'
}}
>
{formatCount(fileKind.count)}
</div>
</>
);
})}
</div>
</Card>
</div>
);
};
export default FileKindStats;

View file

@ -2,8 +2,10 @@ import { Info } from '@phosphor-icons/react';
import clsx from 'clsx';
import { useEffect, useState } from 'react';
import { humanizeSize, Statistics, useLibraryContext, useLibraryQuery } from '@sd/client';
import { Tooltip } from '@sd/ui';
import { useCounter, useLocale } from '~/hooks';
import { Card, Tooltip } from '@sd/ui';
import { useCounter, useIsDark, useLocale } from '~/hooks';
import StorageBar from './StorageBar';
interface StatItemProps {
title: string;
@ -12,15 +14,18 @@ interface StatItemProps {
info?: string;
}
interface Section {
name: string;
value: number;
color: string;
tooltip: string;
}
let mounted = false;
const StatItem = (props: StatItemProps) => {
const { title, bytes, isLoading } = props;
// This is important to the counter working.
// On first render of the counter this will potentially be `false` which means the counter should the count up.
// but in a `useEffect` `mounted` will be set to `true` so that subsequent renders of the counter will not run the count up.
// The acts as a cache of the value of `mounted` on the first render of this `StateItem`.
const [isMounted] = useState(mounted);
const size = humanizeSize(bytes);
@ -36,7 +41,7 @@ const StatItem = (props: StatItemProps) => {
return (
<div
className={clsx(
'group/stat flex w-32 shrink-0 flex-col duration-75',
'group/stat flex w-36 shrink-0 flex-col duration-75',
!bytes && 'hidden'
)}
>
@ -46,7 +51,7 @@ const StatItem = (props: StatItemProps) => {
<Tooltip label={props.info}>
<Info
weight="fill"
className="-mt-0.5 ml-1 inline size-3 text-ink-faint opacity-0 transition-opacity group-hover/stat:opacity-70"
className="-mt-0.5 ml-1 inline size-3 text-ink-faint opacity-0 transition-opacity duration-300 group-hover/stat:opacity-70"
/>
</Tooltip>
)}
@ -69,42 +74,101 @@ const StatItem = (props: StatItemProps) => {
};
const LibraryStats = () => {
const isDark = useIsDark();
const { library } = useLibraryContext();
const stats = useLibraryQuery(['library.statistics']);
const storageBarData = useLibraryQuery(['library.kindStatistics']).data?.statistics;
console.log(storageBarData);
console.log(stats);
const { t } = useLocale();
useEffect(() => {
if (!stats.isLoading) mounted = true;
});
const { t } = useLocale();
}, [stats.isLoading]);
const StatItemNames: Partial<Record<keyof Statistics, string>> = {
total_local_bytes_capacity: t('total_bytes_capacity'),
total_local_bytes_free: t('total_bytes_free'),
total_library_bytes: t('library_bytes'),
library_db_size: t('library_db_size'),
total_local_bytes_capacity: t('total_bytes_capacity'),
total_library_preview_media_bytes: t('preview_media_bytes'),
total_local_bytes_free: t('total_bytes_free'),
total_local_bytes_used: t('total_bytes_used')
total_library_preview_media_bytes: t('preview_media_bytes')
};
const StatDescriptions: Partial<Record<keyof Statistics, string>> = {
total_local_bytes_capacity: t('total_bytes_capacity_description'),
total_library_preview_media_bytes: t('preview_media_bytes_description'),
total_local_bytes_free: t('total_bytes_free_description'),
total_library_bytes: t('library_bytes_description'),
library_db_size: t('library_db_size_description'),
total_local_bytes_free: t('total_bytes_free_description'),
total_local_bytes_used: t('total_bytes_used_description')
total_library_preview_media_bytes: t('preview_media_bytes_description')
};
const displayableStatItems = Object.keys(
StatItemNames
) as unknown as keyof typeof StatItemNames;
if (!stats.data || !stats.data.statistics) {
return <div>Loading...</div>;
}
const { statistics } = stats.data;
const totalSpace = Number(statistics.total_local_bytes_capacity);
const totalUsedSpace = Number(statistics.total_local_bytes_used);
// Define the major categories and aggregate the "Other" category
const majorCategories = ['Document', 'Text', 'Image', 'Video'];
const aggregatedData = (storageBarData ?? []).reduce(
(acc, curr) => {
const category = majorCategories.includes(curr.name) ? curr.name : 'Other';
if (!acc[category]) {
acc[category] = { total_bytes: 0 };
}
acc[category]!.total_bytes += curr.total_bytes[1];
return acc;
},
{} as Record<string, { total_bytes: number }>
);
// Calculate the used space and determine the System Data
const usedSpace = Object.values(aggregatedData).reduce(
(acc, curr) => acc + curr.total_bytes,
0
);
const systemDataBytes = totalUsedSpace - usedSpace;
if (!aggregatedData['Other']) {
aggregatedData['Other'] = { total_bytes: 0 };
}
const sections: Section[] = Object.entries(aggregatedData).map(([name, data], index) => {
const colors = [
'#3A7ECC', // Slightly Darker Blue 400
'#AAAAAA', // Gray
'#004C99', // Tailwind Blue 700
'#2563EB', // Tailwind Blue 500
'#00274D' // Dark Navy Blue,
];
const color = colors[index % colors.length] || '#8F8F8F'; // Use a default color if colors array is empty
return {
name,
value: data.total_bytes,
color,
tooltip: `${name}`
};
});
// Add System Data section
sections.push({
name: 'System Data',
value: systemDataBytes,
color: '#707070', // Gray for System Data
tooltip: 'System data that exists outside of your Spacedrive library'
});
return (
<div className="flex w-full">
<div className="flex gap-3 overflow-hidden">
{Object.entries(stats?.data?.statistics || [])
// sort the stats by the order of the displayableStatItems
<Card className="flex h-[220px] w-[750px] shrink-0 flex-col bg-app-box/50">
<div className="mb-1 flex overflow-hidden p-4">
{Object.entries(statistics)
.sort(
([a], [b]) =>
displayableStatItems.indexOf(a) - displayableStatItems.indexOf(b)
@ -115,14 +179,17 @@ const LibraryStats = () => {
<StatItem
key={`${library.uuid} ${key}`}
title={StatItemNames[key as keyof Statistics]!}
bytes={BigInt(value)}
bytes={BigInt(value as number)}
isLoading={stats.isLoading}
info={StatDescriptions[key as keyof Statistics]}
/>
);
})}
</div>
</div>
<div>
<StorageBar sections={sections} totalSpace={totalSpace} />
</div>
</Card>
);
};

View file

@ -0,0 +1,129 @@
import React, { useState } from 'react';
import { humanizeSize } from '@sd/client';
import { Tooltip } from '@sd/ui';
import { useIsDark } from '~/hooks';
const BARWIDTH = 700;
const lightenColor = (color: string, percent: number) => {
const num = parseInt(color.replace('#', ''), 16);
const amt = Math.round(2.55 * percent);
const R = (num >> 16) + amt;
const G = ((num >> 8) & 0x00ff) + amt;
const B = (num & 0x0000ff) + amt;
return `#${(
0x1000000 +
(R < 255 ? (R < 1 ? 0 : R) : 255) * 0x10000 +
(G < 255 ? (G < 1 ? 0 : G) : 255) * 0x100 +
(B < 255 ? (B < 1 ? 0 : B) : 255)
)
.toString(16)
.slice(1)
.toUpperCase()}`;
};
interface Section {
name: string;
value: number;
color: string;
tooltip: string;
}
interface StorageBarProps {
sections: Section[];
totalSpace: number;
}
const StorageBar: React.FC<StorageBarProps> = ({ sections, totalSpace }) => {
const isDark = useIsDark();
const [hoveredSectionIndex, setHoveredSectionIndex] = useState<number | null>(null);
const getPercentage = (value: number) => {
const percentage = value / totalSpace;
const pixvalue = BARWIDTH * percentage;
return `${pixvalue.toFixed(2)}px`;
};
const usedSpace = sections.reduce((acc, section) => acc + section.value, 0);
const unusedSpace = totalSpace - usedSpace;
const nonSystemSections = sections.filter((section) => section.name !== 'System Data');
const systemSection = sections.find((section) => section.name === 'System Data');
return (
<div className="w-auto p-3">
<div className="relative mt-1 flex h-6 overflow-hidden rounded">
{nonSystemSections.map((section, index) => {
const humanizedValue = humanizeSize(section.value);
const isHovered = hoveredSectionIndex === index;
return (
<Tooltip
key={index}
label={`${humanizedValue.value} ${humanizedValue.unit}`}
position="top"
>
<div
className={`relative h-full`}
style={{
width: getPercentage(section.value),
minWidth: '2px', // Ensure very small sections are visible
backgroundColor: isHovered
? lightenColor(section.color, 30)
: section.color,
transition: 'background-color 0.3s ease-in-out'
}}
onMouseEnter={() => setHoveredSectionIndex(index)}
onMouseLeave={() => setHoveredSectionIndex(null)}
/>
</Tooltip>
);
})}
{unusedSpace > 0 && (
<div
className="relative h-full"
style={{
width: getPercentage(unusedSpace),
backgroundColor: isDark ? '#1C1D25' : '#D3D3D3'
}}
/>
)}
{systemSection && (
<Tooltip
label={`${humanizeSize(systemSection.value).value} ${humanizeSize(systemSection.value).unit}`}
position="top"
>
<div
className="relative h-full rounded-r"
style={{
width: getPercentage(systemSection.value),
minWidth: '2px',
backgroundColor: systemSection.color,
transition: 'background-color 0.3s ease-in-out'
}}
/>
</Tooltip>
)}
</div>
<div className={`mt-6 flex flex-wrap ${isDark ? 'text-ink-dull' : 'text-gray-800'}`}>
{sections.map((section, index) => (
<Tooltip key={index} label={section.tooltip} position="top">
<div
className="mb-2 mr-6 flex items-center"
onMouseEnter={() => setHoveredSectionIndex(index)}
onMouseLeave={() => setHoveredSectionIndex(null)}
>
<span
className="mr-2 inline-block size-2 rounded-full"
style={{ backgroundColor: section.color }}
/>
<span className="text-sm">{section.name}</span>
</div>
</Tooltip>
))}
</div>
</div>
);
};
export default StorageBar;

View file

@ -76,10 +76,9 @@ export const Component = () => {
<div className="mt-4 flex flex-col gap-3 pt-3">
<OverviewSection>
<LibraryStatistics />
</OverviewSection>
<OverviewSection>
<FileKindStatistics />
</OverviewSection>
<OverviewSection count={1} title={t('devices')}>
{node && (
<StatisticItem
@ -93,46 +92,6 @@ export const Component = () => {
connectionType={null}
/>
)}
{/* <StatisticItem
name="Jamie's MacBook"
icon="Laptop"
total_space="4098046511104"
free_space="969004651119"
color="#0362FF"
connection_type="p2p"
/>
<StatisticItem
name="Jamie's iPhone"
icon="Mobile"
total_space="500046511104"
free_space="39006511104"
color="#0362FF"
connection_type="p2p"
/>
<StatisticItem
name="Titan NAS"
icon="Server"
total_space="60000046511104"
free_space="43000046511104"
color="#0362FF"
connection_type="p2p"
/>
<StatisticItem
name="Jamie's iPad"
icon="Tablet"
total_space="1074077906944"
free_space="121006553275"
color="#0362FF"
connection_type="lan"
/>
<StatisticItem
name="Jamie's Air"
icon="Laptop"
total_space="4098046511104"
free_space="969004651119"
color="#0362FF"
connection_type="p2p"
/> */}
<NewCard
icons={['Laptop', 'Server', 'SilverBox', 'Tablet']}
text={t('connect_device_description')}
@ -164,23 +123,6 @@ export const Component = () => {
</OverviewSection>
<OverviewSection count={0} title={t('cloud_drives')}>
{/* <StatisticItem
name="James Pine"
icon="DriveDropbox"
total_space="104877906944"
free_space="074877906944"
color="#0362FF"
connection_type="cloud"
/>
<StatisticItem
name="Spacedrive S3"
icon="DriveAmazonS3"
total_space="1074877906944"
free_space="704877906944"
color="#0362FF"
connection_type="cloud"
/> */}
<NewCard
icons={[
'DriveAmazonS3',
@ -193,22 +135,6 @@ export const Component = () => {
// buttonText={t('connect_cloud)}
/>
</OverviewSection>
{/* <OverviewSection title="Locations">
<div className="flex flex-row gap-2">
{locations.map((location) => (
<div
key={location.id}
className="flex w-[100px] flex-col items-center gap-2"
>
<Icon size={80} name="Folder" />
<span className="text-xs font-medium truncate">
{location.name}
</span>
</div>
))}
</div>
</OverviewSection> */}
</div>
</div>
</SearchContextProvider>

View file

@ -48,6 +48,7 @@
"backfill_sync_description": "Library is paused until backfill completes",
"backups": "Backups",
"backups_description": "Manage your Spacedrive database backups.",
"bar_graph_info": "Hover over each bar to see the file type. Double click to navigate.",
"bitrate": "Bitrate",
"blur_effects": "Blur Effects",
"blur_effects_description": "Some components will have a blur effect applied to them.",
@ -711,10 +712,12 @@
"total_bytes_free_description": "Free space available on all nodes connected to the library.",
"total_bytes_used": "Total used space",
"total_bytes_used_description": "Total space used on all nodes connected to the library.",
"total_files": "Total files",
"trash": "Trash",
"type": "Type",
"ui_animations": "UI Animations",
"ui_animations_description": "Dialogs and other UI elements will animate when opening and closing.",
"unidentified_files_info": "Files that Spacedrive has been unable to identify.",
"unknown": "Unknown",
"unnamed_location": "Unnamed Location",
"update": "Update",

View file

@ -35,6 +35,7 @@
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"crypto-random-string": "^5.0.0",
"d3-force": "^3.0.0",
"dayjs": "^1.11.10",
"framer-motion": "^10.16.4",
"i18next": "^23.7.10",
@ -46,6 +47,7 @@
"react-colorful": "^5.6.1",
"react-dom": "^18.2.0",
"react-error-boundary": "^4.0.11",
"react-force-graph-2d": "^1.25.5",
"react-hook-form": "^7.47.0",
"react-hotkeys-hook": "^4.4.1",
"react-i18next": "^13.5.0",
@ -71,6 +73,7 @@
},
"devDependencies": {
"@sd/config": "workspace:*",
"@types/d3-force": "^3.0.9",
"@types/node": ">18.18.x",
"@types/react": "^18.2.67",
"@types/react-dom": "^18.2.22",

View file

@ -383,9 +383,9 @@ export type JobProgressEvent = { id: string; library_id: string; task_count: num
export type JsonValue = null | boolean | number | string | JsonValue[] | { [key in string]: JsonValue }
export type KindStatistic = { kind: number; name: string; count: number; total_bytes: string }
export type KindStatistic = { kind: number; name: string; count: [number, number]; total_bytes: [number, number] }
export type KindStatistics = { statistics: KindStatistic[] }
export type KindStatistics = { statistics: KindStatistic[]; total_identified_files: number; total_unidentified_files: number }
export type Label = { id: number; name: string; date_created: string | null; date_modified: string | null }

View file

@ -147,7 +147,7 @@ importers:
version: 2.16.0
'@tauri-apps/cli':
specifier: next
version: 2.0.0-beta.19
version: 2.0.0-beta.20
'@types/react':
specifier: ^18.2.67
version: 18.2.67
@ -760,6 +760,9 @@ importers:
crypto-random-string:
specifier: ^5.0.0
version: 5.0.0
d3-force:
specifier: ^3.0.0
version: 3.0.0
dayjs:
specifier: ^1.11.10
version: 1.11.10
@ -793,6 +796,9 @@ importers:
react-error-boundary:
specifier: ^4.0.11
version: 4.0.13(react@18.2.0)
react-force-graph-2d:
specifier: ^1.25.5
version: 1.25.5(react@18.2.0)
react-hook-form:
specifier: ^7.47.0
version: 7.51.1(react@18.2.0)
@ -863,6 +869,9 @@ importers:
'@sd/config':
specifier: workspace:*
version: link:../packages/config
'@types/d3-force':
specifier: ^3.0.9
version: 3.0.9
'@types/node':
specifier: '>18.18.x'
version: 20.11.29
@ -3560,9 +3569,6 @@ packages:
'@radix-ui/number@1.0.1':
resolution: {integrity: sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==}
'@radix-ui/primitive@1.0.0':
resolution: {integrity: sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA==}
'@radix-ui/primitive@1.0.1':
resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==}
@ -3605,11 +3611,6 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-compose-refs@1.0.0':
resolution: {integrity: sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
'@radix-ui/react-compose-refs@1.0.1':
resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==}
peerDependencies:
@ -3663,12 +3664,6 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-dismissable-layer@1.0.2':
resolution: {integrity: sha512-WjJzMrTWROozDqLB0uRWYvj4UuXsM/2L19EmQ3Au+IJWqwvwq9Bwd+P8ivo0Deg9JDPArR1I6MbWNi1CmXsskg==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
'@radix-ui/react-dismissable-layer@1.0.4':
resolution: {integrity: sha512-7UpBa/RKMoHJYjie1gkF1DlK8l1fdU/VKDpoS3rCCo8YBJR294GwcEHyxHw72yvphJ7ld0AXEcSLAzY2F/WyCg==}
peerDependencies:
@ -3843,12 +3838,6 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-primitive@1.0.1':
resolution: {integrity: sha512-fHbmislWVkZaIdeF6GZxF0A/NH/3BjrGIYj+Ae6eTmTCr7EB0RQAAVEiqsXK6p3/JcRqVSBQoceZroj30Jj3XA==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
'@radix-ui/react-primitive@1.0.3':
resolution: {integrity: sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==}
peerDependencies:
@ -3927,11 +3916,6 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-slot@1.0.1':
resolution: {integrity: sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
'@radix-ui/react-slot@1.0.2':
resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==}
peerDependencies:
@ -3993,11 +3977,6 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-use-callback-ref@1.0.0':
resolution: {integrity: sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
'@radix-ui/react-use-callback-ref@1.0.1':
resolution: {integrity: sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==}
peerDependencies:
@ -4016,11 +3995,6 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-use-escape-keydown@1.0.2':
resolution: {integrity: sha512-DXGim3x74WgUv+iMNCF+cAo8xUHHeqvjx8zs7trKf+FkQKPQXLk2sX7Gx1ysH7Q76xCpZuxIJE7HLPxRE+Q+GA==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
'@radix-ui/react-use-escape-keydown@1.0.3':
resolution: {integrity: sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==}
peerDependencies:
@ -4849,6 +4823,7 @@ packages:
'@storybook/testing-library@0.2.2':
resolution: {integrity: sha512-L8sXFJUHmrlyU2BsWWZGuAjv39Jl1uAqUHdxmN42JY15M4+XCMjGlArdCCjDe1wpTSW6USYISA9axjZojgtvnw==}
deprecated: In Storybook 8, this package functionality has been integrated to a new package called @storybook/test, which uses Vitest APIs for an improved experience. When upgrading to Storybook 8 with 'npx storybook@latest upgrade', you will get prompted and will get an automigration for the new package. Please migrate when you can.
'@storybook/theming@8.0.1':
resolution: {integrity: sha512-TUmSHRh3YrpJ25DYjD+9PpJaq9Qf9P1S2xpwfNARM9r2KpkMF1/RgqnnQgZpP9od0Tzvkji7XPzxPU//EmQKEA==}
@ -5118,68 +5093,68 @@ packages:
resolution: {integrity: sha512-Np1opKANzRMF3lgJ9gDquBCB9SxlE2lRmNpVx1+L6RyzAmigkuh0ZulT5jMnDA3JLsuSDU135r/s4t/Pmx4atg==}
engines: {node: '>= 18', npm: '>= 6.6.0', yarn: '>= 1.19.1'}
'@tauri-apps/cli-darwin-arm64@2.0.0-beta.19':
resolution: {integrity: sha512-c4KvyBnQ5C/P3oAyO7WZ71xYxW8yMwDe3I4Ik3Uz6+AXZ2k3xPx19VuxCgTJdJCkxtLvhAGu9Q2IZQuuDoGTsg==}
'@tauri-apps/cli-darwin-arm64@2.0.0-beta.20':
resolution: {integrity: sha512-oCJOCib7GuYkwkBXx+ekamR8NZZU+2i3MLP+DHpDxK5gS2uhCE+CBkamJkNt6y1x6xdVnwyqZOm5RvN4SRtyIA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@tauri-apps/cli-darwin-x64@2.0.0-beta.19':
resolution: {integrity: sha512-t7rzloJwzgNXm82/w97Tq3RcvX7XmRcaxnu8ujV5SrREFxzLNRpkyzzr/vVthV7FZjKGcQf5QmJ3XeGXUfkCfQ==}
'@tauri-apps/cli-darwin-x64@2.0.0-beta.20':
resolution: {integrity: sha512-lC5QSnRExedYN4Ds6ZlSvC2PxP8qfIYBJQ5ktf+PJI5gQALdNeVtd6YnTG1ODCEklfLq9WKkGwp7JdALTU5wDA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@tauri-apps/cli-linux-arm-gnueabihf@2.0.0-beta.19':
resolution: {integrity: sha512-ZnM596ltSUNeBKH9rMGm1Ch1lCaeb1rW79nP1E6REuu1iOBpVAdkporaMWE7JSpkBZmSZdSuVDRhrMDuG7Uc6A==}
'@tauri-apps/cli-linux-arm-gnueabihf@2.0.0-beta.20':
resolution: {integrity: sha512-nZCeBMHHye5DLOJV5k2w658hnCS+LYaOZ8y/G9l3ei+g0L/HBjlSy6r4simsAT5TG8+l3oCZzLBngfTMdDS/YA==}
engines: {node: '>= 10'}
cpu: [arm]
os: [linux]
'@tauri-apps/cli-linux-arm64-gnu@2.0.0-beta.19':
resolution: {integrity: sha512-xHAFx+6EqEKLQMrqQPwnzhygA2b/nn0b7pLF48YBvkDj3KLOmv5cC+K34f2l0KIaLB8B/oVFAQKsfet4XLew+w==}
'@tauri-apps/cli-linux-arm64-gnu@2.0.0-beta.20':
resolution: {integrity: sha512-B79ISVLPVBgwnCchVqwTKU+vxnFYqxKomcR4rmsvxfs0NVtT5QuNzE1k4NUQnw3966yjwhYR3mnHsSJQSB4Eyw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@tauri-apps/cli-linux-arm64-musl@2.0.0-beta.19':
resolution: {integrity: sha512-ySRYhIfNDt/VXCycVt7d/dMBXf7L9iWf0SwynZ2nvJU/MaHIfJUgV68/l3RTRooYOCkYN4v/RRcGFD3wRmtE5g==}
'@tauri-apps/cli-linux-arm64-musl@2.0.0-beta.20':
resolution: {integrity: sha512-ojIkv/1uZHhcrgfIN8xgn4BBeo/Xg+bnV0wer6lD78zyxkUMWeEZ+u3mae1ejCJNhhaZOxNaUQ67MvDOiGyr5Q==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@tauri-apps/cli-linux-x64-gnu@2.0.0-beta.19':
resolution: {integrity: sha512-GEySXBulHQfGr3xuuv2ShnUrQtrWn3ynUtftoMiJNlpa1RTLfzglbUdA7zXag65E8h2jATVYnC/n8/sE5jtSHw==}
'@tauri-apps/cli-linux-x64-gnu@2.0.0-beta.20':
resolution: {integrity: sha512-xBy1FNbHKlc7T6pOmFQQPECxJaI5A9QWX7Kb9N64cNVusoOGlvc3xHYkXMS4PTr7xXOT0yiE1Ww2OwDRJ3lYsg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@tauri-apps/cli-linux-x64-musl@2.0.0-beta.19':
resolution: {integrity: sha512-gz1x/7EhpMcIhUvR7RhG3D+dwUnXF+MIxPoiuDAKzQAj3i6qacZJvwxyRcpVQ6HaUDpmtaHz0AKpWIMmIFL90g==}
'@tauri-apps/cli-linux-x64-musl@2.0.0-beta.20':
resolution: {integrity: sha512-+O6zq5jmtUxA1FUAAwF2ywPysy4NRo2Y6G+ESZDkY9XosRwdt5OUjqAsYktZA3AxDMZVei8r9buwTqUwi9ny/g==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@tauri-apps/cli-win32-arm64-msvc@2.0.0-beta.19':
resolution: {integrity: sha512-Zz/UwU+7QQbz9lu9cpLzX/fCgmBG1lX+K5O97kTJVcqgBiS0zUc5q1efYr7ex4c6NLVP7uaUK3IKwctBy2MvEA==}
'@tauri-apps/cli-win32-arm64-msvc@2.0.0-beta.20':
resolution: {integrity: sha512-RswgMbWyOQcv53CHvIuiuhAh4kKDqaGyZfWD4VlxqX/XhkoF5gsNgr0MxzrY7pmoL+89oVI+fiGVJz4nOQE5vA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@tauri-apps/cli-win32-ia32-msvc@2.0.0-beta.19':
resolution: {integrity: sha512-fdT/u8I31PryeqULgzzUV+bYAlgt9WStJaZWt1/hMDffB9VViL3gO7V67mtNUEhBUMaX/SqItwklbJyy3TKXXg==}
'@tauri-apps/cli-win32-ia32-msvc@2.0.0-beta.20':
resolution: {integrity: sha512-5lgWmDVXhX3SBGbiv5SduM1yajiRnUEJClWhSdRrEEJeXdsxpCsBEhxYnUnDCEzPKxLLn5fdBv3VrVctJ03csQ==}
engines: {node: '>= 10'}
cpu: [ia32]
os: [win32]
'@tauri-apps/cli-win32-x64-msvc@2.0.0-beta.19':
resolution: {integrity: sha512-EHTi4D95mTmPC/MqWU5mBGhwZ0i82iVKEAAGaKDNdwYzibmioeANCzsD8eeyuU0kCE5BCWBYpA+2epGQnfDjMg==}
'@tauri-apps/cli-win32-x64-msvc@2.0.0-beta.20':
resolution: {integrity: sha512-SuSiiVQTQPSzWlsxQp/NMzWbzDS9TdVDOw7CCfgiG5wnT2GsxzrcIAVN6i7ILsVFLxrjr0bIgPldSJcdcH84Yw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@tauri-apps/cli@2.0.0-beta.19':
resolution: {integrity: sha512-IHbgyUpnXY5ZEenQUz2Gce7w1Xl1BgLR6Jyf6SN0VbUVr9qJdSRPN7/FK+4JQFt2DC9076NVYTQFLOt03KNbwA==}
'@tauri-apps/cli@2.0.0-beta.20':
resolution: {integrity: sha512-707q9uIc2oNrYHd2dtMvxTrpZXVpart5EIktnRymNOpphkLlB6WUBjHD+ga45WqTU6cNGKbYvkKqTNfshNul9Q==}
engines: {node: '>= 10'}
hasBin: true
@ -5403,6 +5378,9 @@ packages:
'@types/cross-spawn@6.0.6':
resolution: {integrity: sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA==}
'@types/d3-force@3.0.9':
resolution: {integrity: sha512-IKtvyFdb4Q0LWna6ymywQsEYjK/94SGhPrMfEr1TIc5OBeziTi+1jcCvttts8e0UWZIxpasjnQk9MNk/3iS+kA==}
'@types/debug@4.1.12':
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
@ -5866,6 +5844,10 @@ packages:
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
engines: {node: '>= 0.6'}
accessor-fn@1.5.0:
resolution: {integrity: sha512-dml7D96DY/K5lt4Ra2jMnpL9Bhw5HEGws4p1OAIxFFj9Utd/RxNfEO3T3f0QIWFNwQU7gNxH9snUfqF/zNkP/w==}
engines: {node: '>=12'}
acorn-import-assertions@1.9.0:
resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==}
peerDependencies:
@ -6023,6 +6005,7 @@ packages:
are-we-there-yet@1.1.7:
resolution: {integrity: sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==}
deprecated: This package is no longer supported.
arg@5.0.2:
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
@ -6275,6 +6258,9 @@ packages:
resolution: {integrity: sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==}
engines: {node: '>=12.0.0'}
bezier-js@6.1.4:
resolution: {integrity: sha512-PA0FW9ZpcHbojUCMu28z9Vg/fNkwTj5YhusSAjHHDfHDGLxJ6YUKrAN2vk1fP2MMOxVw4Oko16FMlRGVBGqLKg==}
bidi-js@1.0.3:
resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==}
@ -6499,6 +6485,10 @@ packages:
caniuse-lite@1.0.30001599:
resolution: {integrity: sha512-LRAQHZ4yT1+f9LemSMeqdMpMxZcc4RMWdj4tiFe3G8tNkWK+E58g+/tzotb5cU6TbcVJLr4fySiAW7XmxQvZQA==}
canvas-color-tracker@1.2.1:
resolution: {integrity: sha512-i5clg2pEdaWqHuEM/B74NZNLkHh5+OkXbA/T4iaBiaNDagkOCXkLNrhqUfdUugsRwuaNRU20e/OygzxWRor3yg==}
engines: {node: '>=12'}
caseless@0.12.0:
resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==}
@ -7019,6 +7009,86 @@ packages:
engines: {node: ^16.0.0 || ^18.0.0 || >=20.0.0}
hasBin: true
d3-array@3.2.4:
resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==}
engines: {node: '>=12'}
d3-binarytree@1.0.2:
resolution: {integrity: sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==}
d3-color@3.1.0:
resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
engines: {node: '>=12'}
d3-dispatch@3.0.1:
resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==}
engines: {node: '>=12'}
d3-drag@3.0.0:
resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==}
engines: {node: '>=12'}
d3-ease@3.0.1:
resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
engines: {node: '>=12'}
d3-force-3d@3.0.5:
resolution: {integrity: sha512-tdwhAhoTYZY/a6eo9nR7HP3xSW/C6XvJTbeRpR92nlPzH6OiE+4MliN9feuSFd0tPtEUo+191qOhCTWx3NYifg==}
engines: {node: '>=12'}
d3-force@3.0.0:
resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==}
engines: {node: '>=12'}
d3-format@3.1.0:
resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==}
engines: {node: '>=12'}
d3-interpolate@3.0.1:
resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
engines: {node: '>=12'}
d3-octree@1.0.2:
resolution: {integrity: sha512-Qxg4oirJrNXauiuC94uKMbgxwnhdda9xRLl9ihq45srlJ4Ga3CSgqGcAL8iW7N5CIv4Oz8x3E734ulxyvHPvwA==}
d3-quadtree@3.0.1:
resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==}
engines: {node: '>=12'}
d3-scale-chromatic@3.1.0:
resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==}
engines: {node: '>=12'}
d3-scale@4.0.2:
resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==}
engines: {node: '>=12'}
d3-selection@3.0.0:
resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==}
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'}
d3-transition@3.0.1:
resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==}
engines: {node: '>=12'}
peerDependencies:
d3-selection: 2 - 3
d3-zoom@3.0.0:
resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==}
engines: {node: '>=12'}
dag-map@1.0.2:
resolution: {integrity: sha512-+LSAiGFwQ9dRnRdOeaj7g47ZFJcOUPukAP8J3A3fuZ1g9Y44BG+P1sgApjLXTQPOzC4+7S9Wr8kXsfpINM4jpw==}
@ -8100,6 +8170,10 @@ packages:
for-each@0.3.3:
resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
force-graph@1.43.5:
resolution: {integrity: sha512-HveLELh9yhZXO/QOfaFS38vlwJZ/3sKu+jarfXzRmbmihSOH/BbRWnUvmg8wLFiYy6h4HlH4lkRfZRccHYmXgA==}
engines: {node: '>=12'}
foreach@2.0.6:
resolution: {integrity: sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg==}
@ -8171,6 +8245,9 @@ packages:
from@0.1.7:
resolution: {integrity: sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==}
fromentries@1.3.2:
resolution: {integrity: sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==}
fs-constants@1.0.0:
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
@ -8233,6 +8310,7 @@ packages:
gauge@2.7.4:
resolution: {integrity: sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg==}
deprecated: This package is no longer supported.
gensequence@7.0.0:
resolution: {integrity: sha512-47Frx13aZh01afHJTB3zTtKIlFI6vWY+MYCN9Qpew6i52rfKjnhCF/l1YlC8UmEMvvntZZ6z4PiCcmyuedR2aQ==}
@ -8728,6 +8806,10 @@ packages:
resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
engines: {node: '>=8'}
index-array-by@1.4.1:
resolution: {integrity: sha512-Zu6THdrxQdyTuT2uA5FjUoBEsFHPzHcPIj18FszN6yXKHxSfGcR4TPLabfuT//E25q1Igyx9xta2WMvD/x9P/g==}
engines: {node: '>=12'}
indexof@0.0.1:
resolution: {integrity: sha512-i0G7hLJ1z0DE8dsqJa2rycj9dBmNKgXBvotXtZYXakU9oivfB9Uj2ZBC27qqef2U58/ZLwalxa1X/RDCdkHtVg==}
@ -8740,6 +8822,7 @@ packages:
inflight@1.0.6:
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
@ -8769,6 +8852,10 @@ packages:
resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==}
engines: {node: '>= 0.4'}
internmap@2.0.3:
resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
engines: {node: '>=12'}
interpret@1.4.0:
resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==}
engines: {node: '>= 0.10'}
@ -9124,6 +9211,10 @@ packages:
engines: {node: '>=10'}
hasBin: true
jerrypick@1.1.1:
resolution: {integrity: sha512-XTtedPYEyVp4t6hJrXuRKr/jHj8SC4z+4K0b396PMkov6muL+i8IIamJIvZWe3jUspgIJak0P+BaWKawMYNBLg==}
engines: {node: '>=12'}
jest-environment-node@29.7.0:
resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@ -9274,6 +9365,10 @@ packages:
jwt-decode@3.1.2:
resolution: {integrity: sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==}
kapsule@1.14.5:
resolution: {integrity: sha512-H0iSpTynUzZw3tgraDmReprpFRmH5oP5GPmaNsurSwLx2H5iCpOMIkp5q+sfhB4Tz/UJd1E1IbEE9Z6ksnJ6RA==}
engines: {node: '>=12'}
katex@0.16.9:
resolution: {integrity: sha512-fsSYjWS0EEOwvy81j3vRA8TEAhQhKiqO+FQaKWp0m39qwOzHVBgAUBIXWj1pB+O2W3fIpNa6Y9KSKCVbfPhyAQ==}
hasBin: true
@ -9462,6 +9557,9 @@ packages:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
lodash-es@4.17.21:
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
lodash.camelcase@4.3.0:
resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==}
@ -10353,6 +10451,7 @@ packages:
npmlog@4.1.2:
resolution: {integrity: sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==}
deprecated: This package is no longer supported.
nth-check@2.1.1:
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
@ -10508,6 +10607,7 @@ packages:
osenv@0.1.5:
resolution: {integrity: sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==}
deprecated: This package is no longer supported.
ospath@1.2.2:
resolution: {integrity: sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==}
@ -11145,6 +11245,12 @@ packages:
peerDependencies:
react: '>=16.13.1'
react-force-graph-2d@1.25.5:
resolution: {integrity: sha512-3u8WjZZorpwZSDs3n3QeOS9ZoxFPM+IR9SStYJVQ/qKECydMHarxnf7ynV/MKJbC6kUsc60soD0V+Uq/r2vz7Q==}
engines: {node: '>=12'}
peerDependencies:
react: '*'
react-freeze@1.0.4:
resolution: {integrity: sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA==}
engines: {node: '>=10'}
@ -11208,6 +11314,12 @@ packages:
react: ^17.0.0 || ^16.3.0 || ^15.5.4
react-dom: ^17.0.0 || ^16.3.0 || ^15.5.4
react-kapsule@2.4.0:
resolution: {integrity: sha512-w4Yv9CgWdj8kWGQEPNWFGJJ08dYEZHZpiaFR/DgZjCMBNqv9wus2Gy1qvHVJmJbzvAZbq6jdvFC+NYzEqAlNhQ==}
engines: {node: '>=12'}
peerDependencies:
react: '>=16.13.1'
react-lifecycles-compat@3.0.4:
resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==}
@ -11715,18 +11827,22 @@ packages:
rimraf@2.4.5:
resolution: {integrity: sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ==}
deprecated: Rimraf versions prior to v4 are no longer supported
hasBin: true
rimraf@2.6.3:
resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==}
deprecated: Rimraf versions prior to v4 are no longer supported
hasBin: true
rimraf@2.7.1:
resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==}
deprecated: Rimraf versions prior to v4 are no longer supported
hasBin: true
rimraf@3.0.2:
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
deprecated: Rimraf versions prior to v4 are no longer supported
hasBin: true
ripemd160@2.0.2:
@ -12475,6 +12591,9 @@ packages:
tiny-invariant@1.3.3:
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
tinycolor2@1.6.0:
resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==}
tinyspy@2.2.1:
resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==}
engines: {node: '>=14.0.0'}
@ -16333,10 +16452,6 @@ snapshots:
dependencies:
'@babel/runtime': 7.24.0
'@radix-ui/primitive@1.0.0':
dependencies:
'@babel/runtime': 7.24.0
'@radix-ui/primitive@1.0.1':
dependencies:
'@babel/runtime': 7.24.0
@ -16381,11 +16496,6 @@ snapshots:
'@types/react': 18.2.67
'@types/react-dom': 18.2.22
'@radix-ui/react-compose-refs@1.0.0(react@18.2.0)':
dependencies:
'@babel/runtime': 7.24.0
react: 18.2.0
'@radix-ui/react-compose-refs@1.0.1(@types/react@18.2.67)(react@18.2.0)':
dependencies:
'@babel/runtime': 7.24.0
@ -16445,17 +16555,6 @@ snapshots:
optionalDependencies:
'@types/react': 18.2.67
'@radix-ui/react-dismissable-layer@1.0.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@babel/runtime': 7.24.0
'@radix-ui/primitive': 1.0.0
'@radix-ui/react-compose-refs': 1.0.0(react@18.2.0)
'@radix-ui/react-primitive': 1.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-use-callback-ref': 1.0.0(react@18.2.0)
'@radix-ui/react-use-escape-keydown': 1.0.2(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
'@radix-ui/react-dismissable-layer@1.0.4(@types/react-dom@18.2.22)(@types/react@18.2.67)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@babel/runtime': 7.24.0
@ -16547,7 +16646,7 @@ snapshots:
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.67)(react@18.2.0)
'@radix-ui/react-context': 1.0.1(@types/react@18.2.67)(react@18.2.0)
'@radix-ui/react-direction': 1.0.1(@types/react@18.2.67)(react@18.2.0)
'@radix-ui/react-dismissable-layer': 1.0.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.2.22)(@types/react@18.2.67)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.67)(react@18.2.0)
'@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.2.22)(@types/react@18.2.67)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-id': 1.0.1(@types/react@18.2.67)(react@18.2.0)
@ -16659,13 +16758,6 @@ snapshots:
'@types/react': 18.2.67
'@types/react-dom': 18.2.22
'@radix-ui/react-primitive@1.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@babel/runtime': 7.24.0
'@radix-ui/react-slot': 1.0.1(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
'@radix-ui/react-primitive@1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.67)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@babel/runtime': 7.24.0
@ -16774,12 +16866,6 @@ snapshots:
'@types/react': 18.2.67
'@types/react-dom': 18.2.22
'@radix-ui/react-slot@1.0.1(react@18.2.0)':
dependencies:
'@babel/runtime': 7.24.0
'@radix-ui/react-compose-refs': 1.0.0(react@18.2.0)
react: 18.2.0
'@radix-ui/react-slot@1.0.2(@types/react@18.2.67)(react@18.2.0)':
dependencies:
'@babel/runtime': 7.24.0
@ -16863,11 +16949,6 @@ snapshots:
'@types/react': 18.2.67
'@types/react-dom': 18.2.22
'@radix-ui/react-use-callback-ref@1.0.0(react@18.2.0)':
dependencies:
'@babel/runtime': 7.24.0
react: 18.2.0
'@radix-ui/react-use-callback-ref@1.0.1(@types/react@18.2.67)(react@18.2.0)':
dependencies:
'@babel/runtime': 7.24.0
@ -16883,12 +16964,6 @@ snapshots:
optionalDependencies:
'@types/react': 18.2.67
'@radix-ui/react-use-escape-keydown@1.0.2(react@18.2.0)':
dependencies:
'@babel/runtime': 7.24.0
'@radix-ui/react-use-callback-ref': 1.0.0(react@18.2.0)
react: 18.2.0
'@radix-ui/react-use-escape-keydown@1.0.3(@types/react@18.2.67)(react@18.2.0)':
dependencies:
'@babel/runtime': 7.24.0
@ -18783,48 +18858,48 @@ snapshots:
'@tauri-apps/api@2.0.0-beta.13': {}
'@tauri-apps/cli-darwin-arm64@2.0.0-beta.19':
'@tauri-apps/cli-darwin-arm64@2.0.0-beta.20':
optional: true
'@tauri-apps/cli-darwin-x64@2.0.0-beta.19':
'@tauri-apps/cli-darwin-x64@2.0.0-beta.20':
optional: true
'@tauri-apps/cli-linux-arm-gnueabihf@2.0.0-beta.19':
'@tauri-apps/cli-linux-arm-gnueabihf@2.0.0-beta.20':
optional: true
'@tauri-apps/cli-linux-arm64-gnu@2.0.0-beta.19':
'@tauri-apps/cli-linux-arm64-gnu@2.0.0-beta.20':
optional: true
'@tauri-apps/cli-linux-arm64-musl@2.0.0-beta.19':
'@tauri-apps/cli-linux-arm64-musl@2.0.0-beta.20':
optional: true
'@tauri-apps/cli-linux-x64-gnu@2.0.0-beta.19':
'@tauri-apps/cli-linux-x64-gnu@2.0.0-beta.20':
optional: true
'@tauri-apps/cli-linux-x64-musl@2.0.0-beta.19':
'@tauri-apps/cli-linux-x64-musl@2.0.0-beta.20':
optional: true
'@tauri-apps/cli-win32-arm64-msvc@2.0.0-beta.19':
'@tauri-apps/cli-win32-arm64-msvc@2.0.0-beta.20':
optional: true
'@tauri-apps/cli-win32-ia32-msvc@2.0.0-beta.19':
'@tauri-apps/cli-win32-ia32-msvc@2.0.0-beta.20':
optional: true
'@tauri-apps/cli-win32-x64-msvc@2.0.0-beta.19':
'@tauri-apps/cli-win32-x64-msvc@2.0.0-beta.20':
optional: true
'@tauri-apps/cli@2.0.0-beta.19':
'@tauri-apps/cli@2.0.0-beta.20':
optionalDependencies:
'@tauri-apps/cli-darwin-arm64': 2.0.0-beta.19
'@tauri-apps/cli-darwin-x64': 2.0.0-beta.19
'@tauri-apps/cli-linux-arm-gnueabihf': 2.0.0-beta.19
'@tauri-apps/cli-linux-arm64-gnu': 2.0.0-beta.19
'@tauri-apps/cli-linux-arm64-musl': 2.0.0-beta.19
'@tauri-apps/cli-linux-x64-gnu': 2.0.0-beta.19
'@tauri-apps/cli-linux-x64-musl': 2.0.0-beta.19
'@tauri-apps/cli-win32-arm64-msvc': 2.0.0-beta.19
'@tauri-apps/cli-win32-ia32-msvc': 2.0.0-beta.19
'@tauri-apps/cli-win32-x64-msvc': 2.0.0-beta.19
'@tauri-apps/cli-darwin-arm64': 2.0.0-beta.20
'@tauri-apps/cli-darwin-x64': 2.0.0-beta.20
'@tauri-apps/cli-linux-arm-gnueabihf': 2.0.0-beta.20
'@tauri-apps/cli-linux-arm64-gnu': 2.0.0-beta.20
'@tauri-apps/cli-linux-arm64-musl': 2.0.0-beta.20
'@tauri-apps/cli-linux-x64-gnu': 2.0.0-beta.20
'@tauri-apps/cli-linux-x64-musl': 2.0.0-beta.20
'@tauri-apps/cli-win32-arm64-msvc': 2.0.0-beta.20
'@tauri-apps/cli-win32-ia32-msvc': 2.0.0-beta.20
'@tauri-apps/cli-win32-x64-msvc': 2.0.0-beta.20
'@tauri-apps/plugin-dialog@2.0.0-beta.3':
dependencies:
@ -19125,6 +19200,8 @@ snapshots:
dependencies:
'@types/node': 20.11.29
'@types/d3-force@3.0.9': {}
'@types/debug@4.1.12':
dependencies:
'@types/ms': 0.7.34
@ -19679,6 +19756,8 @@ snapshots:
mime-types: 2.1.35
negotiator: 0.6.3
accessor-fn@1.5.0: {}
acorn-import-assertions@1.9.0(acorn@8.11.3):
dependencies:
acorn: 8.11.3
@ -20181,6 +20260,8 @@ snapshots:
dependencies:
open: 8.4.2
bezier-js@6.1.4: {}
bidi-js@1.0.3:
dependencies:
require-from-string: 2.0.2
@ -20452,6 +20533,10 @@ snapshots:
caniuse-lite@1.0.30001599: {}
canvas-color-tracker@1.2.1:
dependencies:
tinycolor2: 1.6.0
caseless@0.12.0: {}
ccount@2.0.1: {}
@ -21110,6 +21195,89 @@ snapshots:
untildify: 4.0.0
yauzl: 2.10.0
d3-array@3.2.4:
dependencies:
internmap: 2.0.3
d3-binarytree@1.0.2: {}
d3-color@3.1.0: {}
d3-dispatch@3.0.1: {}
d3-drag@3.0.0:
dependencies:
d3-dispatch: 3.0.1
d3-selection: 3.0.0
d3-ease@3.0.1: {}
d3-force-3d@3.0.5:
dependencies:
d3-binarytree: 1.0.2
d3-dispatch: 3.0.1
d3-octree: 1.0.2
d3-quadtree: 3.0.1
d3-timer: 3.0.1
d3-force@3.0.0:
dependencies:
d3-dispatch: 3.0.1
d3-quadtree: 3.0.1
d3-timer: 3.0.1
d3-format@3.1.0: {}
d3-interpolate@3.0.1:
dependencies:
d3-color: 3.1.0
d3-octree@1.0.2: {}
d3-quadtree@3.0.1: {}
d3-scale-chromatic@3.1.0:
dependencies:
d3-color: 3.1.0
d3-interpolate: 3.0.1
d3-scale@4.0.2:
dependencies:
d3-array: 3.2.4
d3-format: 3.1.0
d3-interpolate: 3.0.1
d3-time: 3.1.0
d3-time-format: 4.1.0
d3-selection@3.0.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: {}
d3-transition@3.0.1(d3-selection@3.0.0):
dependencies:
d3-color: 3.1.0
d3-dispatch: 3.0.1
d3-ease: 3.0.1
d3-interpolate: 3.0.1
d3-selection: 3.0.0
d3-timer: 3.0.1
d3-zoom@3.0.0:
dependencies:
d3-dispatch: 3.0.1
d3-drag: 3.0.0
d3-interpolate: 3.0.1
d3-selection: 3.0.0
d3-transition: 3.0.1(d3-selection@3.0.0)
dag-map@1.0.2: {}
damerau-levenshtein@1.0.8: {}
@ -22637,6 +22805,23 @@ snapshots:
dependencies:
is-callable: 1.2.7
force-graph@1.43.5:
dependencies:
'@tweenjs/tween.js': 23.1.1
accessor-fn: 1.5.0
bezier-js: 6.1.4
canvas-color-tracker: 1.2.1
d3-array: 3.2.4
d3-drag: 3.0.0
d3-force-3d: 3.0.5
d3-scale: 4.0.2
d3-scale-chromatic: 3.1.0
d3-selection: 3.0.0
d3-zoom: 3.0.0
index-array-by: 1.4.1
kapsule: 1.14.5
lodash-es: 4.17.21
foreach@2.0.6: {}
foreground-child@3.1.1:
@ -22707,6 +22892,8 @@ snapshots:
from@0.1.7: {}
fromentries@1.3.2: {}
fs-constants@1.0.0: {}
fs-extra@10.1.0:
@ -23405,6 +23592,8 @@ snapshots:
indent-string@4.0.0: {}
index-array-by@1.4.1: {}
indexof@0.0.1: {}
infer-owner@1.0.4: {}
@ -23439,6 +23628,8 @@ snapshots:
hasown: 2.0.2
side-channel: 1.0.6
internmap@2.0.3: {}
interpret@1.4.0: {}
invariant@2.2.4:
@ -23725,6 +23916,8 @@ snapshots:
filelist: 1.0.4
minimatch: 3.1.2
jerrypick@1.1.1: {}
jest-environment-node@29.7.0:
dependencies:
'@jest/environment': 29.7.0
@ -23935,6 +24128,10 @@ snapshots:
jwt-decode@3.1.2: {}
kapsule@1.14.5:
dependencies:
lodash-es: 4.17.21
katex@0.16.9:
dependencies:
commander: 8.3.0
@ -24138,6 +24335,8 @@ snapshots:
dependencies:
p-locate: 5.0.0
lodash-es@4.17.21: {}
lodash.camelcase@4.3.0: {}
lodash.castarray@4.4.0: {}
@ -26358,6 +26557,13 @@ snapshots:
'@babel/runtime': 7.24.0
react: 18.2.0
react-force-graph-2d@1.25.5(react@18.2.0):
dependencies:
force-graph: 1.43.5
prop-types: 15.8.1
react: 18.2.0
react-kapsule: 2.4.0(react@18.2.0)
react-freeze@1.0.4(react@18.2.0):
dependencies:
react: 18.2.0
@ -26412,6 +26618,12 @@ snapshots:
- '@types/react'
- encoding
react-kapsule@2.4.0(react@18.2.0):
dependencies:
fromentries: 1.3.2
jerrypick: 1.1.1
react: 18.2.0
react-lifecycles-compat@3.0.4: {}
react-loading-icons@1.1.0: {}
@ -28120,6 +28332,8 @@ snapshots:
tiny-invariant@1.3.3: {}
tinycolor2@1.6.0: {}
tinyspy@2.2.1: {}
tmp@0.0.33: