Merge remote-tracking branch 'origin/onelauncher/main' into oneclient/main
This commit is contained in:
commit
d4441ae0e4
22 changed files with 1432 additions and 462 deletions
|
|
@ -42,6 +42,8 @@
|
|||
"path": "$DATA/OneLauncher"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"opener:allow-open-url",
|
||||
"opener:default"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,9 @@
|
|||
"react": "catalog:",
|
||||
"react-aria-components": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"react-markdown": "^10.1.0",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"tailwind-merge": "catalog:",
|
||||
"tailwind-variants": "catalog:",
|
||||
"tailwindcss": "catalog:",
|
||||
|
|
@ -42,6 +45,7 @@
|
|||
"valtio": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tanstack/eslint-config": "catalog:",
|
||||
"@types/react": "catalog:",
|
||||
"@types/react-dom": "catalog:",
|
||||
|
|
|
|||
|
|
@ -1,12 +1,19 @@
|
|||
import type { Provider, SearchResult } from '@/bindings.gen';
|
||||
import { abbreviateNumber } from '@/utils';
|
||||
import { Show } from '@onelauncher/common/components';
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
import { abbreviateNumber, LOADERS, upperFirst } from '@/utils';
|
||||
import { Show, Tooltip } from '@onelauncher/common/components';
|
||||
import { Link, useNavigate } from '@tanstack/react-router';
|
||||
import { Download01Icon } from '@untitled-theme/icons-react';
|
||||
import { useMemo } from 'react';
|
||||
import { Focusable } from 'react-aria-components';
|
||||
import LoaderIcon from '../launcher/LoaderIcon';
|
||||
|
||||
function includes<T, TArray extends T>(list: { includes: (arg0: TArray) => boolean }, element: T): element is TArray {
|
||||
return list.includes(element as unknown as TArray);
|
||||
}
|
||||
|
||||
export function PackageGrid({ items, provider }: { items: Array<SearchResult>; provider: Provider }) {
|
||||
return (
|
||||
<div className="grid grid-cols-2 xl:grid-cols-3 gap-2">
|
||||
<div className="grid grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-2">
|
||||
{items.map(item => (
|
||||
<PackageItem key={item.project_id} {...item} provider={provider} />
|
||||
))}
|
||||
|
|
@ -15,16 +22,14 @@ export function PackageGrid({ items, provider }: { items: Array<SearchResult>; p
|
|||
}
|
||||
|
||||
export function PackageItem({ provider, ...item }: SearchResult & { provider: Provider }) {
|
||||
const navigate = useNavigate()
|
||||
function redirect() {
|
||||
navigate({to: "/app/browser/package/$provider/$slug", params: {provider, slug: item.slug}})
|
||||
}
|
||||
const loaders = useMemo(() => item.categories.filter(cat => includes(LOADERS, cat)), []);
|
||||
|
||||
return (
|
||||
<div
|
||||
<Link
|
||||
className="h-full min-w-50 flex overflow-hidden rounded-lg bg-component-bg hover:bg-component-bg-hover flex-col max-h-74 min-h-74"
|
||||
onClick={redirect}
|
||||
params={{ provider, slug: item.slug }}
|
||||
tabIndex={0}
|
||||
to="/app/browser/package/$provider/$slug"
|
||||
>
|
||||
<div
|
||||
className="relative flex items-center justify-center overflow-hidden w-full h-28"
|
||||
|
|
@ -37,13 +42,28 @@ export function PackageItem({ provider, ...item }: SearchResult & { provider: Pr
|
|||
)}
|
||||
when={item.icon_url}
|
||||
>
|
||||
<img alt={`Icon for ${item.title}`} className="absolute z-0 max-w-none w-7/6 opacity-50 filter-blur-xl" src={item.icon_url} />
|
||||
<img alt={`Icon for ${item.title}`} className="absolute z-0 max-w-none w-7/6 opacity-50 blur-xl" src={item.icon_url} />
|
||||
<img
|
||||
alt={`Icon for ${item.title}`}
|
||||
className="relative z-1 aspect-ratio-square rounded-md image-render-auto w-2/5"
|
||||
className="relative z-1 aspect-ratio-square rounded-lg image-render-auto h-5/6"
|
||||
src={item.icon_url}
|
||||
/>
|
||||
</Show>
|
||||
<Tooltip className="bg-component-bg-disabled" text={loaders.map(upperFirst).join(', ')}>
|
||||
<Focusable>
|
||||
<div className="flex flex-col rounded-full bg-component-bg/70 border-component-border/70 border p-1 absolute top-0 right-0 m-2">
|
||||
{loaders.toSpliced(loaders.length > 3 ? 2 : 3).map(loader => <LoaderIcon className="w-5 m-1" key={loader} loader={loader} />)}
|
||||
{loaders.length > 3 && (
|
||||
<div className="bg-component-bg/50 rounded-full w-7 h-7 flex items-center justify-center">
|
||||
<span className="tracking-tight -ml-0.5 mt-0.5">
|
||||
+
|
||||
{loaders.length - 2}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Focusable>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col gap-2 p-3">
|
||||
<div className="flex flex-col gap-2">
|
||||
|
|
@ -70,6 +90,6 @@ export function PackageItem({ provider, ...item }: SearchResult & { provider: Pr
|
|||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { GameLoader, Version, VersionType } from '@/bindings.gen';
|
||||
import type { GameLoader, Version } from '@/bindings.gen';
|
||||
import type { JSX, RefAttributes } from 'react';
|
||||
import DefaultBanner from '@/assets/images/default_banner.png';
|
||||
import LauncherIcon from '@/components/content/LauncherIcon';
|
||||
|
|
@ -10,17 +10,17 @@ import { useCommand, useCommandMut } from '@onelauncher/common';
|
|||
import { Button, Dropdown, SelectList, TextField } from '@onelauncher/common/components';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { ArrowRightIcon, PlusIcon, SearchMdIcon, Server01Icon, TextInputIcon, User03Icon } from '@untitled-theme/icons-react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { Checkbox } from 'react-aria-components';
|
||||
import LoaderIcon from '../LoaderIcon';
|
||||
|
||||
const STEPS = [
|
||||
{
|
||||
id: 'loader',
|
||||
id: 'loader' as const,
|
||||
title: 'Select Provider',
|
||||
},
|
||||
{
|
||||
id: 'info',
|
||||
id: 'info' as const,
|
||||
title: 'Game Setup',
|
||||
},
|
||||
] as const;
|
||||
|
|
@ -32,9 +32,16 @@ interface ClusterFormData {
|
|||
clusterProvider: string;
|
||||
}
|
||||
|
||||
interface FormValidation {
|
||||
isProviderValid: boolean;
|
||||
isNameValid: boolean;
|
||||
isVersionValid: boolean;
|
||||
}
|
||||
|
||||
export function NewClusterCreate() {
|
||||
const queryClient = useQueryClient();
|
||||
const [currentStepIndex, setCurrentStepIndex] = useState(0);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [formData, setFormData] = useState<ClusterFormData>({
|
||||
loader: 'vanilla',
|
||||
clusterName: '',
|
||||
|
|
@ -42,6 +49,12 @@ export function NewClusterCreate() {
|
|||
clusterProvider: '',
|
||||
});
|
||||
|
||||
const validation = useMemo((): FormValidation => ({
|
||||
isProviderValid: Boolean(formData.clusterProvider),
|
||||
isNameValid: Boolean(formData.clusterName.trim()),
|
||||
isVersionValid: Boolean(formData.clusterVersion.trim()),
|
||||
}), [formData.clusterProvider, formData.clusterName, formData.clusterVersion]);
|
||||
|
||||
const create = useCommandMut(() => bindings.core.createCluster({
|
||||
icon: null,
|
||||
mc_loader: formData.loader,
|
||||
|
|
@ -51,129 +64,193 @@ export function NewClusterCreate() {
|
|||
}), {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['getClusters'] });
|
||||
// Modal'ı kapat ve formu sıfırla
|
||||
setIsModalOpen(false);
|
||||
setCurrentStepIndex(0);
|
||||
setFormData({
|
||||
loader: 'vanilla',
|
||||
clusterName: '',
|
||||
clusterVersion: '',
|
||||
clusterProvider: '',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
function handleCreate() {
|
||||
if (!isLastInputStep && !formData.clusterName)
|
||||
const handleCreate = useCallback(() => {
|
||||
if (!validation.isNameValid || !validation.isVersionValid) {
|
||||
console.warn('Form validation failed');
|
||||
return;
|
||||
}
|
||||
|
||||
create.mutate();
|
||||
}, [create, validation.isNameValid, validation.isVersionValid]);
|
||||
|
||||
if (create.isError)
|
||||
console.error(create.error.message);
|
||||
}
|
||||
|
||||
const handleNext = useCallback(async () => {
|
||||
if (currentStepIndex === STEPS.length - 1)
|
||||
const handleNext = useCallback(() => {
|
||||
if (currentStepIndex === STEPS.length - 1) {
|
||||
handleCreate();
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentStepIndex < STEPS.length - 1)
|
||||
setCurrentStepIndex(currentStepIndex + 1);
|
||||
}, [currentStepIndex]);
|
||||
setCurrentStepIndex(prev => prev + 1);
|
||||
}, [currentStepIndex, handleCreate]);
|
||||
|
||||
const handleBack = useCallback(() => {
|
||||
if (currentStepIndex > 0)
|
||||
setCurrentStepIndex(currentStepIndex - 1);
|
||||
setCurrentStepIndex(prev => prev - 1);
|
||||
}, [currentStepIndex]);
|
||||
|
||||
const formHandlers = useMemo(() => ({
|
||||
onProviderChange: (provider: string) =>
|
||||
setFormData(prev => ({ ...prev, clusterProvider: provider })),
|
||||
onLoaderChange: (loader: GameLoader) =>
|
||||
setFormData(prev => ({ ...prev, loader })),
|
||||
onNameChange: (name: string) =>
|
||||
setFormData(prev => ({ ...prev, clusterName: name })),
|
||||
onVersionChange: (version: string) =>
|
||||
setFormData(prev => ({ ...prev, clusterVersion: version })),
|
||||
}), []);
|
||||
|
||||
const currentStepConfig = STEPS[currentStepIndex];
|
||||
let stepContent: JSX.Element;
|
||||
|
||||
switch (currentStepConfig.id) {
|
||||
case 'loader': {
|
||||
stepContent = (
|
||||
<ClusterLoader onSelectProvider={provider => setFormData(prev => ({ ...prev, clusterProvider: provider }))} selectedProvider={formData.clusterProvider} />
|
||||
);
|
||||
break;
|
||||
const stepContent = useMemo((): JSX.Element => {
|
||||
switch (currentStepConfig.id) {
|
||||
case 'loader':
|
||||
return (
|
||||
<ClusterLoader
|
||||
onSelectProvider={formHandlers.onProviderChange}
|
||||
selectedProvider={formData.clusterProvider}
|
||||
/>
|
||||
);
|
||||
case 'info':
|
||||
return (
|
||||
<ClusterInformation
|
||||
clusterLoader={formData.loader}
|
||||
clusterName={formData.clusterName}
|
||||
clusterVersion={formData.clusterVersion}
|
||||
onClusterLoaderChange={formHandlers.onLoaderChange}
|
||||
onClusterNameChange={formHandlers.onNameChange}
|
||||
onClusterVersionChange={formHandlers.onVersionChange}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return <div>Unknown step</div>;
|
||||
}
|
||||
|
||||
case 'info': {
|
||||
stepContent = (
|
||||
<ClusterInformation
|
||||
clusterLoader={formData.loader}
|
||||
clusterName={formData.clusterName}
|
||||
clusterVersion={formData.clusterVersion}
|
||||
onClusterLoaderChange={loader => setFormData(prev => ({ ...prev, loader }))}
|
||||
onClusterNameChange={name => setFormData(prev => ({ ...prev, clusterName: name }))}
|
||||
onClusterVersionChange={version =>
|
||||
setFormData(prev => ({ ...prev, clusterVersion: version }))}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, [currentStepConfig.id, formData, formHandlers]);
|
||||
|
||||
const isFirstStep = currentStepIndex === 0;
|
||||
const isLastInputStep = currentStepIndex === STEPS.length - 1;
|
||||
const nextButtonText = isLastInputStep ? 'Create' : 'Next';
|
||||
|
||||
let isNextDisabled = false;
|
||||
if (currentStepConfig.id === 'loader' && !formData.clusterProvider)
|
||||
isNextDisabled = true;
|
||||
else if (
|
||||
currentStepConfig.id === 'info'
|
||||
&& (!formData.clusterName.trim() || !formData.clusterVersion.trim())
|
||||
)
|
||||
isNextDisabled = false;
|
||||
const isNextDisabled = useMemo(() => {
|
||||
switch (currentStepConfig.id) {
|
||||
case 'loader':
|
||||
return !validation.isProviderValid;
|
||||
case 'info':
|
||||
return !validation.isNameValid || !validation.isVersionValid;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}, [currentStepConfig.id, validation]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-row justify-between w-full">
|
||||
<div>
|
||||
<TextField className="p-1" iconLeft={<SearchMdIcon className="size-4" />} placeholder="Search for something..." />
|
||||
</div>
|
||||
|
||||
<Modal.Trigger>
|
||||
<Button>
|
||||
<PlusIcon className="stroke-width-[2.2] size-5" />
|
||||
New cluster
|
||||
</Button>
|
||||
|
||||
<Modal>
|
||||
<div className="min-w-sm flex flex-col rounded-lg bg-page text-center">
|
||||
<div className="theme-OneLauncher-Dark relative h-25 flex">
|
||||
<div className="absolute left-0 top-0 h-full w-full">
|
||||
<img alt="Header Image" className="h-full w-full rounded-t-lg" src={DefaultBanner} />
|
||||
</div>
|
||||
<div
|
||||
className="absolute left-0 top-0 h-full flex w-full flex-row items-center justify-start gap-x-4 bg-[radial-gradient(at_center,#00000077,transparent)] px-10"
|
||||
>
|
||||
<Server01Icon className="h-8 w-8 text-fg-primary" />
|
||||
<div className="flex flex-col items-start justify-center">
|
||||
<h1 className="h-10 text-fg-primary text-2xl font-semibold">New Cluster</h1>
|
||||
<span className="text-fg-primary">{STEPS[currentStepIndex].title}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col rounded-b-lg border border-white/5">
|
||||
<div className="p-3">
|
||||
{stepContent}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row justify-end gap-x-2 p-3 pt-0">
|
||||
<Button
|
||||
children="Previous"
|
||||
color="ghost"
|
||||
isDisabled={isFirstStep}
|
||||
onClick={handleBack}
|
||||
/>
|
||||
<Button
|
||||
color="primary"
|
||||
isDisabled={isNextDisabled}
|
||||
onClick={handleNext}
|
||||
>
|
||||
{nextButtonText}
|
||||
{' '}
|
||||
<ArrowRightIcon />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</Modal.Trigger>
|
||||
<div className="flex flex-row justify-between w-full">
|
||||
<div>
|
||||
<TextField
|
||||
aria-label="Cluster Search"
|
||||
className="p-1"
|
||||
iconLeft={<SearchMdIcon className="size-4" />}
|
||||
placeholder="Search for something..."
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
<Button onPress={() => setIsModalOpen(true)}>
|
||||
<PlusIcon className="stroke-width-[2.2] size-5" />
|
||||
New cluster
|
||||
</Button>
|
||||
|
||||
<Modal isDismissable isOpen={isModalOpen} onOpenChange={setIsModalOpen}>
|
||||
<div className="min-w-sm flex flex-col rounded-lg bg-page text-center">
|
||||
<ModalHeader currentStep={currentStepConfig} />
|
||||
|
||||
<div className="flex flex-col rounded-b-lg border border-white/5">
|
||||
<div className="p-3">
|
||||
{stepContent}
|
||||
</div>
|
||||
|
||||
<ModalFooter
|
||||
isFirstStep={isFirstStep}
|
||||
isNextDisabled={isNextDisabled}
|
||||
nextButtonText={nextButtonText}
|
||||
onBack={handleBack}
|
||||
onNext={handleNext}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ModalHeaderProps {
|
||||
currentStep: typeof STEPS[number];
|
||||
}
|
||||
|
||||
function ModalHeader({ currentStep }: ModalHeaderProps) {
|
||||
return (
|
||||
<div className="theme-OneLauncher-Dark relative h-25 flex">
|
||||
<div className="absolute left-0 top-0 h-full w-full">
|
||||
<img
|
||||
alt="Header Image"
|
||||
className="h-full w-full rounded-t-lg"
|
||||
src={DefaultBanner}
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute left-0 top-0 h-full flex w-full flex-row items-center justify-start gap-x-4 bg-[radial-gradient(at_center,#00000077,transparent)] px-10">
|
||||
<Server01Icon className="h-8 w-8 text-fg-primary" />
|
||||
<div className="flex flex-col items-start justify-center">
|
||||
<h1 className="h-10 text-fg-primary text-2xl font-semibold">New Cluster</h1>
|
||||
<span className="text-fg-primary">{currentStep.title}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ModalFooterProps {
|
||||
isFirstStep: boolean;
|
||||
isNextDisabled: boolean;
|
||||
nextButtonText: string;
|
||||
onBack: () => void;
|
||||
onNext: () => void;
|
||||
}
|
||||
|
||||
function ModalFooter({
|
||||
isFirstStep,
|
||||
isNextDisabled,
|
||||
nextButtonText,
|
||||
onBack,
|
||||
onNext,
|
||||
}: ModalFooterProps) {
|
||||
return (
|
||||
<div className="flex flex-row justify-end gap-x-2 p-3 pt-0">
|
||||
<Button
|
||||
color="ghost"
|
||||
isDisabled={isFirstStep}
|
||||
onClick={onBack}
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
isDisabled={isNextDisabled}
|
||||
onClick={onNext}
|
||||
>
|
||||
{nextButtonText}
|
||||
{' '}
|
||||
<ArrowRightIcon />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -183,49 +260,60 @@ interface ClusterLoaderProps {
|
|||
}
|
||||
|
||||
function ClusterLoader({ selectedProvider, onSelectProvider }: ClusterLoaderProps) {
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<ProviderCard
|
||||
icon={<User03Icon />}
|
||||
name="New"
|
||||
selected={selectedProvider === 'new'}
|
||||
setSelected={() => onSelectProvider('new')}
|
||||
/>
|
||||
const providers = useMemo(() => [
|
||||
{ id: 'new', name: 'New', icon: <User03Icon /> },
|
||||
...LAUNCHER_IMPORT_TYPES.map(data => ({
|
||||
id: data,
|
||||
name: data,
|
||||
icon: <LauncherIcon launcher={data} />,
|
||||
})),
|
||||
], []);
|
||||
|
||||
{LAUNCHER_IMPORT_TYPES.map((data, i) => (
|
||||
<ProviderCard
|
||||
icon={<LauncherIcon launcher={data} />}
|
||||
key={i}
|
||||
name={data}
|
||||
selected={selectedProvider === data}
|
||||
setSelected={() => onSelectProvider(data)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
return (
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{providers.map(provider => (
|
||||
<ProviderCard
|
||||
icon={provider.icon}
|
||||
key={provider.id}
|
||||
name={provider.name}
|
||||
onSelect={() => onSelectProvider(provider.id)}
|
||||
selected={selectedProvider === provider.id}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ProviderCardProps {
|
||||
setSelected: () => void;
|
||||
onSelect: () => void;
|
||||
selected: boolean;
|
||||
icon: JSX.Element;
|
||||
name: string;
|
||||
}
|
||||
|
||||
function ProviderCard(props: ProviderCardProps) {
|
||||
const { selected, setSelected, icon, name } = props;
|
||||
function ProviderCard({ selected, onSelect, icon, name }: ProviderCardProps) {
|
||||
const cardClassName = useMemo(() => {
|
||||
const baseClasses = 'flex flex-col justify-center items-center gap-y-3 py-2 px-4 hover:bg-component-bg-hover active:bg-component-bg-pressed rounded-lg cursor-pointer transition-colors';
|
||||
return selected ? `${baseClasses} bg-component-bg` : baseClasses;
|
||||
}, [selected]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex flex-col justify-center items-center gap-y-3 py-2 px-4 hover:bg-component-bg-hover active:bg-component-bg-pressed rounded-lg ${selected ? 'bg-component-bg' : ''}`}
|
||||
onClick={() => setSelected()}
|
||||
className={cardClassName}
|
||||
onClick={onSelect}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
onSelect();
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className="h-8 w-8 flex items-center justify-center [&>svg]:(w-8 h-8!)">
|
||||
{icon}
|
||||
</div>
|
||||
<span>{name}</span>
|
||||
<span className="text-sm font-medium">{name}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -253,6 +341,7 @@ function ClusterInformation({
|
|||
<div className="flex flex-col gap-y-4">
|
||||
<Option header="Name">
|
||||
<TextField
|
||||
aria-label="Cluster Name"
|
||||
className="w-full"
|
||||
iconLeft={<TextInputIcon className="size-4" />}
|
||||
onChange={e => onClusterNameChange(e.target.value)}
|
||||
|
|
@ -296,61 +385,62 @@ interface VersionReleaseFilters {
|
|||
snapshot: boolean;
|
||||
}
|
||||
|
||||
function VersionSelector(props: VersionSelectorProps) {
|
||||
const { versions, selectedVersion, onChange } = props;
|
||||
const [filteredVersions, setFilteredVersions] = useState<Array<Version>>([]);
|
||||
const [filters, setFilters] = useState<VersionReleaseFilters>({
|
||||
old_alpha: false,
|
||||
old_beta: false,
|
||||
release: true,
|
||||
snapshot: false,
|
||||
});
|
||||
const DEFAULT_FILTERS: VersionReleaseFilters = {
|
||||
old_alpha: false,
|
||||
old_beta: false,
|
||||
release: true,
|
||||
snapshot: false,
|
||||
};
|
||||
|
||||
const refresh = useCallback((filters: VersionReleaseFilters, versions: Array<Version>) => {
|
||||
setFilteredVersions(() => {
|
||||
if (Object.values(filters).every(value => value === false))
|
||||
return versions;
|
||||
function VersionSelector({ versions, selectedVersion, onChange }: VersionSelectorProps) {
|
||||
const [filters, setFilters] = useState<VersionReleaseFilters>(DEFAULT_FILTERS);
|
||||
|
||||
return versions.filter(version => filters[version.type]);
|
||||
});
|
||||
}, []);
|
||||
const filteredVersions = useMemo(() => {
|
||||
if (!versions)
|
||||
return [];
|
||||
|
||||
const toggleFilter = (name: keyof VersionReleaseFilters) => {
|
||||
if (Object.values(filters).every(value => !value))
|
||||
return versions;
|
||||
|
||||
return versions.filter(version => filters[version.type]);
|
||||
}, [versions, filters]);
|
||||
|
||||
const toggleFilter = useCallback((name: keyof VersionReleaseFilters) => {
|
||||
setFilters(prev => ({
|
||||
...prev,
|
||||
[name]: !prev[name],
|
||||
}));
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (versions)
|
||||
refresh(filters, versions);
|
||||
}, [filters, versions, refresh]);
|
||||
|
||||
useEffect(() => {
|
||||
if (versions)
|
||||
setFilteredVersions(versions);
|
||||
}, [versions]);
|
||||
const filterEntries = useMemo(() =>
|
||||
Object.entries(filters) as Array<[keyof VersionReleaseFilters, boolean]>, [filters]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 flex-row gap-2">
|
||||
<SelectList className="min-h-40 min-w-3/5 max-h-40 border border-component-border bg-component-bg" onChange={onChange} value={selectedVersion}>
|
||||
<SelectList
|
||||
className="min-h-40 min-w-3/5 max-h-40 border border-component-border bg-component-bg"
|
||||
onChange={onChange}
|
||||
value={selectedVersion}
|
||||
>
|
||||
<ScrollableContainer>
|
||||
{filteredVersions.map(version => (
|
||||
<SelectList.Item key={version.id} value={version.id}>{version.id}</SelectList.Item>
|
||||
<SelectList.Item key={version.id} value={version.id}>
|
||||
{version.id}
|
||||
</SelectList.Item>
|
||||
))}
|
||||
</ScrollableContainer>
|
||||
</SelectList>
|
||||
|
||||
<div className="flex flex-1 flex-col gap-y-2 overflow-hidden border border-component-border bg-component-bg rounded-lg p-2">
|
||||
{Object.keys(filters).map(filter => (
|
||||
{filterEntries.map(([filterName, isSelected]) => (
|
||||
<Checkbox
|
||||
className="selected:bg-component-bg-pressed rounded-lg"
|
||||
isSelected={filters[filter as VersionType]}
|
||||
key={filter}
|
||||
onChange={() => toggleFilter(filter as VersionType)}
|
||||
aria-label={`Filter ${filterName.replace('_', ' ')}`}
|
||||
className="selected:bg-component-bg-pressed rounded-lg p-1 text-sm"
|
||||
isSelected={isSelected}
|
||||
key={filterName}
|
||||
onChange={() => toggleFilter(filterName)}
|
||||
>
|
||||
{filter}
|
||||
<span className="capitalize">{filterName.replace('_', ' ')}</span>
|
||||
</Checkbox>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -363,11 +453,13 @@ type OptionProps = {
|
|||
children?: JSX.Element | Array<JSX.Element>;
|
||||
} & RefAttributes<HTMLDivElement>;
|
||||
|
||||
function Option(props: OptionProps) {
|
||||
function Option({ header, children, ...divProps }: OptionProps) {
|
||||
return (
|
||||
<div {...props} className="flex flex-col gap-y-2 items-stretch">
|
||||
<h3 className="text-left text-md text-fg-secondary font-semibold uppercase">{props.header}</h3>
|
||||
{props.children}
|
||||
<div {...divProps} className="flex flex-col gap-y-2 items-stretch">
|
||||
<h3 className="text-left text-md text-fg-secondary font-semibold uppercase">
|
||||
{header}
|
||||
</h3>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,68 @@
|
|||
import type { DialogProps } from 'react-aria-components';
|
||||
import type { VariantProps } from 'tailwind-variants';
|
||||
import {
|
||||
Dialog as AriaDialog,
|
||||
Modal as AriaModal,
|
||||
DialogTrigger,
|
||||
ModalOverlay,
|
||||
} from 'react-aria-components';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import { tv } from 'tailwind-variants';
|
||||
|
||||
interface ModalProps extends DialogProps { }
|
||||
const modalVariants = tv({
|
||||
slots: {
|
||||
overlay: [
|
||||
'fixed inset-0 z-50',
|
||||
'flex items-center justify-center',
|
||||
'bg-black/50 p-4',
|
||||
|
||||
'data-[entering]:animate-in data-[entering]:fade-in data-[entering]:duration-200',
|
||||
'data-[exiting]:animate-out data-[exiting]:fade-out data-[exiting]:duration-150',
|
||||
],
|
||||
modal: [
|
||||
'data-[entering]:animate-in data-[entering]:zoom-in-95 data-[entering]:duration-200',
|
||||
'data-[exiting]:animate-out data-[exiting]:zoom-out-95 data-[exiting]:duration-150',
|
||||
],
|
||||
dialog: [
|
||||
'outline-none',
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
export interface ModalProps extends DialogProps, VariantProps<typeof modalVariants> {
|
||||
isOpen?: boolean;
|
||||
onOpenChange?: (isOpen: boolean) => void;
|
||||
isDismissable?: boolean;
|
||||
overlayClassName?: string;
|
||||
modalClassName?: string;
|
||||
}
|
||||
|
||||
function Modal({
|
||||
className,
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
isDismissable = true,
|
||||
overlayClassName,
|
||||
modalClassName,
|
||||
...props
|
||||
}: ModalProps) {
|
||||
const { overlay, modal, dialog } = modalVariants();
|
||||
|
||||
const isControlled = isOpen !== undefined || onOpenChange !== undefined;
|
||||
|
||||
const overlayProps = {
|
||||
className: twMerge(overlay(), overlayClassName),
|
||||
isDismissable,
|
||||
...(isControlled && { isOpen, onOpenChange }),
|
||||
};
|
||||
|
||||
function Modal({ className, ...props }: ModalProps) {
|
||||
return (
|
||||
<ModalOverlay className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4" isDismissable>
|
||||
<AriaModal>
|
||||
<AriaDialog className="" {...props} />
|
||||
|
||||
<ModalOverlay {...overlayProps}>
|
||||
<AriaModal className={twMerge(modal(), modalClassName)}>
|
||||
<AriaDialog
|
||||
className={twMerge(dialog(), className)}
|
||||
{...props}
|
||||
/>
|
||||
</AriaModal>
|
||||
</ModalOverlay>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import type { ClusterModel, GameLoader, ManagedPackage, ManagedVersion, Paginated, Provider, SearchQuery, SearchResult } from '@/bindings.gen';
|
||||
import type { BindingCommands } from '@/types/global';
|
||||
import type { UndefinedInitialDataOptions } from '@tanstack/react-query';
|
||||
import { bindings } from '@/main';
|
||||
import { PROVIDERS } from '@/utils';
|
||||
import { useCommand } from '@onelauncher/common';
|
||||
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
export interface BrowserControllerType {
|
||||
cluster: ClusterModel | undefined;
|
||||
|
|
@ -29,19 +30,24 @@ export function BrowserProvider(props: any) {
|
|||
const { children } = props;
|
||||
const [cluster, setCluster] = useState<ClusterModel | undefined>(undefined);
|
||||
const [provider, setProvider] = useState<Provider>(PROVIDERS[0]);
|
||||
const navigate = useNavigate()
|
||||
const navigate = useNavigate();
|
||||
const [query, setQuery] = useState<SearchQuery>({
|
||||
filters: null,
|
||||
filters: {
|
||||
categories: null,
|
||||
game_versions: null,
|
||||
loaders: null,
|
||||
package_types: ["mod"]
|
||||
},
|
||||
query: null,
|
||||
limit: 18 as unknown as bigint,
|
||||
offset: null,
|
||||
sort: null,
|
||||
});
|
||||
|
||||
useEffect(()=>{
|
||||
if(query.filters?.categories || query.query)
|
||||
navigate({to: "/app/browser/search"})
|
||||
},[query])
|
||||
useEffect(() => {
|
||||
if (query.filters?.categories || query.query)
|
||||
navigate({ to: '/app/browser/search' });
|
||||
}, [query]);
|
||||
|
||||
const context = useMemo<BrowserControllerType>(() => ({
|
||||
cluster,
|
||||
|
|
@ -65,10 +71,10 @@ export function useBrowserSearch(provider: Provider, query: SearchQuery, options
|
|||
return useCommand('searchPackages', () => bindings.core.searchPackages(provider, validFilters ? query : { ...query, filters: null }), options);
|
||||
}
|
||||
|
||||
export function usePackageData(provider: Provider, slug: string, options?: Omit<UndefinedInitialDataOptions<ManagedPackage>, "queryKey" | "queryFn"> | undefined){
|
||||
return useCommand("getPackage", () => bindings.core.getPackage(provider, slug), options)
|
||||
export function usePackageData(provider: Provider, slug: string, options?: Omit<UndefinedInitialDataOptions<ManagedPackage>, 'queryKey' | 'queryFn'> | undefined, key: false | BindingCommands | (string & {}) = `getPackage.${provider}.${slug}`) {
|
||||
return useCommand(key, () => bindings.core.getPackage(provider, slug), options);
|
||||
}
|
||||
|
||||
export function usePackageVersions(provider: Provider, slug: string, {mc_versions, loaders, offset, limit, ...options}:{mc_versions?:Array<string>|null, loaders?:Array<GameLoader>|null, offset?:number, limit: number} & Omit<UndefinedInitialDataOptions<Paginated<ManagedVersion>>, "queryKey" | "queryFn">){
|
||||
return useCommand("getPackageVersions", ()=>bindings.core.getPackageVersions(provider, slug, mc_versions??null, loaders??null, (offset??0) as unknown as bigint, limit as unknown as bigint), options)
|
||||
export function usePackageVersions(provider: Provider, slug: string, { mc_versions, loaders, offset, limit, ...options }: { mc_versions?: Array<string> | null; loaders?: Array<GameLoader> | null; offset?: number; limit: number } & Omit<UndefinedInitialDataOptions<Paginated<ManagedVersion>>, 'queryKey' | 'queryFn'>, key: false | BindingCommands | (string & {}) = `getPackageVersions.${provider}.${slug}.${mc_versions?.join('')}.${loaders?.join('')}`) {
|
||||
return useCommand(key, () => bindings.core.getPackageVersions(provider, slug, mc_versions ?? null, loaders ?? null, (offset ?? 0) as unknown as bigint, limit as unknown as bigint), options);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,54 +0,0 @@
|
|||
import type { ClusterModel } from '@/bindings.gen';
|
||||
import { bindings } from '@/main';
|
||||
import { useCommand } from '@onelauncher/common';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export function useRecentCluster() {
|
||||
const result = useCommand('getClusters', bindings.core.getClusters);
|
||||
const [cluster, setCluster] = useState<ClusterModel | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!result.data)
|
||||
return;
|
||||
|
||||
let mostRecentCluster: ClusterModel | undefined;
|
||||
|
||||
for (const c of result.data) {
|
||||
if (!mostRecentCluster) {
|
||||
mostRecentCluster = c;
|
||||
continue;
|
||||
}
|
||||
|
||||
const currentPlayed = mostRecentCluster.last_played;
|
||||
const newPlayed = c.last_played;
|
||||
|
||||
if (typeof currentPlayed !== 'string' && typeof newPlayed === 'string') {
|
||||
mostRecentCluster = c;
|
||||
}
|
||||
else if (typeof currentPlayed === 'string' && typeof newPlayed === 'string') {
|
||||
const playedAt = new Date(currentPlayed);
|
||||
const clusterPlayedAt = new Date(newPlayed);
|
||||
|
||||
if (clusterPlayedAt > playedAt)
|
||||
mostRecentCluster = c;
|
||||
}
|
||||
}
|
||||
|
||||
setCluster(mostRecentCluster);
|
||||
}, [result.data]);
|
||||
|
||||
return cluster;
|
||||
}
|
||||
|
||||
export function useClusters() {
|
||||
const result = useCommand('getClusters', bindings.core.getClusters);
|
||||
const [clusters, setClusters] = useState<Array<ClusterModel> | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!result.data)
|
||||
return;
|
||||
setClusters(result.data);
|
||||
}, [result.data]);
|
||||
|
||||
return clusters;
|
||||
}
|
||||
164
apps/onelauncher/frontend/src/hooks/useCluster.tsx
Normal file
164
apps/onelauncher/frontend/src/hooks/useCluster.tsx
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
import type { ClusterModel } from '@/bindings.gen';
|
||||
import type { ModalProps } from '@/components/overlay/Modal';
|
||||
import { bindings } from '@/main';
|
||||
import { useCommand } from '@onelauncher/common';
|
||||
import { useEffect, useState } from 'react';
|
||||
import Modal from '@/components/overlay/Modal';
|
||||
import { Button, Dropdown } from '@onelauncher/common/components';
|
||||
import { Heading } from 'react-aria-components';
|
||||
|
||||
export function useRecentCluster() {
|
||||
const result = useCommand('getClusters', bindings.core.getClusters);
|
||||
const [cluster, setCluster] = useState<ClusterModel | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!result.data)
|
||||
return;
|
||||
|
||||
let mostRecentCluster: ClusterModel | undefined;
|
||||
|
||||
for (const c of result.data) {
|
||||
if (!mostRecentCluster) {
|
||||
mostRecentCluster = c;
|
||||
continue;
|
||||
}
|
||||
|
||||
const currentPlayed = mostRecentCluster.last_played;
|
||||
const newPlayed = c.last_played;
|
||||
|
||||
if (typeof currentPlayed !== 'string' && typeof newPlayed === 'string') {
|
||||
mostRecentCluster = c;
|
||||
}
|
||||
else if (typeof currentPlayed === 'string' && typeof newPlayed === 'string') {
|
||||
const playedAt = new Date(currentPlayed);
|
||||
const clusterPlayedAt = new Date(newPlayed);
|
||||
|
||||
if (clusterPlayedAt > playedAt)
|
||||
mostRecentCluster = c;
|
||||
}
|
||||
}
|
||||
|
||||
setCluster(mostRecentCluster);
|
||||
}, [result.data]);
|
||||
|
||||
return cluster;
|
||||
}
|
||||
|
||||
export function useClusters() {
|
||||
const result = useCommand('getClusters', bindings.core.getClusters);
|
||||
const [clusters, setClusters] = useState<Array<ClusterModel> | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!result.data)
|
||||
return;
|
||||
setClusters(result.data);
|
||||
}, [result.data]);
|
||||
|
||||
return clusters;
|
||||
}
|
||||
|
||||
type ChooseClusterModalProps = ModalProps & {
|
||||
clusters?: ClusterModel[];
|
||||
confirmText?: string;
|
||||
selected?: (clusters: ClusterModel[]) => number;
|
||||
onSelected?: (cluster: ClusterModel) => void;
|
||||
};
|
||||
|
||||
|
||||
export function ChooseClusterModal({clusters: clusterList, selected: defaultSelected, onSelected, confirmText, ...props}: ChooseClusterModalProps) {
|
||||
const clusters = useClusters()
|
||||
const [selected, setSelected] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!clusters) return;
|
||||
const newSelected = defaultSelected?.(clusters)
|
||||
if (newSelected) setSelected(newSelected)
|
||||
}, [clusters])
|
||||
|
||||
function chooseCluster() {
|
||||
if (clusters !== undefined && selected !== undefined) {
|
||||
const cluster = clusters[selected];
|
||||
if (cluster !== undefined)
|
||||
onSelected?.(cluster);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal {...props}>
|
||||
<div className="min-w-sm flex flex-col rounded-lg bg-page text-center p-4 gap-2">
|
||||
|
||||
<Heading slot='title' className='text-xl font-semibold'>Select a Cluster</Heading>
|
||||
<Dropdown
|
||||
onSelectionChange={(index) => {
|
||||
setSelected(index as number)
|
||||
}}
|
||||
selectedKey={selected}
|
||||
>
|
||||
{clusters?.map((cluster, index) => (
|
||||
<Dropdown.Item id={index} key={cluster.id}>
|
||||
{cluster.name}
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</Dropdown>
|
||||
|
||||
<div className='flex gap-2'>
|
||||
<Button
|
||||
color="secondary"
|
||||
children="Close"
|
||||
slot="close"
|
||||
/>
|
||||
<Button
|
||||
children={confirmText ?? "Save"}
|
||||
slot="close"
|
||||
onClick={chooseCluster}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
|
||||
// return (
|
||||
// <Modal
|
||||
// {...props}
|
||||
// buttons={[
|
||||
// <Button
|
||||
// buttonStyle="secondary"
|
||||
// children="Close"
|
||||
// onClick={props.hide}
|
||||
// />,
|
||||
// <Button
|
||||
// children="Save"
|
||||
// onClick={chooseCluster}
|
||||
// />,
|
||||
// ]}
|
||||
// children={(
|
||||
// <Switch>
|
||||
// <Match when={clusters() !== undefined}>
|
||||
// <Dropdown
|
||||
// disabled={clusters()?.length === 0}
|
||||
// onChange={setSelected}
|
||||
// selected={selected}
|
||||
// >
|
||||
// <Switch>
|
||||
// <Match when={(clusters()?.length || 0) > 0}>
|
||||
// <For each={clusters()!}>
|
||||
// {cluster => (
|
||||
// <Dropdown.Row>{cluster.meta.name}</Dropdown.Row>
|
||||
// )}
|
||||
// </For>
|
||||
// </Match>
|
||||
// <Match when>
|
||||
// <Dropdown.Row>No clusters found</Dropdown.Row>
|
||||
// </Match>
|
||||
// </Switch>
|
||||
// </Dropdown>
|
||||
// </Match>
|
||||
// <Match when>
|
||||
// <div>Loading...</div>
|
||||
// </Match>
|
||||
// </Switch>
|
||||
// )}
|
||||
// title="Choose a cluster"
|
||||
// />
|
||||
// );
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { Button, Show } from '@onelauncher/common/components';
|
||||
import { ChevronRightIcon, ChevronLeftIcon } from '@untitled-theme/icons-react';
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from '@untitled-theme/icons-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
export interface PaginationOptions {
|
||||
|
|
@ -16,7 +16,7 @@ function usePagination(options: PaginationOptions) {
|
|||
const next = () => setPage(page => page + 1);
|
||||
const prev = () => setPage(page => page - 1);
|
||||
|
||||
const totalPages = Math.ceil(options.itemsCount / options.itemsPerPage);
|
||||
const totalPages = Math.ceil(options.itemsCount / options.itemsPerPage);
|
||||
|
||||
const hasNext = page < totalPages;
|
||||
const hasPrev = page > 1;
|
||||
|
|
@ -24,12 +24,12 @@ function usePagination(options: PaginationOptions) {
|
|||
const offset = (page - 1) * options.itemsPerPage;
|
||||
|
||||
// Exported
|
||||
function reset(/*options: PaginationOptions*/) {
|
||||
function reset(/* options: PaginationOptions */) {
|
||||
setPage(1);
|
||||
}
|
||||
|
||||
function getVisiblePageNumbers(page: number) {
|
||||
const pages: number[] = [];
|
||||
const pages: Array<number> = [];
|
||||
|
||||
const left = page - 1 > 1 ? page - 1 : null;
|
||||
const right = page + 1 < totalPages ? page + 1 : null;
|
||||
|
|
@ -59,7 +59,7 @@ function usePagination(options: PaginationOptions) {
|
|||
setPage(props.page);
|
||||
}}
|
||||
>
|
||||
<span className='text-trim'>{props.page}</span>
|
||||
<span className="text-trim">{props.page}</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
|
@ -68,11 +68,11 @@ function usePagination(options: PaginationOptions) {
|
|||
<div className="flex flex-row items-center justify-center gap-x-1">
|
||||
|
||||
<Button
|
||||
size='icon'
|
||||
color='secondary'
|
||||
children={<ChevronLeftIcon />}
|
||||
color="secondary"
|
||||
isDisabled={page <= 1}
|
||||
onClick={() => setPage(page => page - 1)}
|
||||
onClick={prev}
|
||||
size="icon"
|
||||
/>
|
||||
|
||||
<PaginationBtn page={1} />
|
||||
|
|
@ -81,7 +81,7 @@ function usePagination(options: PaginationOptions) {
|
|||
<span>...</span>
|
||||
</Show>
|
||||
|
||||
{getVisiblePageNumbers(page).map(page=><PaginationBtn page={page} />)}
|
||||
{getVisiblePageNumbers(page).map(page => <PaginationBtn key={page} page={page} />)}
|
||||
|
||||
<Show when={page < totalPages - 2}>
|
||||
<span>...</span>
|
||||
|
|
@ -92,11 +92,11 @@ function usePagination(options: PaginationOptions) {
|
|||
</Show>
|
||||
|
||||
<Button
|
||||
size="icon"
|
||||
color='secondary'
|
||||
children={<ChevronRightIcon />}
|
||||
color="secondary"
|
||||
isDisabled={page >= totalPages}
|
||||
onClick={() => setPage(page => page + 1)}
|
||||
onClick={next}
|
||||
size="icon"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { isDev, registerDevExperience, registerNativeExperience } from '@onelauncher/common';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { createRouter, RouterProvider } from '@tanstack/react-router';
|
||||
import { createHashHistory, createRouter, RouterProvider } from '@tanstack/react-router';
|
||||
import { StrictMode } from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { createTauRPCProxy } from './bindings.gen';
|
||||
|
|
@ -25,6 +25,8 @@ const queryClient = new QueryClient({
|
|||
},
|
||||
});
|
||||
|
||||
const hashHistory = createHashHistory();
|
||||
|
||||
// Tanstack Router
|
||||
const router = createRouter({
|
||||
routeTree,
|
||||
|
|
@ -36,6 +38,7 @@ const router = createRouter({
|
|||
defaultStructuralSharing: true,
|
||||
defaultPreloadStaleTime: 0,
|
||||
defaultNotFoundComponent: () => <div>Not Found</div>,
|
||||
history: hashHistory,
|
||||
});
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
import type { Provider } from '@/bindings.gen';
|
||||
import OneConfigLogo from '@/assets/logos/oneconfig.svg';
|
||||
import { PackageGrid } from '@/components/content/PackageItem';
|
||||
import { useBrowserContext, useBrowserSearch } from '@/hooks/useBrowser';
|
||||
import { useBrowserSearch, usePackageData } from '@/hooks/useBrowser';
|
||||
import { PROVIDERS } from '@/utils';
|
||||
import { Button, Show } from '@onelauncher/common/components';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||
import { ChevronRightIcon } from '@untitled-theme/icons-react';
|
||||
import { useState } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { BrowserLayout } from './route';
|
||||
|
||||
export const Route = createFileRoute('/app/browser/')({
|
||||
component: RouteComponent
|
||||
component: RouteComponent,
|
||||
|
||||
});
|
||||
|
||||
|
|
@ -26,17 +26,22 @@ function RouteComponent() {
|
|||
}
|
||||
|
||||
function Featured() {
|
||||
const context = useBrowserContext();
|
||||
// const context = useBrowserContext();
|
||||
const [provider, slug] = useMemo<[Provider, string]>(() => ['Modrinth', 'iris'], []);
|
||||
const featuredPackage = usePackageData(provider, slug, {});
|
||||
const navigate = useNavigate();
|
||||
const featuredImageIndex = useMemo(() => featuredPackage.data?.gallery.findIndex(image => image.featured), [featuredPackage.data]);
|
||||
const [selectedImage, setSelectedImage] = useState(featuredImageIndex ?? 0);
|
||||
return (
|
||||
<Show when={!context.query.query && !(context.query.filters?.categories && context.query.filters.categories.length > 0)}>
|
||||
<Show when={featuredPackage.isSuccess}>
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<h5 className="ml-2">Featured</h5>
|
||||
<div className="w-full flex flex-row overflow-hidden rounded-lg bg-page-elevated">
|
||||
<h5 className="ml-2 uppercase opacity-60">Featured</h5>
|
||||
<div className="w-full flex flex-col lg:flex-row overflow-hidden rounded-lg bg-page-elevated">
|
||||
<div className="w-full p-1">
|
||||
<img alt="thumbnail" className="aspect-ratio-video h-full rounded-md object-cover object-center" src="https://cdn.modrinth.com/data/AANobbMI/295862f4724dc3f78df3447ad6072b2dcd3ef0c9_96.webp" />
|
||||
<img alt="thumbnail" className="aspect-video h-full w-full rounded-md object-cover object-center" src={featuredPackage.data?.gallery[selectedImage].url} />
|
||||
</div>
|
||||
<div className="max-w-84 min-w-52 flex flex-col gap-y-1 p-4">
|
||||
<h2>Sodium</h2>
|
||||
<div className="max-w-64 min-w-52 flex flex-col gap-y-1 p-4">
|
||||
<h2 className="text-[1.5rem] font-semibold">{featuredPackage.data?.name}</h2>
|
||||
|
||||
<Show when={false}>
|
||||
<div className="w-fit flex flex-row items-center gap-x-1 rounded-lg bg-border/10 px-1.5 py-1 text-fg-primary transition hover:opacity-80">
|
||||
|
|
@ -45,11 +50,24 @@ function Featured() {
|
|||
</div>
|
||||
</Show>
|
||||
|
||||
<p className="mt-1 flex-1 leading-normal">Peak mod</p>
|
||||
<p className="my-1 flex-1 leading-normal">{featuredPackage.data?.short_desc}</p>
|
||||
|
||||
<div className="grid grid-cols-3 gap-2 h-full">
|
||||
{featuredPackage.data?.gallery.map(
|
||||
(image, index) => (
|
||||
<img
|
||||
className="aspect-video object-center object-cover rounded-md"
|
||||
key={image.url}
|
||||
onClick={() => setSelectedImage(index)}
|
||||
src={image.url}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row justify-end">
|
||||
<Button color="ghost">
|
||||
View Package
|
||||
<Button color="ghost" onClick={() => navigate({ to: '/app/browser/package/$provider/$slug', params: { provider, slug } })}>
|
||||
View
|
||||
<ChevronRightIcon />
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -73,7 +91,7 @@ function Lists() {
|
|||
function List({ provider }: { provider: Provider }) {
|
||||
const search = useBrowserSearch(provider, {
|
||||
filters: null,
|
||||
limit: 18 as unknown as bigint,
|
||||
limit: 24 as unknown as bigint,
|
||||
offset: null,
|
||||
query: null,
|
||||
sort: null,
|
||||
|
|
@ -81,7 +99,7 @@ function List({ provider }: { provider: Provider }) {
|
|||
const [expanded, setExpanded] = useState(false);
|
||||
return (
|
||||
<div>
|
||||
<h3 className="uppercase p-3 opacity-60">{provider}</h3>
|
||||
<h5 className="uppercase m-2 opacity-60">{provider}</h5>
|
||||
<div className={`relative overflow-hidden ${expanded ? '' : 'h-128'}`}>
|
||||
{search.isSuccess
|
||||
? <PackageGrid items={search.data.items} provider={provider} />
|
||||
|
|
|
|||
|
|
@ -1,84 +1,137 @@
|
|||
import type { ClusterModel, ManagedPackage, ManagedUser, ManagedVersion, Paginated, Provider } from '@/bindings.gen'
|
||||
import { useBrowserContext, usePackageData, usePackageVersions } from '@/hooks/useBrowser'
|
||||
import { useClusters } from '@/hooks/useCluster'
|
||||
import { bindings } from '@/main'
|
||||
import { abbreviateNumber, formatAsRelative, PROVIDERS } from '@/utils'
|
||||
import { useCommand } from '@onelauncher/common'
|
||||
import { Show, Tooltip, Button, Dropdown } from '@onelauncher/common/components'
|
||||
import { createFileRoute, Link } from '@tanstack/react-router'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import { Download01Icon, LinkExternal01Icon, File02Icon, CalendarIcon, ClockRewindIcon, ChevronDownIcon, ChevronUpIcon } from '@untitled-theme/icons-react'
|
||||
import { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Collection, ListBox, ListBoxItem, Popover, Select } from 'react-aria-components'
|
||||
|
||||
import type { ClusterModel, ManagedPackage, ManagedUser, ManagedVersion, PackageDonationUrl, Provider } from '@/bindings.gen';
|
||||
import type { HTMLProps } from 'react';
|
||||
import Modal from '@/components/overlay/Modal';
|
||||
import { useBrowserContext, usePackageData, usePackageVersions } from '@/hooks/useBrowser';
|
||||
import { ChooseClusterModal, useClusters } from '@/hooks/useCluster';
|
||||
import { bindings } from '@/main';
|
||||
import { abbreviateNumber, formatAsRelative, PROVIDERS, upperFirst } from '@/utils';
|
||||
import { useCommand } from '@onelauncher/common';
|
||||
import { Button, Show, Tooltip } from '@onelauncher/common/components';
|
||||
import { createFileRoute, Link } from '@tanstack/react-router';
|
||||
import { openUrl } from '@tauri-apps/plugin-opener';
|
||||
import { CalendarIcon, ChevronDownIcon, ChevronUpIcon, ClockRewindIcon, Download01Icon, File02Icon, LinkExternal01Icon } from '@untitled-theme/icons-react';
|
||||
import { createContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Cell, Collection, Column, ListBox, ListBoxItem, Popover, Pressable, Row, Select, Tab, Table, TableBody, TableHeader, TabList, TabPanel, Tabs } from 'react-aria-components';
|
||||
import Markdown from 'react-markdown';
|
||||
import rehypeRaw from 'rehype-raw';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import usePagination from '@/hooks/usePagination';
|
||||
|
||||
export const Route = createFileRoute('/app/browser/package/$provider/$slug')({
|
||||
component: RouteComponent,
|
||||
})
|
||||
});
|
||||
|
||||
function includes<T, C extends T>(list: { includes: (arg0: C) => boolean }, element: T): element is C {
|
||||
return list.includes(element as unknown as C)
|
||||
function includes<T, TArray extends T>(list: { includes: (arg0: TArray) => boolean }, element: T): element is TArray {
|
||||
return list.includes(element as unknown as TArray);
|
||||
}
|
||||
|
||||
type PackageContextType = {
|
||||
pkg: ManagedPackage | undefined
|
||||
versions: Paginated<ManagedVersion> | undefined
|
||||
function CustomA({ href, children, includeIcon, className, ...rest }: { href: string; children: any, includeIcon?: boolean } & HTMLProps<HTMLAnchorElement>) {
|
||||
return (
|
||||
<a
|
||||
className={twMerge("text-fg-primary underline", className)}
|
||||
href={href}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
openUrl(href);
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
{includeIcon && <LinkExternal01Icon className='inline w-4 ml-1' />}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
interface PackageContextType {
|
||||
pkg: ManagedPackage | undefined;
|
||||
}
|
||||
|
||||
const PackageContext = createContext<PackageContextType>({
|
||||
pkg: undefined,
|
||||
versions: undefined
|
||||
})
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const { provider, slug } = Route.useParams()
|
||||
if (!includes(PROVIDERS, provider)) throw new Error("Invalid provider");
|
||||
const packageData = usePackageData(provider, slug)
|
||||
const browserContext = useBrowserContext()
|
||||
const { data: versions } = usePackageVersions(provider, slug, {
|
||||
mc_versions: browserContext.cluster ? [browserContext.cluster.mc_version] : null,
|
||||
loaders: browserContext.cluster ? [browserContext.cluster.mc_loader] : null,
|
||||
limit: 50
|
||||
})
|
||||
const { provider, slug } = Route.useParams();
|
||||
if (!includes(PROVIDERS, provider))
|
||||
throw new Error('Invalid provider');
|
||||
const packageData = usePackageData(provider, slug, {});
|
||||
const browserContext = useBrowserContext();
|
||||
|
||||
return (
|
||||
<PackageContext value={{ pkg: packageData.data, versions }}>
|
||||
<PackageContext value={{ pkg: packageData.data }}>
|
||||
<Show when={packageData.isSuccess && !packageData.isFetching}>
|
||||
<div className="h-full flex flex-1 flex-row items-start gap-x-4">
|
||||
<BrowserSidebar package={packageData.data!} />
|
||||
|
||||
<div className="min-h-full flex flex-1 flex-col items-start gap-y-4 pb-8">
|
||||
<div className="flex flex-none flex-row gap-x-1 rounded-lg bg-component-bg p-1">
|
||||
<Link to='/app/browser/package/$provider/$slug' params={{ provider, slug }}>About</Link>
|
||||
<Link to='/app/browser/package/$provider/$slug' params={{ provider, slug }}>Versions</Link>
|
||||
<Tabs className="min-h-full flex flex-1 flex-col items-start gap-y-4 pb-8">
|
||||
<TabList className="flex flex-none flex-row gap-x-1 rounded-lg bg-component-bg p-1 w-min">
|
||||
<Tab className="uppercase p-2.5 text-trim rounded-md selected:bg-component-bg-pressed disabled:hidden" id="about">About</Tab>
|
||||
<Tab className="uppercase p-2.5 text-trim rounded-md selected:bg-component-bg-pressed disabled:hidden" id="versions">Versions</Tab>
|
||||
<Tab className="uppercase p-2.5 text-trim rounded-md selected:bg-component-bg-pressed disabled:hidden" id="gallery" isDisabled={packageData.data?.gallery.length === 0}>Gallery</Tab>
|
||||
</TabList>
|
||||
<TabPanel className="prose prose-invert prose-sm max-w-none prose-code:before:content-none prose-code:after:content-none prose-code:bg-component-bg-disabled prose-code:rounded-sm prose-code:p-1! prose-code:text-trim prose-img:inline-block prose-a:inline-block" id="about">
|
||||
<div className="h-full min-h-full flex-1 w-full rounded-lg bg-component-bg p-3">
|
||||
<Markdown
|
||||
components={{
|
||||
a: ({ node, children, ...props }) => <CustomA children={children} href={props.href as string} />,
|
||||
}}
|
||||
rehypePlugins={[rehypeRaw]}
|
||||
remarkPlugins={[remarkGfm]}
|
||||
>
|
||||
{packageData.data?.body}
|
||||
</Markdown>
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel id="versions">
|
||||
<Versions/>
|
||||
</TabPanel>
|
||||
<TabPanel className="flex flex-wrap gap-2 justify-center" id="gallery">
|
||||
|
||||
{packageData.data?.gallery.map(item => (
|
||||
<Modal.Trigger key={item.url}>
|
||||
<Pressable>
|
||||
<div className="rounded-md overflow-hidden bg-component-bg-pressed outline-0 h-64 flex flex-col relative" aria-label='Loaders'>
|
||||
<img className="h-full" src={item.thumbnail_url} />
|
||||
{item.title && <div className="absolute w-full bottom-0 bg-component-bg-disabled/80 p-2">{item.title}</div>}
|
||||
</div>
|
||||
</Pressable>
|
||||
<Modal className="w-full">
|
||||
<img className="rounded-xl" src={item.url} />
|
||||
</Modal>
|
||||
</Modal.Trigger>
|
||||
))}
|
||||
|
||||
</div>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
<div className="h-full min-h-full w-full flex-1">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</PackageContext>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function getPackageUrl(pkg: ManagedPackage): string {
|
||||
switch (pkg.provider) {
|
||||
case 'Modrinth': return `https://modrinth.com/project/${pkg.slug}`
|
||||
case 'Curseforge': return `https://www.curseforge.com/minecraft/${pkg.package_type}s/${pkg.slug}`
|
||||
case 'SkyClient': return ``
|
||||
}
|
||||
}
|
||||
|
||||
function BrowserSidebar({ package: pkg }: { package: ManagedPackage }) {
|
||||
const { provider } = Route.useParams()
|
||||
const { provider } = Route.useParams();
|
||||
|
||||
const createdAt = useMemo(() => pkg.created ? new Date(pkg.created) : null, []);
|
||||
const updatedAt = useMemo(() => pkg.updated ? new Date(pkg.updated) : null, []);
|
||||
|
||||
const authors = useCommand("getUsersFromAuthor", () => bindings.core.getUsersFromAuthor(provider as Provider, pkg.author))
|
||||
const authors = useCommand('getUsersFromAuthor', () => bindings.core.getUsersFromAuthor(provider as Provider, pkg.author));
|
||||
|
||||
return (
|
||||
<div className="sticky top-0 z-1 max-w-60 min-w-54 flex flex-col gap-y-4">
|
||||
<div className="min-h-72 flex flex-col overflow-hidden rounded-lg bg-component-bg">
|
||||
<div className="z-1 max-w-60 min-w-54 flex flex-col gap-y-4 mb-6">
|
||||
<div className="flex flex-col overflow-hidden rounded-lg bg-component-bg">
|
||||
<div className="relative h-28 flex items-center justify-center overflow-hidden">
|
||||
<img alt={`Icon for ${pkg.name}`} className="absolute z-0 max-w-none w-7/6 filter-blur-xl" src={pkg.icon_url || ''} />
|
||||
<img alt={`Icon for ${pkg.name}`} className="absolute z-0 max-w-none w-7/6 blur-xl" src={pkg.icon_url || ''} />
|
||||
<img alt={`Icon for ${pkg.name}`} className="relative z-1 aspect-ratio-square w-2/5 rounded-md image-render-auto" src={pkg.icon_url || ''} />
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col gap-2 p-3">
|
||||
|
|
@ -106,22 +159,30 @@ function BrowserSidebar({ package: pkg }: { package: ManagedPackage }) {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<InstallButton {...pkg} />
|
||||
<InstallButton />
|
||||
|
||||
{/* <div className="flex flex-col gap-2 rounded-lg bg-component-bg p-3">
|
||||
<div className="flex flex-col rounded-lg bg-component-bg p-3">
|
||||
<h4 className="text-fg-primary font-bold">Links</h4>
|
||||
<Link href={getPackageUrl(contentPackage)} includeIcon>
|
||||
{contentPackage.provider}
|
||||
{' '}
|
||||
Page
|
||||
</Link>
|
||||
</div> */}
|
||||
<div className='flex flex-col'>
|
||||
<CustomA href={getPackageUrl(pkg)} includeIcon className='text-link hover:text-link-hover'>
|
||||
{provider}
|
||||
{' '}
|
||||
Page
|
||||
</CustomA>
|
||||
{(Object.entries(pkg.links) as [keyof typeof pkg.links, typeof pkg.links[keyof typeof pkg.links]][]).filter(a => a[1]).map(link =>
|
||||
typeof link[1] == "string"
|
||||
? <CustomA href={link[1]} children={upperFirst(link[0])} className='text-link hover:text-link-hover' includeIcon />
|
||||
: (link[1] as PackageDonationUrl[]).map(donationLink => <CustomA href={donationLink.url} className='text-link hover:text-link-hover' children={upperFirst(donationLink.id)} includeIcon />)
|
||||
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 rounded-lg bg-component-bg p-3">
|
||||
<h4 className="text-fg-primary font-bold">Authors</h4>
|
||||
{
|
||||
authors.isSuccess
|
||||
? authors.data.map(author => <Author author={author} />)
|
||||
? authors.data.map(author => <Author author={author} key={author.id} />)
|
||||
: <h3>Loading...</h3>
|
||||
}
|
||||
|
||||
|
|
@ -133,7 +194,7 @@ function BrowserSidebar({ package: pkg }: { package: ManagedPackage }) {
|
|||
<div className="flex flex-row items-start gap-x-1">
|
||||
<File02Icon className="h-3 min-w-3 w-3" />
|
||||
License
|
||||
<Link to={pkg.license?.url ?? "#"}>
|
||||
<Link to={pkg.license?.url ?? '#'}>
|
||||
{pkg.license?.name || pkg.license?.id || 'Unknown'}
|
||||
</Link>
|
||||
</div>
|
||||
|
|
@ -168,103 +229,104 @@ function BrowserSidebar({ package: pkg }: { package: ManagedPackage }) {
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
function colorForType(type: string) {
|
||||
switch (type.toLowerCase()) {
|
||||
case 'release':
|
||||
return 'bg-code-trace';
|
||||
case 'snapshot':
|
||||
return 'bg-code-debug';
|
||||
case 'beta':
|
||||
return 'bg-code-warn';
|
||||
case 'alpha':
|
||||
return 'bg-code-error';
|
||||
default:
|
||||
return 'bg-border/05';
|
||||
}
|
||||
}
|
||||
|
||||
function InstallButton({ ...pkg }: ManagedPackage) {
|
||||
const triggerRef = useRef<HTMLDivElement>(null)
|
||||
const { provider, slug } = Route.useParams()
|
||||
const [open, setOpen] = useState(false)
|
||||
const clusters = useClusters()
|
||||
const browserContext = useBrowserContext()
|
||||
const { versions } = useContext(PackageContext)
|
||||
function InstallButton() {
|
||||
const triggerRef = useRef<HTMLDivElement>(null);
|
||||
const { provider, slug } = Route.useParams();
|
||||
if(!includes(PROVIDERS, provider)) throw new Error("invalid provider")
|
||||
const [open, setOpen] = useState(false);
|
||||
const clusters = useClusters();
|
||||
const browserContext = useBrowserContext();
|
||||
const { data: versions } = usePackageVersions(provider, slug, {
|
||||
mc_versions: browserContext.cluster ? [browserContext.cluster.mc_version] : [],
|
||||
loaders: browserContext.cluster ? [browserContext.cluster.mc_loader] : [],
|
||||
limit: 1
|
||||
});
|
||||
const version = useMemo(() => {
|
||||
if (!versions || !browserContext.cluster) return undefined;
|
||||
return versions.items.findLast(version =>
|
||||
version.mc_versions.includes(browserContext.cluster!.mc_version)
|
||||
&& version.loaders.includes(browserContext.cluster!.mc_loader)
|
||||
)
|
||||
}, [browserContext.cluster, versions])
|
||||
if (!versions || !browserContext.cluster)
|
||||
return undefined;
|
||||
return versions.items[0]
|
||||
}, [browserContext.cluster, versions]);
|
||||
|
||||
function download() {
|
||||
if (!version || !browserContext.cluster || !includes(PROVIDERS, provider)) return false;
|
||||
downloadPackage(browserContext.cluster, provider, version)
|
||||
if (!version || !browserContext.cluster || !includes(PROVIDERS, provider))
|
||||
return false;
|
||||
downloadPackage(browserContext.cluster, provider, version);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
|
||||
<div className="h-12 flex flex-row w-full relative">
|
||||
<Button
|
||||
color={version ? "primary" : "secondary"}
|
||||
className="max-w-full flex-1 rounded-r-none!"
|
||||
onClick={download}
|
||||
className="max-w-full flex-1 rounded-r-none disabled:text-white/50 disabled:bg-blue-900"
|
||||
color="primary"
|
||||
isDisabled={!version}
|
||||
onClick={download}
|
||||
>
|
||||
<Download01Icon />
|
||||
<div className='w-full text-sm'>
|
||||
<div className="w-full text-sm">
|
||||
{
|
||||
browserContext.cluster ?
|
||||
version
|
||||
? <span>Download latest to <br /><span className='text-md font-semibold'>{browserContext.cluster.name}</span></span>
|
||||
browserContext.cluster
|
||||
? version
|
||||
? (
|
||||
<span>
|
||||
Download latest to
|
||||
<br />
|
||||
<span className="text-md font-semibold">{browserContext.cluster.name}</span>
|
||||
</span>
|
||||
)
|
||||
: `No matching version found`
|
||||
: "Select a Cluster"
|
||||
: 'Select a Cluster'
|
||||
}
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
<Button className="w-8 h-full rounded-l-none border-l border-white/10" onClick={() => setOpen(true)}>
|
||||
<Button className="w-8 h-full rounded-l-none border-l border-white/10 px-2" onClick={() => setOpen(true)}>
|
||||
{open
|
||||
? <ChevronUpIcon />
|
||||
: <ChevronDownIcon />
|
||||
}
|
||||
: <ChevronDownIcon />}
|
||||
</Button>
|
||||
<Select onOpenChange={setOpen} isOpen={open}
|
||||
aria-label='cluster'
|
||||
onSelectionChange={e => {
|
||||
const cluster = clusters?.find(cluster => cluster.id as unknown as number == e)
|
||||
if (cluster) browserContext.setCluster(cluster)
|
||||
}}>
|
||||
<div className="w-full h-full absolute -z-10" ref={triggerRef}></div>
|
||||
<Select
|
||||
aria-label="cluster" isOpen={open}
|
||||
onOpenChange={setOpen}
|
||||
onSelectionChange={(e) => {
|
||||
const cluster = clusters?.find(cluster => cluster.id as unknown as number === e);
|
||||
if (cluster)
|
||||
browserContext.setCluster(cluster);
|
||||
}}
|
||||
>
|
||||
<div className="w-full h-full absolute -z-10" ref={triggerRef}></div>
|
||||
|
||||
<Popover triggerRef={triggerRef} className="mt-1 rounded-lg shadow-md bg-component-bg border border-component-border -translate-x-full" style={{ width: `${triggerRef.current?.clientWidth}px` }}>
|
||||
<Popover className="mt-1 rounded-lg shadow-md bg-component-bg border border-component-border -translate-x-full" style={{ width: `${triggerRef.current?.clientWidth}px` }} triggerRef={triggerRef}>
|
||||
<ListBox className="outline-none flex flex-col gap-0.5">
|
||||
<Collection items={clusters}>
|
||||
{item => <ListBoxItem id={item.id as unknown as number}
|
||||
className="group/item flex flex-row items-center justify-between gap-2 rounded-lg p-2 w-full text-dropdown-item-text hover:bg-component-bg-hover data-[focused]:bg-component-bg-pressed">{item.name}</ListBoxItem>}
|
||||
{item => (
|
||||
<ListBoxItem
|
||||
className="group/item flex flex-row items-center justify-between gap-2 rounded-lg p-2 w-full text-dropdown-item-text hover:bg-component-bg-hover data-[focused]:bg-component-bg-pressed"
|
||||
id={item.id as unknown as number}
|
||||
>
|
||||
{item.name}
|
||||
</ListBoxItem>
|
||||
)}
|
||||
</Collection>
|
||||
</ListBox>
|
||||
</Popover>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function Author({ author }: { author: ManagedUser }) {
|
||||
|
||||
//TODO: onclick and fallback avatar url
|
||||
// TODO: onclick and fallback avatar url
|
||||
return (
|
||||
<a
|
||||
className="flex flex-row items-center gap-x-1 rounded-md p-1 active:bg-component-bg-pressed hover:bg-component-bg-hover"
|
||||
onClick={() => { if (author.url) openUrl(author.url) }}
|
||||
onClick={() => {
|
||||
if (author.url)
|
||||
openUrl(author.url);
|
||||
}}
|
||||
>
|
||||
<img alt={`${author.username}'s avatar`} className="h-8 min-h-8 min-w-8 w-8 rounded-[5px]" src={author.avatar_url || ""} />
|
||||
<img alt={`${author.username}'s avatar`} className="h-8 min-h-8 min-w-8 w-8 rounded-[5px]" src={author.avatar_url || ''} />
|
||||
<div className="flex flex-1 flex-col justify-center gap-y-1">
|
||||
<span>{author.username}</span>
|
||||
|
||||
|
|
@ -279,9 +341,96 @@ function Author({ author }: { author: ManagedUser }) {
|
|||
<LinkExternal01Icon className="h-4 w-4" />
|
||||
</a>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
function colorForType(type: string) {
|
||||
switch (type.toLowerCase()) {
|
||||
case 'release':
|
||||
return 'bg-code-trace/30';
|
||||
case 'snapshot':
|
||||
return 'bg-code-debug/30';
|
||||
case 'beta':
|
||||
return 'bg-code-warn/30';
|
||||
case 'alpha':
|
||||
return 'bg-code-error/30';
|
||||
default:
|
||||
return 'bg-border/30';
|
||||
}
|
||||
}
|
||||
|
||||
function Versions(){
|
||||
const {provider, slug} = Route.useParams()
|
||||
if(!includes(PROVIDERS, provider)) throw new Error("invalid provider")
|
||||
const [offset, setOffset] = useState(0)
|
||||
const { data: versions } = usePackageVersions(provider, slug, {
|
||||
limit: 20,
|
||||
offset
|
||||
});
|
||||
const pagination = usePagination({
|
||||
itemsCount: versions?.total as unknown as number,
|
||||
itemsPerPage: versions?.limit as unknown as number
|
||||
})
|
||||
useEffect(()=>{
|
||||
setOffset(pagination.offset)
|
||||
},[pagination.offset])
|
||||
useEffect(()=>{
|
||||
pagination.reset()
|
||||
},[versions?.total])
|
||||
return <div className='flex flex-col'>
|
||||
<div className='flex justify-between'>
|
||||
<pagination.Navigation/>
|
||||
</div>
|
||||
<Table className="border-separate border-spacing-x-none border-spacing-y-1">
|
||||
<TableHeader className="text-left">
|
||||
<Column className="pr-2"/>
|
||||
<Column className="pr-4" isRowHeader>Name</Column>
|
||||
<Column className="pr-4">Game Versions</Column>
|
||||
<Column className="pr-4">Loaders</Column>
|
||||
<Column className="pr-4">Created</Column>
|
||||
<Column className="pr-4">Downloads</Column>
|
||||
<Column className="pr-4"/>
|
||||
</TableHeader>
|
||||
<TableBody className="">
|
||||
{versions?.items.map(item=>
|
||||
<VersionRow version={item}/>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
}
|
||||
|
||||
function VersionRow({version}:{version:ManagedVersion}){
|
||||
const {provider} = Route.useParams()
|
||||
if(!includes(PROVIDERS, provider)) throw new Error("invalid provider")
|
||||
return (
|
||||
<Row className="my-2 bg-page-elevated px-4 [&>td]:py-4">
|
||||
<Cell className="p-4 my-2 bg-component-bg rounded-l-xl">
|
||||
<Tooltip text={upperFirst(version.release_type)}>
|
||||
<div className={`${colorForType(version.release_type)} h-8 w-8 flex items-center justify-center rounded-md`}>
|
||||
<span className="font-bold">{version.release_type.charAt(0).toUpperCase()}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Cell>
|
||||
<Cell className="p-4 px-2 bg-component-bg">{version.display_name}</Cell>
|
||||
<Cell className="p-4 pl-0 bg-component-bg">{version.mc_versions.join(", ")}</Cell>
|
||||
<Cell className="p-4 pl-0 bg-component-bg">{version.loaders.map(upperFirst).join(", ")}</Cell>
|
||||
<Cell className="p-4 pl-0 bg-component-bg">{formatAsRelative(Date.parse(version.published))}</Cell>
|
||||
<Cell className="p-4 pl-0 bg-component-bg">{version.downloads}</Cell>
|
||||
<Cell className="p-4 pl-0 bg-component-bg rounded-r-xl">
|
||||
<Modal.Trigger>
|
||||
<Button
|
||||
size='icon'
|
||||
color='secondary'
|
||||
children={<Download01Icon />}
|
||||
/>
|
||||
<ChooseClusterModal confirmText='Download' onSelected={cluster=>downloadPackage(cluster, provider, version, true)}/>
|
||||
</Modal.Trigger>
|
||||
</Cell>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
function downloadPackage(cluster: ClusterModel, provider: Provider, version: ManagedVersion, skipCompatibility = false) {
|
||||
return bindings.core.downloadPackage(provider, version.project_id, version.version_id, cluster.id, skipCompatibility)
|
||||
return bindings.core.downloadPackage(provider, version.project_id, version.version_id, cluster.id, skipCompatibility);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,9 +18,9 @@ function RouteComponent() {
|
|||
<BrowserProvider>
|
||||
<div>
|
||||
<AnimatedOutlet
|
||||
enter={{animate: {opacity: 1}, initial: {opacity: 1}}}
|
||||
exit={{animate: {opacity: 0}, initial: {opacity: 0}}}
|
||||
from={Route.id}
|
||||
enter={{ animate: { opacity: 1 }, initial: { opacity: 1 } }}
|
||||
exit={{ animate: { opacity: 0 }, initial: { opacity: 0 } }}
|
||||
from={Route.id}
|
||||
/>
|
||||
</div>
|
||||
</BrowserProvider>
|
||||
|
|
@ -30,14 +30,14 @@ function RouteComponent() {
|
|||
export function BrowserLayout(props: any) {
|
||||
return (
|
||||
<div className="relative h-full flex flex-1 flex-col items-center gap-2">
|
||||
<div className="h-full w-full max-w-screen-xl flex flex-1 flex-col items-center gap-y-2">
|
||||
<div className="h-full w-full max-w-screen-2xl flex flex-1 flex-col items-center gap-y-2">
|
||||
<div className="grid grid-cols-[220px_auto_220px] w-full gap-x-6">
|
||||
<div />
|
||||
<BrowserToolbar />
|
||||
<div />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-[220px_auto_220px] w-full max-w-screen-xl gap-x-6 pb-8">
|
||||
<div className="grid grid-cols-[220px_auto_220px] w-full gap-x-6 pb-8">
|
||||
<BrowserCategories />
|
||||
|
||||
<div className="h-full flex flex-col gap-y-4">
|
||||
|
|
@ -107,7 +107,7 @@ const defaultFilters: Filters = {
|
|||
|
||||
function BrowserCategories() {
|
||||
const context = useBrowserContext();
|
||||
const categories = browserCategories.byPackageType('mod', context.provider);
|
||||
const categories = browserCategories.byPackageType((context.query.filters?.package_types ?? ["mod"])[0], context.provider);
|
||||
|
||||
function switchCategory(category: string) {
|
||||
const newCategories = context.query.filters?.categories?.includes(category)
|
||||
|
|
@ -122,7 +122,7 @@ function BrowserCategories() {
|
|||
<div className="flex flex-col gap-y-6">
|
||||
<Show when>
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<h6 className="my-1 uppercase opacity-60">Categories</h6>
|
||||
<h5 className="my-1 uppercase opacity-60">Categories</h5>
|
||||
{categories.map(category => (
|
||||
<p
|
||||
aria-selected={context.query.filters?.categories?.includes(category.id)}
|
||||
|
|
@ -142,16 +142,15 @@ function BrowserCategories() {
|
|||
|
||||
function BrowserToolbar() {
|
||||
const context = useBrowserContext();
|
||||
const [query, setQuery] = useState(context.query.query ?? "")
|
||||
const [query, setQuery] = useState(context.query.query ?? '');
|
||||
return (
|
||||
<div className="w-full flex flex-row justify-between bg-page">
|
||||
<div className="flex flex-row gap-2" />
|
||||
|
||||
<div className="flex flex-row gap-2">
|
||||
<TextField
|
||||
value={query}
|
||||
iconLeft={<SearchMdIcon />}
|
||||
onChange={e=>setQuery(e.currentTarget.value)}
|
||||
onChange={e => setQuery(e.currentTarget.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key !== 'Enter')
|
||||
return;
|
||||
|
|
@ -159,6 +158,7 @@ function BrowserToolbar() {
|
|||
context.setQuery({ ...context.query, query: e.currentTarget.value });
|
||||
}}
|
||||
placeholder="Search for content"
|
||||
value={query}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import type { Paginated, SearchResult } from '@/bindings.gen';
|
||||
import type { PaginationOptions } from '@/hooks/usePagination';
|
||||
import { PackageGrid } from '@/components/content/PackageItem';
|
||||
import { useBrowserContext, useBrowserSearch } from '@/hooks/useBrowser';
|
||||
import usePagination from '@/hooks/usePagination';
|
||||
import { Show } from '@onelauncher/common/components';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { BrowserLayout } from './route';
|
||||
import type { Paginated, SearchResult } from '@/bindings.gen';
|
||||
import type { PaginationOptions } from '@/hooks/usePagination';
|
||||
import usePagination from '@/hooks/usePagination';
|
||||
|
||||
export const Route = createFileRoute('/app/browser/search')({
|
||||
component: RouteComponent,
|
||||
|
|
@ -33,40 +33,39 @@ function Search() {
|
|||
return (
|
||||
<>
|
||||
{
|
||||
search.data
|
||||
? <Results results={search.data}/>
|
||||
: <h1>Loading</h1>
|
||||
search.data
|
||||
? <Results results={search.data} />
|
||||
: <h1>Loading</h1>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Results({results}:{results:Paginated<SearchResult>}){
|
||||
function Results({ results }: { results: Paginated<SearchResult> }) {
|
||||
const context = useBrowserContext();
|
||||
const [oldTotal, setOldTotal] = useState(results.total)
|
||||
const [oldTotal, setOldTotal] = useState(results.total);
|
||||
const pagination = usePagination({
|
||||
itemsCount: results.total as unknown as number,
|
||||
itemsPerPage: context.query.limit as unknown as number
|
||||
})
|
||||
useEffect(()=>{
|
||||
context.setQuery({...context.query, offset: pagination.offset as unknown as bigint})
|
||||
},[pagination.page])
|
||||
useEffect(()=>{
|
||||
if(oldTotal === results.total) return;
|
||||
pagination.reset()
|
||||
console.log("pagination reset", results.total)
|
||||
setOldTotal(results.total)
|
||||
},[results.total])
|
||||
itemsPerPage: context.query.limit as unknown as number,
|
||||
});
|
||||
useEffect(() => {
|
||||
context.setQuery({ ...context.query, offset: pagination.offset as unknown as bigint });
|
||||
}, [pagination.page]);
|
||||
useEffect(() => {
|
||||
if (oldTotal === results.total)
|
||||
return;
|
||||
pagination.reset();
|
||||
setOldTotal(results.total);
|
||||
}, [results.total]);
|
||||
return (
|
||||
<div>
|
||||
<div className='w-full flex justify-end my-2'>
|
||||
<pagination.Navigation/>
|
||||
<div className="w-full flex justify-end my-2">
|
||||
<pagination.Navigation />
|
||||
</div>
|
||||
{results.items.length == 0
|
||||
? <h2 className='text-center text-lg font-light italic opacity-60'>No results</h2>
|
||||
: <PackageGrid items={results.items} provider={context.provider}/>
|
||||
}
|
||||
{results.items.length === 0
|
||||
? <h4 className="text-center text-lg font-light opacity-60">No results</h4>
|
||||
: <PackageGrid items={results.items} provider={context.provider} />}
|
||||
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import Modal from '@/components/overlay/Modal';
|
||||
import ScrollableContainer from '@/components/ScrollableContainer';
|
||||
import { bindings } from '@/main';
|
||||
import { useCommand } from '@onelauncher/common';
|
||||
|
|
@ -6,6 +7,7 @@ import { createFileRoute } from '@tanstack/react-router';
|
|||
import { convertFileSrc } from '@tauri-apps/api/core';
|
||||
import { dataDir, join } from '@tauri-apps/api/path';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Pressable } from 'react-aria-components';
|
||||
import Sidebar from '../settings/route';
|
||||
|
||||
export const Route = createFileRoute('/app/cluster/screenshots')({
|
||||
|
|
@ -60,15 +62,20 @@ function ScreenshotEntry({
|
|||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="flex flex-col items-center gap-3 rounded-xl bg-component-bg p-3 active:bg-component-bg-pressed hover:bg-component-bg-hover hover:opacity-80 cursor-pointer"
|
||||
>
|
||||
<img
|
||||
alt={path}
|
||||
className="aspect-video w-full rounded-lg"
|
||||
src={imagePath}
|
||||
/>
|
||||
</div>
|
||||
<Modal.Trigger>
|
||||
<Pressable>
|
||||
<div className="rounded-md overflow-hidden bg-component-bg-pressed outline-0 h-64 flex flex-col relative">
|
||||
<img
|
||||
alt={path}
|
||||
className="aspect-video w-full rounded-lg"
|
||||
src={imagePath}
|
||||
/>
|
||||
</div>
|
||||
</Pressable>
|
||||
<Modal className="w-full">
|
||||
<img className="rounded-xl" src={imagePath} />
|
||||
</Modal>
|
||||
</Modal.Trigger>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
@import "tailwindcss";
|
||||
@plugin "@tailwindcss/typography";
|
||||
@plugin "tailwindcss-react-aria-components";
|
||||
@plugin "tailwindcss-animated";
|
||||
@import "../../node_modules/@onelauncher/common/dist/theme.css";
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import type { PackageType, Provider } from "@/bindings.gen";
|
||||
|
||||
export const BROWSER_VIEWS: Array<string> = ['grid', 'list'] as const;
|
||||
|
||||
export interface CategoryItem {
|
||||
|
|
@ -6,7 +8,7 @@ export interface CategoryItem {
|
|||
}
|
||||
|
||||
export const browserCategories = {
|
||||
byPackageType(packageType: 'mod' | string & {}, provider: string): Array<CategoryItem> {
|
||||
byPackageType(packageType: PackageType, provider: Provider): Array<CategoryItem> {
|
||||
switch (packageType) {
|
||||
case 'mod':
|
||||
return this.mod(provider);
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ pub struct SearchResult {
|
|||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ManagedPackage {
|
||||
pub id: String,
|
||||
pub slug: String,
|
||||
pub provider: Provider,
|
||||
pub package_type: PackageType,
|
||||
pub name: String,
|
||||
|
|
@ -97,6 +98,7 @@ pub struct ManagedPackage {
|
|||
pub links: PackageLinks,
|
||||
pub status: PackageStatus,
|
||||
pub downloads: usize,
|
||||
pub gallery: Vec<PackageGallery>
|
||||
}
|
||||
|
||||
#[onelauncher_macro::specta]
|
||||
|
|
@ -205,6 +207,16 @@ pub struct PackageDonationUrl {
|
|||
pub url: String,
|
||||
}
|
||||
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct PackageGallery {
|
||||
pub url: String,
|
||||
pub thumbnail_url: String,
|
||||
pub title: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub featured: Option<bool>
|
||||
}
|
||||
|
||||
/// https://api.modrinth.com/v2/tag/donation_platform
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use super::ProviderExt;
|
||||
use crate::api::packages::data::{
|
||||
ManagedPackage, ManagedUser, ManagedVersion, ManagedVersionDependency, ManagedVersionFile, PackageAuthor, PackageDependencyType, PackageDonationUrl, PackageLicense, PackageLinks, PackageReleaseType, PackageSide, PackageStatus, SearchQuery, SearchResult, DEFAULT_LIMIT
|
||||
ManagedPackage, ManagedUser, ManagedVersion, ManagedVersionDependency, ManagedVersionFile, PackageAuthor, PackageDependencyType, PackageDonationUrl, PackageGallery, PackageLicense, PackageLinks, PackageReleaseType, PackageSide, PackageStatus, SearchQuery, SearchResult, DEFAULT_LIMIT
|
||||
};
|
||||
use crate::error::LauncherResult;
|
||||
use crate::utils::http;
|
||||
|
|
@ -346,6 +346,7 @@ struct ModrinthPackage {
|
|||
pub icon_url: Option<String>,
|
||||
#[serde(alias = "project_id")]
|
||||
pub id: String,
|
||||
pub slug: String,
|
||||
pub team: String,
|
||||
#[serde(default)]
|
||||
pub organization: Option<String>,
|
||||
|
|
@ -363,7 +364,7 @@ struct ModrinthPackage {
|
|||
#[serde(default)]
|
||||
pub license: Option<PackageLicense>,
|
||||
// #[serde(default)]
|
||||
// pub gallery: Vec<Gallery>,
|
||||
pub gallery: Vec<ModrinthGallery>,
|
||||
#[serde(default)]
|
||||
pub status: PackageStatus,
|
||||
}
|
||||
|
|
@ -373,6 +374,7 @@ impl From<ModrinthPackage> for ManagedPackage {
|
|||
Self {
|
||||
provider: Provider::Modrinth,
|
||||
id: value.id,
|
||||
slug: value.slug,
|
||||
package_type: value.project_type,
|
||||
name: value.title,
|
||||
short_desc: value.description,
|
||||
|
|
@ -405,6 +407,7 @@ impl From<ModrinthPackage> for ManagedPackage {
|
|||
},
|
||||
status: value.status,
|
||||
downloads: value.downloads,
|
||||
gallery: value.gallery.into_iter().map(Into::into).collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -532,6 +535,29 @@ impl From<ModrinthUser> for ManagedUser {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ModrinthGallery {
|
||||
#[serde(rename = "raw_url")]
|
||||
pub url: String,
|
||||
#[serde(rename = "url")]
|
||||
pub thumbnail_url: String,
|
||||
pub title: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub featured: Option<bool>
|
||||
}
|
||||
|
||||
impl From<ModrinthGallery> for PackageGallery {
|
||||
fn from(value: ModrinthGallery) -> Self {
|
||||
Self {
|
||||
url: value.url,
|
||||
thumbnail_url: value.thumbnail_url,
|
||||
title: value.title,
|
||||
description: value.description,
|
||||
featured: value.featured
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
enum FacetOperation {
|
||||
NotEq,
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ import { tv } from 'tailwind-variants';
|
|||
|
||||
export const tooltipVariants = tv({
|
||||
base: [
|
||||
'px-3 py-2 text-sm font-medium text-white bg-gray-900 rounded-lg shadow-lg',
|
||||
'border border-gray-700 max-w-xs z-50',
|
||||
'px-3 py-2 text-sm font-medium text-white bg-component-bg rounded-lg shadow-lg',
|
||||
'border border-component-border max-w-xs z-50',
|
||||
'data-[entering]:animate-in data-[entering]:fade-in data-[entering]:zoom-in-95',
|
||||
'data-[exiting]:animate-out data-[exiting]:fade-out data-[exiting]:zoom-out-95',
|
||||
],
|
||||
|
|
|
|||
|
|
@ -109,3 +109,7 @@ body {
|
|||
.os-hide-horizontal-scrollbar > .os-scrollbar-horizontal {
|
||||
display: none;
|
||||
}
|
||||
|
||||
code {
|
||||
@apply rounded-md p-2 text-left;
|
||||
}
|
||||
|
|
|
|||
496
pnpm-lock.yaml
generated
496
pnpm-lock.yaml
generated
|
|
@ -396,6 +396,15 @@ importers:
|
|||
react-dom:
|
||||
specifier: 'catalog:'
|
||||
version: 19.1.0(react@19.1.0)
|
||||
react-markdown:
|
||||
specifier: ^10.1.0
|
||||
version: 10.1.0(@types/react@19.1.2)(react@19.1.0)
|
||||
rehype-raw:
|
||||
specifier: ^7.0.0
|
||||
version: 7.0.0
|
||||
remark-gfm:
|
||||
specifier: ^4.0.1
|
||||
version: 4.0.1
|
||||
tailwind-merge:
|
||||
specifier: 'catalog:'
|
||||
version: 3.2.0
|
||||
|
|
@ -418,6 +427,9 @@ importers:
|
|||
specifier: 'catalog:'
|
||||
version: 2.1.5(@types/react@19.1.2)(react@19.1.0)
|
||||
devDependencies:
|
||||
'@tailwindcss/typography':
|
||||
specifier: ^0.5.16
|
||||
version: 0.5.16(tailwindcss@4.1.10)
|
||||
'@tanstack/eslint-config':
|
||||
specifier: 'catalog:'
|
||||
version: 0.1.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)
|
||||
|
|
@ -2624,6 +2636,11 @@ packages:
|
|||
resolution: {integrity: sha512-v0C43s7Pjw+B9w21htrQwuFObSkio2aV/qPx/mhrRldbqxbWJK6KizM+q7BF1/1CmuLqZqX3CeYF7s7P9fbA8Q==}
|
||||
engines: {node: '>= 10'}
|
||||
|
||||
'@tailwindcss/typography@0.5.16':
|
||||
resolution: {integrity: sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==}
|
||||
peerDependencies:
|
||||
tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1'
|
||||
|
||||
'@tailwindcss/vite@4.1.10':
|
||||
resolution: {integrity: sha512-QWnD5HDY2IADv+vYR82lOhqOlS1jSCUUAmfem52cXAhRTKxpDh3ARX8TTXJTCCO7Rv7cD2Nlekabv02bwP3a2A==}
|
||||
peerDependencies:
|
||||
|
|
@ -2862,9 +2879,15 @@ packages:
|
|||
'@types/eslint@9.6.1':
|
||||
resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==}
|
||||
|
||||
'@types/estree-jsx@1.0.5':
|
||||
resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==}
|
||||
|
||||
'@types/estree@1.0.7':
|
||||
resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==}
|
||||
|
||||
'@types/hast@3.0.4':
|
||||
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
|
||||
|
||||
'@types/json-schema@7.0.15':
|
||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||
|
||||
|
|
@ -2894,6 +2917,9 @@ packages:
|
|||
'@types/semver@7.5.8':
|
||||
resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==}
|
||||
|
||||
'@types/unist@2.0.11':
|
||||
resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
|
||||
|
||||
'@types/unist@3.0.3':
|
||||
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
|
||||
|
||||
|
|
@ -2944,6 +2970,9 @@ packages:
|
|||
resolution: {integrity: sha512-I+/rgqOVBn6f0o7NDTmAPWWC6NuqhV174lfYvAm9fUaWeiefLdux9/YI3/nLugEn9L8fcSi0XmpKi/r5u0nmpw==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@ungap/structured-clone@1.3.0':
|
||||
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
|
||||
|
||||
'@unrs/resolver-binding-darwin-arm64@1.7.2':
|
||||
resolution: {integrity: sha512-vxtBno4xvowwNmO/ASL0Y45TpHqmNkAaDtz4Jqb+clmcVSSl8XCG/PNFFkGsXXXS6AMjP+ja/TtNCFFa1QwLRg==}
|
||||
cpu: [arm64]
|
||||
|
|
@ -3250,6 +3279,9 @@ packages:
|
|||
peerDependencies:
|
||||
'@babel/core': ^7.0.0
|
||||
|
||||
bail@2.0.2:
|
||||
resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==}
|
||||
|
||||
balanced-match@1.0.2:
|
||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||
|
||||
|
|
@ -3337,9 +3369,18 @@ packages:
|
|||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
character-entities-html4@2.1.0:
|
||||
resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==}
|
||||
|
||||
character-entities-legacy@3.0.0:
|
||||
resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==}
|
||||
|
||||
character-entities@2.0.2:
|
||||
resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==}
|
||||
|
||||
character-reference-invalid@2.0.1:
|
||||
resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==}
|
||||
|
||||
check-error@2.1.1:
|
||||
resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==}
|
||||
engines: {node: '>= 16'}
|
||||
|
|
@ -3382,6 +3423,9 @@ packages:
|
|||
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
comma-separated-tokens@2.0.3:
|
||||
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
|
||||
|
||||
commander@6.2.1:
|
||||
resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
|
@ -3445,6 +3489,11 @@ packages:
|
|||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
cssesc@3.0.0:
|
||||
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
|
||||
engines: {node: '>=4'}
|
||||
hasBin: true
|
||||
|
||||
cssstyle@4.1.0:
|
||||
resolution: {integrity: sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -3839,6 +3888,9 @@ packages:
|
|||
resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
|
||||
engines: {node: '>=4.0'}
|
||||
|
||||
estree-util-is-identifier-name@3.0.0:
|
||||
resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==}
|
||||
|
||||
estree-walker@2.0.2:
|
||||
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
|
||||
|
||||
|
|
@ -3864,6 +3916,9 @@ packages:
|
|||
exsolve@1.0.5:
|
||||
resolution: {integrity: sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg==}
|
||||
|
||||
extend@3.0.2:
|
||||
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
|
||||
|
||||
fast-deep-equal@3.1.3:
|
||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||
|
||||
|
|
@ -4043,6 +4098,27 @@ packages:
|
|||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
hast-util-from-parse5@8.0.3:
|
||||
resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==}
|
||||
|
||||
hast-util-parse-selector@4.0.0:
|
||||
resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==}
|
||||
|
||||
hast-util-raw@9.1.0:
|
||||
resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==}
|
||||
|
||||
hast-util-to-jsx-runtime@2.3.6:
|
||||
resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==}
|
||||
|
||||
hast-util-to-parse5@8.0.0:
|
||||
resolution: {integrity: sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==}
|
||||
|
||||
hast-util-whitespace@3.0.0:
|
||||
resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
|
||||
|
||||
hastscript@9.0.1:
|
||||
resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==}
|
||||
|
||||
he@1.2.0:
|
||||
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
|
||||
hasBin: true
|
||||
|
|
@ -4061,6 +4137,12 @@ packages:
|
|||
resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
html-url-attributes@3.0.1:
|
||||
resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
|
||||
|
||||
html-void-elements@3.0.0:
|
||||
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
|
||||
|
||||
http-proxy-agent@7.0.2:
|
||||
resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
|
||||
engines: {node: '>= 14'}
|
||||
|
|
@ -4116,6 +4198,12 @@ packages:
|
|||
intl-messageformat@10.7.16:
|
||||
resolution: {integrity: sha512-UmdmHUmp5CIKKjSoE10la5yfU+AYJAaiYLsodbjL4lji83JNvgOQUjGaGhGrpFCb0Uh7sl7qfP1IyILa8Z40ug==}
|
||||
|
||||
is-alphabetical@2.0.1:
|
||||
resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==}
|
||||
|
||||
is-alphanumerical@2.0.1:
|
||||
resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==}
|
||||
|
||||
is-arrayish@0.2.1:
|
||||
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
|
||||
|
||||
|
|
@ -4134,6 +4222,9 @@ packages:
|
|||
resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
is-decimal@2.0.1:
|
||||
resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==}
|
||||
|
||||
is-extglob@2.1.1:
|
||||
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
|
@ -4146,6 +4237,9 @@ packages:
|
|||
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
is-hexadecimal@2.0.1:
|
||||
resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==}
|
||||
|
||||
is-html@2.0.0:
|
||||
resolution: {integrity: sha512-S+OpgB5i7wzIue/YSE5hg0e5ZYfG3hhpNh9KGl6ayJ38p7ED6wxQLd1TV91xHpcTvw90KMJ9EwN3F/iNflHBVg==}
|
||||
engines: {node: '>=8'}
|
||||
|
|
@ -4365,8 +4459,11 @@ packages:
|
|||
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
lodash.debounce@4.0.8:
|
||||
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
|
||||
lodash.castarray@4.4.0:
|
||||
resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==}
|
||||
|
||||
lodash.isplainobject@4.0.6:
|
||||
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
|
||||
|
||||
lodash.merge@4.6.2:
|
||||
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
||||
|
|
@ -4430,9 +4527,21 @@ packages:
|
|||
mdast-util-gfm@3.1.0:
|
||||
resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==}
|
||||
|
||||
mdast-util-mdx-expression@2.0.1:
|
||||
resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==}
|
||||
|
||||
mdast-util-mdx-jsx@3.2.0:
|
||||
resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==}
|
||||
|
||||
mdast-util-mdxjs-esm@2.0.1:
|
||||
resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==}
|
||||
|
||||
mdast-util-phrasing@4.1.0:
|
||||
resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==}
|
||||
|
||||
mdast-util-to-hast@13.2.0:
|
||||
resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==}
|
||||
|
||||
mdast-util-to-markdown@2.1.2:
|
||||
resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==}
|
||||
|
||||
|
|
@ -4724,6 +4833,9 @@ packages:
|
|||
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
parse-entities@4.0.2:
|
||||
resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==}
|
||||
|
||||
parse-imports-exports@0.2.4:
|
||||
resolution: {integrity: sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==}
|
||||
|
||||
|
|
@ -4812,6 +4924,10 @@ packages:
|
|||
resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
postcss-selector-parser@6.0.10:
|
||||
resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
postcss@8.5.3:
|
||||
resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
|
@ -4844,6 +4960,12 @@ packages:
|
|||
resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
|
||||
engines: {node: '>= 0.6.0'}
|
||||
|
||||
property-information@6.5.0:
|
||||
resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==}
|
||||
|
||||
property-information@7.1.0:
|
||||
resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
|
||||
|
||||
proxy-compare@3.0.1:
|
||||
resolution: {integrity: sha512-V9plBAt3qjMlS1+nC8771KNf6oJ12gExvaxnNzN/9yVRLdTv/lc+oJlnSzrdYDAvBfTStPCoiaCOTmTs0adv7Q==}
|
||||
|
||||
|
|
@ -4882,6 +5004,12 @@ packages:
|
|||
peerDependencies:
|
||||
react: '>=16.8 || ^17.0.0 || ^18.0.0'
|
||||
|
||||
react-markdown@10.1.0:
|
||||
resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==}
|
||||
peerDependencies:
|
||||
'@types/react': '>=18'
|
||||
react: '>=18'
|
||||
|
||||
react-refresh@0.17.0:
|
||||
resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
|
@ -4937,6 +5065,21 @@ packages:
|
|||
resolution: {integrity: sha512-qx+xQGZVsy55CH0a1hiVwHmqjLryfh7wQyF5HO07XJ9f7dQMY/gPQHhlyDkIzJKC+x2fUCpCcUODUUUFrm7SHA==}
|
||||
hasBin: true
|
||||
|
||||
rehype-raw@7.0.0:
|
||||
resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==}
|
||||
|
||||
remark-gfm@4.0.1:
|
||||
resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==}
|
||||
|
||||
remark-parse@11.0.0:
|
||||
resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==}
|
||||
|
||||
remark-rehype@11.1.2:
|
||||
resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==}
|
||||
|
||||
remark-stringify@11.0.0:
|
||||
resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==}
|
||||
|
||||
remove-accents@0.5.0:
|
||||
resolution: {integrity: sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==}
|
||||
|
||||
|
|
@ -5078,6 +5221,9 @@ packages:
|
|||
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
space-separated-tokens@2.0.2:
|
||||
resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
|
||||
|
||||
spdx-correct@3.2.0:
|
||||
resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==}
|
||||
|
||||
|
|
@ -5129,6 +5275,9 @@ packages:
|
|||
string_decoder@1.3.0:
|
||||
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
|
||||
|
||||
stringify-entities@4.0.4:
|
||||
resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
|
||||
|
||||
strip-ansi@6.0.1:
|
||||
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
||||
engines: {node: '>=8'}
|
||||
|
|
@ -5152,8 +5301,11 @@ packages:
|
|||
strnum@1.0.5:
|
||||
resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==}
|
||||
|
||||
style-to-object@1.0.8:
|
||||
resolution: {integrity: sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==}
|
||||
style-to-js@1.1.17:
|
||||
resolution: {integrity: sha512-xQcBGDxJb6jjFCTzvQtfiPn6YvvP2O8U1MDIPNfJQlWMYfktPy+iGsHE7cssjs7y84d9fQaK4UF3RIJaAHSoYA==}
|
||||
|
||||
style-to-object@1.0.9:
|
||||
resolution: {integrity: sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw==}
|
||||
|
||||
supports-color@7.2.0:
|
||||
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
||||
|
|
@ -5280,6 +5432,12 @@ packages:
|
|||
traverse@0.3.9:
|
||||
resolution: {integrity: sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==}
|
||||
|
||||
trim-lines@3.0.1:
|
||||
resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
|
||||
|
||||
trough@2.2.0:
|
||||
resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==}
|
||||
|
||||
ts-api-utils@2.1.0:
|
||||
resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==}
|
||||
engines: {node: '>=18.12'}
|
||||
|
|
@ -5369,9 +5527,15 @@ packages:
|
|||
resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
unified@11.0.5:
|
||||
resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==}
|
||||
|
||||
unist-util-is@6.0.0:
|
||||
resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==}
|
||||
|
||||
unist-util-position@5.0.0:
|
||||
resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==}
|
||||
|
||||
unist-util-stringify-position@4.0.0:
|
||||
resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==}
|
||||
|
||||
|
|
@ -5433,6 +5597,15 @@ packages:
|
|||
react:
|
||||
optional: true
|
||||
|
||||
vfile-location@5.0.3:
|
||||
resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==}
|
||||
|
||||
vfile-message@4.0.2:
|
||||
resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==}
|
||||
|
||||
vfile@6.0.3:
|
||||
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
|
||||
|
||||
vite-node@2.1.3:
|
||||
resolution: {integrity: sha512-I1JadzO+xYX887S39Do+paRePCKoiDrWRRjp9kkG5he0t7RXNvPAJPCQSJqbGN4uCrFFeS3Kj3sLqY8NMYBEdA==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
|
|
@ -5585,6 +5758,9 @@ packages:
|
|||
resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
web-namespaces@2.0.1:
|
||||
resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==}
|
||||
|
||||
webidl-conversions@3.0.1:
|
||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||
|
||||
|
|
@ -8171,6 +8347,14 @@ snapshots:
|
|||
'@tailwindcss/oxide-win32-arm64-msvc': 4.1.10
|
||||
'@tailwindcss/oxide-win32-x64-msvc': 4.1.10
|
||||
|
||||
'@tailwindcss/typography@0.5.16(tailwindcss@4.1.10)':
|
||||
dependencies:
|
||||
lodash.castarray: 4.4.0
|
||||
lodash.isplainobject: 4.0.6
|
||||
lodash.merge: 4.6.2
|
||||
postcss-selector-parser: 6.0.10
|
||||
tailwindcss: 4.1.10
|
||||
|
||||
'@tailwindcss/vite@4.1.10(vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.6.0))':
|
||||
dependencies:
|
||||
'@tailwindcss/node': 4.1.10
|
||||
|
|
@ -8424,8 +8608,16 @@ snapshots:
|
|||
'@types/estree': 1.0.7
|
||||
'@types/json-schema': 7.0.15
|
||||
|
||||
'@types/estree-jsx@1.0.5':
|
||||
dependencies:
|
||||
'@types/estree': 1.0.7
|
||||
|
||||
'@types/estree@1.0.7': {}
|
||||
|
||||
'@types/hast@3.0.4':
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
|
||||
'@types/json-schema@7.0.15': {}
|
||||
|
||||
'@types/mdast@4.0.4':
|
||||
|
|
@ -8452,6 +8644,8 @@ snapshots:
|
|||
|
||||
'@types/semver@7.5.8': {}
|
||||
|
||||
'@types/unist@2.0.11': {}
|
||||
|
||||
'@types/unist@3.0.3': {}
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.31.1(@typescript-eslint/parser@8.31.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)':
|
||||
|
|
@ -8531,6 +8725,8 @@ snapshots:
|
|||
'@typescript-eslint/types': 8.31.1
|
||||
eslint-visitor-keys: 4.2.0
|
||||
|
||||
'@ungap/structured-clone@1.3.0': {}
|
||||
|
||||
'@unrs/resolver-binding-darwin-arm64@1.7.2':
|
||||
optional: true
|
||||
|
||||
|
|
@ -8855,6 +9051,8 @@ snapshots:
|
|||
babel-plugin-jsx-dom-expressions: 0.39.8(@babel/core@7.27.1)
|
||||
optional: true
|
||||
|
||||
bail@2.0.2: {}
|
||||
|
||||
balanced-match@1.0.2: {}
|
||||
|
||||
bare-events@2.4.2:
|
||||
|
|
@ -8940,8 +9138,14 @@ snapshots:
|
|||
ansi-styles: 4.3.0
|
||||
supports-color: 7.2.0
|
||||
|
||||
character-entities-html4@2.1.0: {}
|
||||
|
||||
character-entities-legacy@3.0.0: {}
|
||||
|
||||
character-entities@2.0.2: {}
|
||||
|
||||
character-reference-invalid@2.0.1: {}
|
||||
|
||||
check-error@2.1.1:
|
||||
optional: true
|
||||
|
||||
|
|
@ -8985,6 +9189,8 @@ snapshots:
|
|||
delayed-stream: 1.0.0
|
||||
optional: true
|
||||
|
||||
comma-separated-tokens@2.0.3: {}
|
||||
|
||||
commander@6.2.1: {}
|
||||
|
||||
comment-parser@1.4.1: {}
|
||||
|
|
@ -9043,6 +9249,8 @@ snapshots:
|
|||
shebang-command: 2.0.0
|
||||
which: 2.0.2
|
||||
|
||||
cssesc@3.0.0: {}
|
||||
|
||||
cssstyle@4.1.0:
|
||||
dependencies:
|
||||
rrweb-cssom: 0.7.1
|
||||
|
|
@ -9501,7 +9709,7 @@ snapshots:
|
|||
is-html: 2.0.0
|
||||
kebab-case: 1.0.2
|
||||
known-css-properties: 0.30.0
|
||||
style-to-object: 1.0.8
|
||||
style-to-object: 1.0.9
|
||||
typescript: 5.8.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
|
@ -9632,6 +9840,8 @@ snapshots:
|
|||
|
||||
estraverse@5.3.0: {}
|
||||
|
||||
estree-util-is-identifier-name@3.0.0: {}
|
||||
|
||||
estree-walker@2.0.2: {}
|
||||
|
||||
estree-walker@3.0.3:
|
||||
|
|
@ -9662,6 +9872,8 @@ snapshots:
|
|||
|
||||
exsolve@1.0.5: {}
|
||||
|
||||
extend@3.0.2: {}
|
||||
|
||||
fast-deep-equal@3.1.3: {}
|
||||
|
||||
fast-fifo@1.3.2: {}
|
||||
|
|
@ -9836,6 +10048,79 @@ snapshots:
|
|||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
|
||||
hast-util-from-parse5@8.0.3:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
'@types/unist': 3.0.3
|
||||
devlop: 1.1.0
|
||||
hastscript: 9.0.1
|
||||
property-information: 7.1.0
|
||||
vfile: 6.0.3
|
||||
vfile-location: 5.0.3
|
||||
web-namespaces: 2.0.1
|
||||
|
||||
hast-util-parse-selector@4.0.0:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
|
||||
hast-util-raw@9.1.0:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
'@types/unist': 3.0.3
|
||||
'@ungap/structured-clone': 1.3.0
|
||||
hast-util-from-parse5: 8.0.3
|
||||
hast-util-to-parse5: 8.0.0
|
||||
html-void-elements: 3.0.0
|
||||
mdast-util-to-hast: 13.2.0
|
||||
parse5: 7.1.2
|
||||
unist-util-position: 5.0.0
|
||||
unist-util-visit: 5.0.0
|
||||
vfile: 6.0.3
|
||||
web-namespaces: 2.0.1
|
||||
zwitch: 2.0.4
|
||||
|
||||
hast-util-to-jsx-runtime@2.3.6:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.7
|
||||
'@types/hast': 3.0.4
|
||||
'@types/unist': 3.0.3
|
||||
comma-separated-tokens: 2.0.3
|
||||
devlop: 1.1.0
|
||||
estree-util-is-identifier-name: 3.0.0
|
||||
hast-util-whitespace: 3.0.0
|
||||
mdast-util-mdx-expression: 2.0.1
|
||||
mdast-util-mdx-jsx: 3.2.0
|
||||
mdast-util-mdxjs-esm: 2.0.1
|
||||
property-information: 7.1.0
|
||||
space-separated-tokens: 2.0.2
|
||||
style-to-js: 1.1.17
|
||||
unist-util-position: 5.0.0
|
||||
vfile-message: 4.0.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
hast-util-to-parse5@8.0.0:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
comma-separated-tokens: 2.0.3
|
||||
devlop: 1.1.0
|
||||
property-information: 6.5.0
|
||||
space-separated-tokens: 2.0.2
|
||||
web-namespaces: 2.0.1
|
||||
zwitch: 2.0.4
|
||||
|
||||
hast-util-whitespace@3.0.0:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
|
||||
hastscript@9.0.1:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
comma-separated-tokens: 2.0.3
|
||||
hast-util-parse-selector: 4.0.0
|
||||
property-information: 7.1.0
|
||||
space-separated-tokens: 2.0.2
|
||||
|
||||
he@1.2.0: {}
|
||||
|
||||
hosted-git-info@2.8.9: {}
|
||||
|
|
@ -9851,6 +10136,10 @@ snapshots:
|
|||
html-tags@3.3.1:
|
||||
optional: true
|
||||
|
||||
html-url-attributes@3.0.1: {}
|
||||
|
||||
html-void-elements@3.0.0: {}
|
||||
|
||||
http-proxy-agent@7.0.2:
|
||||
dependencies:
|
||||
agent-base: 7.1.1
|
||||
|
|
@ -9897,8 +10186,7 @@ snapshots:
|
|||
|
||||
inherits@2.0.4: {}
|
||||
|
||||
inline-style-parser@0.2.4:
|
||||
optional: true
|
||||
inline-style-parser@0.2.4: {}
|
||||
|
||||
intl-messageformat@10.7.16:
|
||||
dependencies:
|
||||
|
|
@ -9907,6 +10195,13 @@ snapshots:
|
|||
'@formatjs/icu-messageformat-parser': 2.11.2
|
||||
tslib: 2.8.1
|
||||
|
||||
is-alphabetical@2.0.1: {}
|
||||
|
||||
is-alphanumerical@2.0.1:
|
||||
dependencies:
|
||||
is-alphabetical: 2.0.1
|
||||
is-decimal: 2.0.1
|
||||
|
||||
is-arrayish@0.2.1: {}
|
||||
|
||||
is-binary-path@2.1.0:
|
||||
|
|
@ -9925,6 +10220,8 @@ snapshots:
|
|||
dependencies:
|
||||
hasown: 2.0.2
|
||||
|
||||
is-decimal@2.0.1: {}
|
||||
|
||||
is-extglob@2.1.1: {}
|
||||
|
||||
is-fullwidth-code-point@3.0.0: {}
|
||||
|
|
@ -9933,6 +10230,8 @@ snapshots:
|
|||
dependencies:
|
||||
is-extglob: 2.1.1
|
||||
|
||||
is-hexadecimal@2.0.1: {}
|
||||
|
||||
is-html@2.0.0:
|
||||
dependencies:
|
||||
html-tags: 3.3.1
|
||||
|
|
@ -10134,7 +10433,9 @@ snapshots:
|
|||
dependencies:
|
||||
p-locate: 5.0.0
|
||||
|
||||
lodash.debounce@4.0.8: {}
|
||||
lodash.castarray@4.4.0: {}
|
||||
|
||||
lodash.isplainobject@4.0.6: {}
|
||||
|
||||
lodash.merge@4.6.2: {}
|
||||
|
||||
|
|
@ -10259,11 +10560,62 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
mdast-util-mdx-expression@2.0.1:
|
||||
dependencies:
|
||||
'@types/estree-jsx': 1.0.5
|
||||
'@types/hast': 3.0.4
|
||||
'@types/mdast': 4.0.4
|
||||
devlop: 1.1.0
|
||||
mdast-util-from-markdown: 2.0.2
|
||||
mdast-util-to-markdown: 2.1.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
mdast-util-mdx-jsx@3.2.0:
|
||||
dependencies:
|
||||
'@types/estree-jsx': 1.0.5
|
||||
'@types/hast': 3.0.4
|
||||
'@types/mdast': 4.0.4
|
||||
'@types/unist': 3.0.3
|
||||
ccount: 2.0.1
|
||||
devlop: 1.1.0
|
||||
mdast-util-from-markdown: 2.0.2
|
||||
mdast-util-to-markdown: 2.1.2
|
||||
parse-entities: 4.0.2
|
||||
stringify-entities: 4.0.4
|
||||
unist-util-stringify-position: 4.0.0
|
||||
vfile-message: 4.0.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
mdast-util-mdxjs-esm@2.0.1:
|
||||
dependencies:
|
||||
'@types/estree-jsx': 1.0.5
|
||||
'@types/hast': 3.0.4
|
||||
'@types/mdast': 4.0.4
|
||||
devlop: 1.1.0
|
||||
mdast-util-from-markdown: 2.0.2
|
||||
mdast-util-to-markdown: 2.1.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
mdast-util-phrasing@4.1.0:
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
unist-util-is: 6.0.0
|
||||
|
||||
mdast-util-to-hast@13.2.0:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
'@types/mdast': 4.0.4
|
||||
'@ungap/structured-clone': 1.3.0
|
||||
devlop: 1.1.0
|
||||
micromark-util-sanitize-uri: 2.0.1
|
||||
trim-lines: 3.0.1
|
||||
unist-util-position: 5.0.0
|
||||
unist-util-visit: 5.0.0
|
||||
vfile: 6.0.3
|
||||
|
||||
mdast-util-to-markdown@2.1.2:
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
|
|
@ -10653,6 +11005,16 @@ snapshots:
|
|||
dependencies:
|
||||
callsites: 3.1.0
|
||||
|
||||
parse-entities@4.0.2:
|
||||
dependencies:
|
||||
'@types/unist': 2.0.11
|
||||
character-entities-legacy: 3.0.0
|
||||
character-reference-invalid: 2.0.1
|
||||
decode-named-character-reference: 1.1.0
|
||||
is-alphanumerical: 2.0.1
|
||||
is-decimal: 2.0.1
|
||||
is-hexadecimal: 2.0.1
|
||||
|
||||
parse-imports-exports@0.2.4:
|
||||
dependencies:
|
||||
parse-statements: 1.0.11
|
||||
|
|
@ -10671,7 +11033,6 @@ snapshots:
|
|||
parse5@7.1.2:
|
||||
dependencies:
|
||||
entities: 4.5.0
|
||||
optional: true
|
||||
|
||||
pascal-case@3.1.2:
|
||||
dependencies:
|
||||
|
|
@ -10731,6 +11092,11 @@ snapshots:
|
|||
|
||||
pluralize@8.0.0: {}
|
||||
|
||||
postcss-selector-parser@6.0.10:
|
||||
dependencies:
|
||||
cssesc: 3.0.0
|
||||
util-deprecate: 1.0.2
|
||||
|
||||
postcss@8.5.3:
|
||||
dependencies:
|
||||
nanoid: 3.3.11
|
||||
|
|
@ -10753,6 +11119,10 @@ snapshots:
|
|||
|
||||
process@0.11.10: {}
|
||||
|
||||
property-information@6.5.0: {}
|
||||
|
||||
property-information@7.1.0: {}
|
||||
|
||||
proxy-compare@3.0.1: {}
|
||||
|
||||
punycode@2.3.1: {}
|
||||
|
|
@ -10848,12 +11218,23 @@ snapshots:
|
|||
react: 19.1.0
|
||||
scheduler: 0.26.0
|
||||
|
||||
react-just-parallax@3.1.16(react@19.1.0):
|
||||
react-markdown@10.1.0(@types/react@19.1.2)(react@19.1.0):
|
||||
dependencies:
|
||||
framesync: 6.1.0
|
||||
lodash.debounce: 4.0.8
|
||||
prefix: 1.0.0
|
||||
'@types/hast': 3.0.4
|
||||
'@types/mdast': 4.0.4
|
||||
'@types/react': 19.1.2
|
||||
devlop: 1.1.0
|
||||
hast-util-to-jsx-runtime: 2.3.6
|
||||
html-url-attributes: 3.0.1
|
||||
mdast-util-to-hast: 13.2.0
|
||||
react: 19.1.0
|
||||
remark-parse: 11.0.0
|
||||
remark-rehype: 11.1.2
|
||||
unified: 11.0.5
|
||||
unist-util-visit: 5.0.0
|
||||
vfile: 6.0.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
react-refresh@0.17.0: {}
|
||||
|
||||
|
|
@ -10946,6 +11327,46 @@ snapshots:
|
|||
dependencies:
|
||||
jsesc: 0.5.0
|
||||
|
||||
rehype-raw@7.0.0:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
hast-util-raw: 9.1.0
|
||||
vfile: 6.0.3
|
||||
|
||||
remark-gfm@4.0.1:
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
mdast-util-gfm: 3.1.0
|
||||
micromark-extension-gfm: 3.0.0
|
||||
remark-parse: 11.0.0
|
||||
remark-stringify: 11.0.0
|
||||
unified: 11.0.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
remark-parse@11.0.0:
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
mdast-util-from-markdown: 2.0.2
|
||||
micromark-util-types: 2.0.2
|
||||
unified: 11.0.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
remark-rehype@11.1.2:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
'@types/mdast': 4.0.4
|
||||
mdast-util-to-hast: 13.2.0
|
||||
unified: 11.0.5
|
||||
vfile: 6.0.3
|
||||
|
||||
remark-stringify@11.0.0:
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
mdast-util-to-markdown: 2.1.2
|
||||
unified: 11.0.5
|
||||
|
||||
remove-accents@0.5.0: {}
|
||||
|
||||
require-from-string@2.0.2: {}
|
||||
|
|
@ -11118,6 +11539,8 @@ snapshots:
|
|||
|
||||
source-map@0.6.1: {}
|
||||
|
||||
space-separated-tokens@2.0.2: {}
|
||||
|
||||
spdx-correct@3.2.0:
|
||||
dependencies:
|
||||
spdx-expression-parse: 3.0.1
|
||||
|
|
@ -11179,6 +11602,11 @@ snapshots:
|
|||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
stringify-entities@4.0.4:
|
||||
dependencies:
|
||||
character-entities-html4: 2.1.0
|
||||
character-entities-legacy: 3.0.0
|
||||
|
||||
strip-ansi@6.0.1:
|
||||
dependencies:
|
||||
ansi-regex: 5.0.1
|
||||
|
|
@ -11197,10 +11625,13 @@ snapshots:
|
|||
|
||||
strnum@1.0.5: {}
|
||||
|
||||
style-to-object@1.0.8:
|
||||
style-to-js@1.1.17:
|
||||
dependencies:
|
||||
style-to-object: 1.0.9
|
||||
|
||||
style-to-object@1.0.9:
|
||||
dependencies:
|
||||
inline-style-parser: 0.2.4
|
||||
optional: true
|
||||
|
||||
supports-color@7.2.0:
|
||||
dependencies:
|
||||
|
|
@ -11322,6 +11753,10 @@ snapshots:
|
|||
|
||||
traverse@0.3.9: {}
|
||||
|
||||
trim-lines@3.0.1: {}
|
||||
|
||||
trough@2.2.0: {}
|
||||
|
||||
ts-api-utils@2.1.0(typescript@5.8.3):
|
||||
dependencies:
|
||||
typescript: 5.8.3
|
||||
|
|
@ -11394,10 +11829,24 @@ snapshots:
|
|||
|
||||
unicorn-magic@0.3.0: {}
|
||||
|
||||
unified@11.0.5:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
bail: 2.0.2
|
||||
devlop: 1.1.0
|
||||
extend: 3.0.2
|
||||
is-plain-obj: 4.1.0
|
||||
trough: 2.2.0
|
||||
vfile: 6.0.3
|
||||
|
||||
unist-util-is@6.0.0:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
|
||||
unist-util-position@5.0.0:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
|
||||
unist-util-stringify-position@4.0.0:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
|
|
@ -11481,6 +11930,21 @@ snapshots:
|
|||
'@types/react': 19.1.2
|
||||
react: 19.1.0
|
||||
|
||||
vfile-location@5.0.3:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
vfile: 6.0.3
|
||||
|
||||
vfile-message@4.0.2:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
unist-util-stringify-position: 4.0.0
|
||||
|
||||
vfile@6.0.3:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
vfile-message: 4.0.2
|
||||
|
||||
vite-node@2.1.3(@types/node@22.16.5)(lightningcss@1.30.1)(sass@1.80.3):
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
|
|
@ -11643,6 +12107,8 @@ snapshots:
|
|||
xml-name-validator: 5.0.0
|
||||
optional: true
|
||||
|
||||
web-namespaces@2.0.1: {}
|
||||
|
||||
webidl-conversions@3.0.1: {}
|
||||
|
||||
webidl-conversions@7.0.0:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue