etape color

This commit is contained in:
Arthur Belleville 2025-11-19 22:24:23 +01:00
parent 33bd462f87
commit 2e16353f5e
No known key found for this signature in database
15 changed files with 325 additions and 31 deletions

View file

@ -15,6 +15,7 @@ import {
useTasksByTablo,
useUpdateEtape,
} from "../hooks/tasks";
import { getEtapeColor } from "../utils/etapeColors";
interface TabloOverviewSectionProps {
tablo: UserTablo;
@ -163,10 +164,11 @@ export const TabloOverviewSection = ({ tablo, isAdmin }: TabloOverviewSectionPro
<ul className="space-y-3">
{sortedEtapes.map((etape, index) => {
const isEditing = editingEtapeId === etape.id;
const etapeColor = getEtapeColor(etape.position);
return (
<li
key={etape.id}
className="flex items-start gap-3 rounded-lg border border-border bg-card/40 px-4 py-3"
className={`flex items-start gap-3 rounded-lg border px-4 py-3 ${etapeColor.bg} ${etapeColor.border}`}
>
{canManageEtapes && (
<div className="flex flex-col gap-1 pt-1">
@ -224,27 +226,27 @@ export const TabloOverviewSection = ({ tablo, isAdmin }: TabloOverviewSectionPro
</div>
) : (
<>
<TypographyP className="text-base font-medium text-foreground">
<TypographyP className={`text-base font-medium ${etapeColor.text}`}>
{etape.title}
</TypographyP>
<TypographyMuted className="text-xs text-muted-foreground">
<TypographyMuted className={`text-xs ${etapeColor.text} opacity-70`}>
Étape {etape.position + 1}
</TypographyMuted>
{(() => {
const { total, done, ongoing } = getEtapeTaskCounts(etape.id);
return (
<div className="flex gap-3 mt-2 text-xs">
<span className="text-muted-foreground">
<span className="font-medium text-foreground">{total}</span>{" "}
<div className={`flex gap-3 mt-2 text-xs ${etapeColor.text}`}>
<span className="opacity-70">
<span className="font-medium opacity-100">{total}</span>{" "}
{pluralize("tâche", total)}
</span>
{ongoing > 0 && (
<span className="text-blue-600 dark:text-blue-400">
<span className="opacity-90">
<span className="font-medium">{ongoing}</span> en cours
</span>
)}
{done > 0 && (
<span className="text-green-600 dark:text-green-400">
<span className="opacity-90">
<span className="font-medium">{done}</span>{" "}
{pluralize("terminée", done)}
</span>

View file

@ -10,6 +10,7 @@ import {
useTasksByTablo,
useUpdateTaskPositions,
} from "../hooks/tasks";
import { getEtapeColor } from "../utils/etapeColors";
import { KanbanBoard } from "./kanban/KanbanBoard";
import { TaskModal } from "./kanban/TaskModal";
@ -39,6 +40,15 @@ export const TabloTasksSection = ({ tablo }: TabloTasksSectionProps) => {
[etapes]
);
const etapeColorMap = useMemo(
() =>
etapes.reduce<Record<string, ReturnType<typeof getEtapeColor>>>((map, etape) => {
map[etape.id] = getEtapeColor(etape.position);
return map;
}, {}),
[etapes]
);
// Check for tasks without parent (orphaned tasks)
const orphanedTasks = useMemo(() => {
return tasks?.filter((task) => !task.parent_task_id) || [];
@ -196,6 +206,7 @@ export const TabloTasksSection = ({ tablo }: TabloTasksSectionProps) => {
members={members}
etapes={etapes}
etapeTitles={etapeTitleMap}
etapeColors={etapeColorMap}
onTaskClick={handleTaskClick}
onAddTask={handleAddTask}
onAddTaskInline={handleCreateTask}

View file

@ -0,0 +1,168 @@
import { useEffect, useRef } from "react";
import * as THREE from "three";
const usePrefersReducedMotion = () => {
if (typeof window === "undefined") return true;
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
};
export const ThreeLoginBackground = () => {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!containerRef.current) return;
if (usePrefersReducedMotion()) return;
const renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true,
});
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace;
containerRef.current.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x030014, 0.035);
const camera = new THREE.PerspectiveCamera(
45,
window.innerWidth / window.innerHeight,
0.1,
100
);
camera.position.set(0, 0, 14);
const ambientLight = new THREE.AmbientLight(0x6d28d9, 0.6);
scene.add(ambientLight);
const pointLight = new THREE.PointLight(0x8b5cf6, 40, 40);
pointLight.position.set(5, 5, 6);
scene.add(pointLight);
const rimLight = new THREE.PointLight(0x38bdf8, 25, 30);
rimLight.position.set(-4, -3, -6);
scene.add(rimLight);
const coreGeometry = new THREE.IcosahedronGeometry(3, 1);
const coreMaterial = new THREE.MeshStandardMaterial({
color: new THREE.Color("#8b5cf6"),
emissive: new THREE.Color("#6366f1"),
metalness: 0.85,
roughness: 0.2,
transparent: true,
opacity: 0.8,
wireframe: true,
});
const core = new THREE.Mesh(coreGeometry, coreMaterial);
scene.add(core);
const haloGeometry = new THREE.RingGeometry(4.2, 5.6, 128);
const haloMaterial = new THREE.MeshBasicMaterial({
color: 0x38bdf8,
side: THREE.DoubleSide,
transparent: true,
opacity: 0.25,
});
const halo = new THREE.Mesh(haloGeometry, haloMaterial);
halo.rotation.x = Math.PI / 2.4;
scene.add(halo);
const orbitCount = 1200;
const orbitPositions = new Float32Array(orbitCount * 3);
for (let i = 0; i < orbitCount; i += 1) {
const radius = 7 + Math.random() * 8;
const angle = Math.random() * Math.PI * 2;
orbitPositions[i * 3] = Math.cos(angle) * radius;
orbitPositions[i * 3 + 1] = (Math.random() - 0.5) * 6;
orbitPositions[i * 3 + 2] = Math.sin(angle) * radius;
}
const orbitGeometry = new THREE.BufferGeometry();
orbitGeometry.setAttribute("position", new THREE.BufferAttribute(orbitPositions, 3));
const orbitMaterial = new THREE.PointsMaterial({
color: 0x818cf8,
size: 0.07,
transparent: true,
opacity: 0.65,
});
const orbitField = new THREE.Points(orbitGeometry, orbitMaterial);
scene.add(orbitField);
const floatingGeometry = new THREE.TetrahedronGeometry(0.35);
const floatingMaterial = new THREE.MeshStandardMaterial({
color: 0xffffff,
emissive: 0x7c3aed,
emissiveIntensity: 0.4,
metalness: 0.4,
roughness: 0.3,
});
const floatingMeshes: THREE.Mesh[] = [];
for (let i = 0; i < 25; i += 1) {
const mesh = new THREE.Mesh(floatingGeometry, floatingMaterial.clone());
mesh.position.set(
(Math.random() - 0.5) * 20,
(Math.random() - 0.5) * 10,
(Math.random() - 0.5) * 20
);
mesh.rotation.set(Math.random() * Math.PI, Math.random() * Math.PI, Math.random() * Math.PI);
mesh.scale.setScalar(0.6 + Math.random() * 0.9);
floatingMeshes.push(mesh);
scene.add(mesh);
}
const clock = new THREE.Clock();
let frameId: number;
const animate = () => {
const elapsed = clock.getElapsedTime();
core.rotation.x = elapsed * 0.2;
core.rotation.y = elapsed * 0.3;
halo.rotation.z = elapsed * 0.1;
orbitField.rotation.y = elapsed * 0.05;
orbitField.rotation.x = Math.sin(elapsed * 0.1) * 0.05;
floatingMeshes.forEach((mesh, index) => {
mesh.position.y += Math.sin(elapsed + index) * 0.002;
mesh.rotation.x += 0.005;
mesh.rotation.y += 0.008;
});
camera.position.x = Math.sin(elapsed * 0.2) * 1.5;
camera.position.y = Math.cos(elapsed * 0.15) * 0.8;
camera.lookAt(0, 0, 0);
renderer.render(scene, camera);
frameId = requestAnimationFrame(animate);
};
animate();
const handleResize = () => {
const { innerWidth, innerHeight } = window;
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
};
window.addEventListener("resize", handleResize);
return () => {
cancelAnimationFrame(frameId);
window.removeEventListener("resize", handleResize);
renderer.dispose();
coreGeometry.dispose();
haloGeometry.dispose();
orbitGeometry.dispose();
floatingGeometry.dispose();
containerRef.current?.removeChild(renderer.domElement);
};
}, []);
return (
<div
ref={containerRef}
className="absolute inset-0 -z-10 pointer-events-none"
aria-hidden="true"
/>
);
};

View file

@ -5,6 +5,7 @@ import type {
TaskStatus,
} from "@xtablo/shared-types";
import { useState } from "react";
import type { getEtapeColor } from "../../utils/etapeColors";
import { KanbanColumn } from "./KanbanColumn";
import type { TabloMember } from "./types";
@ -13,6 +14,7 @@ interface KanbanBoardProps {
members: TabloMember[];
etapes: Etape[];
etapeTitles: Record<string, string>;
etapeColors: Record<string, ReturnType<typeof getEtapeColor>>;
onTaskClick: (task: KanbanTask) => void;
onAddTask: (status: TaskStatus) => void;
onAddTaskInline: (task: {
@ -29,6 +31,7 @@ export const KanbanBoard = ({
columns,
members,
etapes,
etapeColors,
onTaskClick,
onAddTask,
onAddTaskInline,
@ -62,6 +65,7 @@ export const KanbanBoard = ({
column={column}
members={members}
etapes={etapes}
etapeColors={etapeColors}
onTaskClick={onTaskClick}
onAddTask={onAddTask}
onAddTaskInline={onAddTaskInline}

View file

@ -6,6 +6,7 @@ import type {
} from "@xtablo/shared-types";
import { Button } from "@xtablo/ui/components/button";
import { Plus } from "lucide-react";
import type { getEtapeColor } from "../../utils/etapeColors";
import { InlineTaskCreate } from "./InlineTaskCreate";
import { KanbanTaskCard } from "./KanbanTaskCard";
import type { TabloMember } from "./types";
@ -14,6 +15,7 @@ interface KanbanColumnProps {
column: KanbanColumnType;
members: TabloMember[];
etapes: Etape[];
etapeColors: Record<string, ReturnType<typeof getEtapeColor>>;
onTaskClick: (task: KanbanTask) => void;
onAddTask: (status: KanbanColumnType["status"]) => void;
onAddTaskInline: (task: {
@ -32,6 +34,7 @@ export const KanbanColumn = ({
column,
members,
etapes,
etapeColors,
onTaskClick,
onAddTask,
onAddTaskInline,
@ -71,22 +74,24 @@ export const KanbanColumn = ({
Aucune tâche
</div>
) : (
column.tasks.map((task: KanbanTask) => (
<div
key={task.id}
draggable
onDragStart={(e) => onDragStart(e, task)}
className="cursor-move"
>
<KanbanTaskCard
task={task}
etapeTitle={
etapes.find((etape) => etape.id === task.parent_task_id)?.title ?? undefined
}
onClick={() => onTaskClick(task)}
/>
</div>
))
column.tasks.map((task: KanbanTask) => {
const etape = etapes.find((e) => e.id === task.parent_task_id);
return (
<div
key={task.id}
draggable
onDragStart={(e) => onDragStart(e, task)}
className="cursor-move"
>
<KanbanTaskCard
task={task}
etapeTitle={etape?.title}
etapeColor={task.parent_task_id ? etapeColors[task.parent_task_id] : undefined}
onClick={() => onTaskClick(task)}
/>
</div>
);
})
)}
</div>

View file

@ -1,14 +1,16 @@
import type { KanbanTask } from "@xtablo/shared-types";
import { TypographyH4, TypographyMuted } from "@xtablo/ui/components/typography";
import { User } from "lucide-react";
import type { getEtapeColor } from "../../utils/etapeColors";
interface KanbanTaskCardProps {
task: KanbanTask;
etapeTitle?: string;
etapeColor?: ReturnType<typeof getEtapeColor>;
onClick: () => void;
}
export const KanbanTaskCard = ({ task, etapeTitle, onClick }: KanbanTaskCardProps) => {
export const KanbanTaskCard = ({ task, etapeTitle, etapeColor, onClick }: KanbanTaskCardProps) => {
return (
<div
onClick={onClick}
@ -24,11 +26,13 @@ export const KanbanTaskCard = ({ task, etapeTitle, onClick }: KanbanTaskCardProp
</TypographyMuted>
)}
{/* Status Pill */}
{/* Status Pill with Etape Color */}
<div className="mb-2">
<span
className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${
etapeTitle ? "bg-primary/10 text-primary" : "bg-muted text-muted-foreground"
className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium border ${
etapeColor
? `${etapeColor.bg} ${etapeColor.text} ${etapeColor.border}`
: "bg-muted text-muted-foreground border-muted"
}`}
>
{etapeTitle ?? "Sans Étape"}

View file

@ -1,4 +1,3 @@
import { AnimatedBackground } from "@ui/components/AnimatedBackground";
import { LoginWithGoogle } from "@ui/components/BrandButtons/LoginWithGoogle";
import { useTheme } from "@xtablo/shared/contexts/ThemeContext";
import { Button } from "@xtablo/ui/components/button";
@ -11,6 +10,7 @@ import { useTranslation } from "react-i18next";
import { Link, useSearchParams } from "react-router-dom";
import { twMerge } from "tailwind-merge";
import { useLoginEmail } from "../hooks/auth";
import { ThreeLoginBackground } from "../components/ThreeLoginBackground";
export function LoginPage() {
const [searchParams] = useSearchParams();
@ -96,7 +96,8 @@ export function LoginPage() {
return (
<div className="min-h-screen flex items-center justify-center bg-linear-to-br from-primary/10 via-background to-secondary/5 animate-gradient-x bg-size-[400%_400%] relative overflow-hidden">
<AnimatedBackground />
<ThreeLoginBackground />
{/* <AnimatedBackground /> */}
<div
ref={cardRef}
className={twMerge(

View file

@ -0,0 +1,40 @@
/**
* Get color classes for an etape based on its position
* Colors cycle through: yellow (light), green, blue, gray
*/
export const getEtapeColor = (position: number) => {
const colors = [
{
// Light/Yellow
bg: "bg-yellow-100 dark:bg-yellow-950/30",
text: "text-yellow-700 dark:text-yellow-400",
border: "border-yellow-200 dark:border-yellow-900/50",
indicator: "bg-yellow-400 dark:bg-yellow-500",
},
{
// Green
bg: "bg-green-100 dark:bg-green-950/30",
text: "text-green-700 dark:text-green-400",
border: "border-green-200 dark:border-green-900/50",
indicator: "bg-green-400 dark:bg-green-500",
},
{
// Blue
bg: "bg-blue-100 dark:bg-blue-950/30",
text: "text-blue-700 dark:text-blue-400",
border: "border-blue-200 dark:border-blue-900/50",
indicator: "bg-blue-400 dark:bg-blue-500",
},
{
// Gray
bg: "bg-gray-100 dark:bg-gray-800/30",
text: "text-gray-700 dark:text-gray-400",
border: "border-gray-200 dark:border-gray-700/50",
indicator: "bg-gray-400 dark:bg-gray-500",
},
];
return colors[position % colors.length];
};

File diff suppressed because one or more lines are too long

View file

@ -154,3 +154,4 @@ For a fresh setup:

View file

@ -324,3 +324,4 @@ All standard Stripe objects synced automatically:

View file

@ -211,3 +211,4 @@ However, this is not recommended as the old implementation had incorrect logic.

View file

@ -281,3 +281,4 @@ await stripeSync.syncSubscriptions();

View file

@ -203,3 +203,4 @@ All 142 tests are now passing with proper authentication logic being tested. The

View file

@ -350,6 +350,9 @@ importers:
stream-chat-react:
specifier: ^13.1.0
version: 13.9.0(@emoji-mart/data@1.2.1)(@types/react@19.0.10)(emoji-mart@5.6.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(stream-chat@9.24.0)(typescript@5.9.3)
three:
specifier: ^0.172.0
version: 0.172.0
ts-pattern:
specifier: ^5.6.2
version: 5.8.0
@ -414,6 +417,9 @@ importers:
'@types/react-dom':
specifier: 19.0.4
version: 19.0.4(@types/react@19.0.10)
'@types/three':
specifier: ^0.181.0
version: 0.181.0
'@typescript-eslint/eslint-plugin':
specifier: ^7.0.2
version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)
@ -1292,6 +1298,9 @@ packages:
'@date-fns/tz@1.4.1':
resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==}
'@dimforge/rapier3d-compat@0.12.0':
resolution: {integrity: sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==}
'@emnapi/runtime@1.6.0':
resolution: {integrity: sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA==}
@ -3961,6 +3970,9 @@ packages:
'@tsconfig/node16@1.0.4':
resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==}
'@tweenjs/tween.js@23.1.3':
resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==}
'@types/aria-query@5.0.4':
resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==}
@ -4092,6 +4104,12 @@ packages:
'@types/stack-utils@2.0.3':
resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==}
'@types/stats.js@0.17.4':
resolution: {integrity: sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==}
'@types/three@0.181.0':
resolution: {integrity: sha512-MLF1ks8yRM2k71D7RprFpDb9DOX0p22DbdPqT/uAkc6AtQXjxWCVDjCy23G9t1o8HcQPk7woD2NIyiaWcWPYmA==}
'@types/tough-cookie@4.0.5':
resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==}
@ -4107,6 +4125,9 @@ packages:
'@types/use-sync-external-store@0.0.6':
resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==}
'@types/webxr@0.5.24':
resolution: {integrity: sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==}
'@types/whatwg-mimetype@3.0.2':
resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==}
@ -4356,6 +4377,9 @@ packages:
'@vitest/utils@4.0.8':
resolution: {integrity: sha512-pdk2phO5NDvEFfUTxcTP8RFYjVj/kfLSPIN5ebP2Mu9kcIMeAQTbknqcFEyBcC4z2pJlJI9aS5UQjcYfhmKAow==}
'@webgpu/types@0.1.66':
resolution: {integrity: sha512-YA2hLrwLpDsRueNDXIMqN9NTzD6bCDkuXbOSe0heS+f8YE8usA6Gbv1prj81pzVHrbaAma7zObnIC+I6/sXJgA==}
abab@2.0.6:
resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==}
deprecated: Use your platform's native atob() and btoa() methods instead
@ -6517,6 +6541,9 @@ packages:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
meshoptimizer@0.22.0:
resolution: {integrity: sha512-IebiK79sqIy+E4EgOr+CAw+Ke8hAspXKzBd0JdgEmPHiAwmvEj2S4h1rfvo+o/BnfEYd/jAOg5IeeIjzlzSnDg==}
micromark-core-commonmark@2.0.3:
resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==}
@ -8005,6 +8032,9 @@ packages:
thread-stream@3.1.0:
resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==}
three@0.172.0:
resolution: {integrity: sha512-6HMgMlzU97MsV7D/tY8Va38b83kz8YJX+BefKjspMNAv0Vx6dxMogHOrnRl/sbMIs3BPUKijPqDqJ/+UwJbIow==}
through@2.3.8:
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
@ -9739,6 +9769,8 @@ snapshots:
'@date-fns/tz@1.4.1': {}
'@dimforge/rapier3d-compat@0.12.0': {}
'@emnapi/runtime@1.6.0':
dependencies:
tslib: 2.8.1
@ -12919,6 +12951,8 @@ snapshots:
'@tsconfig/node16@1.0.4': {}
'@tweenjs/tween.js@23.1.3': {}
'@types/aria-query@5.0.4': {}
'@types/babel__core@7.20.5':
@ -13076,6 +13110,18 @@ snapshots:
'@types/stack-utils@2.0.3': {}
'@types/stats.js@0.17.4': {}
'@types/three@0.181.0':
dependencies:
'@dimforge/rapier3d-compat': 0.12.0
'@tweenjs/tween.js': 23.1.3
'@types/stats.js': 0.17.4
'@types/webxr': 0.5.24
'@webgpu/types': 0.1.66
fflate: 0.8.2
meshoptimizer: 0.22.0
'@types/tough-cookie@4.0.5': {}
'@types/trusted-types@2.0.7':
@ -13087,6 +13133,8 @@ snapshots:
'@types/use-sync-external-store@0.0.6': {}
'@types/webxr@0.5.24': {}
'@types/whatwg-mimetype@3.0.2': {}
'@types/ws@8.18.1':
@ -13417,6 +13465,8 @@ snapshots:
'@vitest/pretty-format': 4.0.8
tinyrainbow: 3.0.3
'@webgpu/types@0.1.66': {}
abab@2.0.6: {}
abort-controller@3.0.0:
@ -16244,6 +16294,8 @@ snapshots:
merge2@1.4.1: {}
meshoptimizer@0.22.0: {}
micromark-core-commonmark@2.0.3:
dependencies:
decode-named-character-reference: 1.2.0
@ -18075,6 +18127,8 @@ snapshots:
dependencies:
real-require: 0.2.0
three@0.172.0: {}
through@2.3.8: {}
tiny-async-pool@2.1.0: {}