From fbb9158684fb28c98e1bdf43bfe8367576f0d0c1 Mon Sep 17 00:00:00 2001 From: Nathan Lamy Date: Tue, 23 Dec 2025 15:15:11 +0100 Subject: [PATCH] feat: add stress --- app/components/ui/radio-group.tsx | 45 ++++ app/components/ui/text-typing.tsx | 180 +++++++++++++ app/lib/get-strict-context.tsx | 36 +++ app/lib/use-is-in-view.tsx | 25 ++ app/routes.ts | 1 + app/routes/stress.tsx | 435 ++++++++++++++++++++++++++++++ package.json | 2 + pnpm-lock.yaml | 124 +++++++++ 8 files changed, 848 insertions(+) create mode 100644 app/components/ui/radio-group.tsx create mode 100644 app/components/ui/text-typing.tsx create mode 100644 app/lib/get-strict-context.tsx create mode 100644 app/lib/use-is-in-view.tsx create mode 100644 app/routes/stress.tsx diff --git a/app/components/ui/radio-group.tsx b/app/components/ui/radio-group.tsx new file mode 100644 index 0000000..b1c5667 --- /dev/null +++ b/app/components/ui/radio-group.tsx @@ -0,0 +1,45 @@ +"use client" + +import * as React from "react" +import * as RadioGroupPrimitive from "@radix-ui/react-radio-group" +import { CircleIcon } from "lucide-react" + +import { cn } from "~/lib/utils" + +function RadioGroup({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function RadioGroupItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + + ) +} + +export { RadioGroup, RadioGroupItem } diff --git a/app/components/ui/text-typing.tsx b/app/components/ui/text-typing.tsx new file mode 100644 index 0000000..6f1776e --- /dev/null +++ b/app/components/ui/text-typing.tsx @@ -0,0 +1,180 @@ +"use client"; + +import * as React from "react"; +import { motion, type HTMLMotionProps } from "motion/react"; + +import { useIsInView, type UseIsInViewOptions } from "~/lib/use-is-in-view"; +import { getStrictContext } from "~/lib/get-strict-context"; + +type TypingTextContextType = { + isTyping: boolean; + setIsTyping: (isTyping: boolean) => void; +}; + +const [TypingTextProvider, useTypingText] = + getStrictContext("TypingTextContext"); + +type TypingTextProps = React.ComponentProps<"span"> & { + duration?: number; + delay?: number; + loop?: boolean; + holdDelay?: number; + text: string | string[]; +} & UseIsInViewOptions; + +function TypingText({ + ref, + children, + duration = 100, + delay = 0, + inView = false, + inViewMargin = "0px", + inViewOnce = true, + loop = false, + holdDelay = 1000, + text, + ...props +}: TypingTextProps) { + const { ref: localRef, isInView } = useIsInView( + ref as React.Ref, + { + inView, + inViewOnce, + inViewMargin, + } + ); + + const [isTyping, setIsTyping] = React.useState(false); + const [started, setStarted] = React.useState(false); + const [displayedText, setDisplayedText] = React.useState(""); + + React.useEffect(() => { + if (isInView) { + const timeoutId = setTimeout(() => { + setStarted(true); + }, delay); + return () => clearTimeout(timeoutId); + } + }, [isInView, delay]); + + React.useEffect(() => { + if (!started) return; + + const timeoutIds: Array> = []; + const texts: string[] = typeof text === "string" ? [text] : text; + + const typeText = (str: string, onComplete: () => void) => { + setIsTyping(true); + let currentIndex = 0; + const type = () => { + if (currentIndex <= str.length) { + setDisplayedText(str.substring(0, currentIndex)); + currentIndex++; + const id = setTimeout(type, duration); + timeoutIds.push(id); + } else { + setIsTyping(false); + onComplete(); + } + }; + type(); + }; + + const eraseText = (str: string, onComplete: () => void) => { + setIsTyping(true); + let currentIndex = str.length; + const erase = () => { + if (currentIndex >= 0) { + setDisplayedText(str.substring(0, currentIndex)); + currentIndex--; + const id = setTimeout(erase, duration); + timeoutIds.push(id); + } else { + setIsTyping(false); + onComplete(); + } + }; + erase(); + }; + + const animateTexts = (index: number) => { + typeText(texts[index] ?? "", () => { + const isLast = index === texts.length - 1; + if (isLast && !loop) { + return; + } + const id = setTimeout(() => { + eraseText(texts[index] ?? "", () => { + const nextIndex = isLast ? 0 : index + 1; + animateTexts(nextIndex); + }); + }, holdDelay); + timeoutIds.push(id); + }); + }; + + animateTexts(0); + + return () => { + timeoutIds.forEach(clearTimeout); + }; + }, [text, duration, started, loop, holdDelay]); + + return ( + + + {displayedText} + {children} + + + ); +} + +type TypingTextCursorProps = Omit, "children">; + +function TypingTextCursor({ + style, + variants, + ...props +}: TypingTextCursorProps) { + const { isTyping } = useTypingText(); + + return ( + + ); +} + +export { + TypingText, + TypingTextCursor, + type TypingTextProps, + type TypingTextCursorProps, +}; diff --git a/app/lib/get-strict-context.tsx b/app/lib/get-strict-context.tsx new file mode 100644 index 0000000..1af7c47 --- /dev/null +++ b/app/lib/get-strict-context.tsx @@ -0,0 +1,36 @@ +import * as React from "react"; + +function getStrictContext( + name?: string +): readonly [ + ({ + value, + children, + }: { + value: T; + children?: React.ReactNode; + }) => React.JSX.Element, + () => T +] { + const Context = React.createContext(undefined); + + const Provider = ({ + value, + children, + }: { + value: T; + children?: React.ReactNode; + }) => {children}; + + const useSafeContext = () => { + const ctx = React.useContext(Context); + if (ctx === undefined) { + throw new Error(`useContext must be used within ${name ?? "a Provider"}`); + } + return ctx; + }; + + return [Provider, useSafeContext] as const; +} + +export { getStrictContext }; diff --git a/app/lib/use-is-in-view.tsx b/app/lib/use-is-in-view.tsx new file mode 100644 index 0000000..ac2a94a --- /dev/null +++ b/app/lib/use-is-in-view.tsx @@ -0,0 +1,25 @@ +import * as React from "react"; +import { useInView, type UseInViewOptions } from "motion/react"; + +interface UseIsInViewOptions { + inView?: boolean; + inViewOnce?: boolean; + inViewMargin?: UseInViewOptions["margin"]; +} + +function useIsInView( + ref: React.Ref, + options: UseIsInViewOptions = {} +) { + const { inView, inViewOnce = false, inViewMargin = "0px" } = options; + const localRef = React.useRef(null); + React.useImperativeHandle(ref, () => localRef.current as T); + const inViewResult = useInView(localRef, { + once: inViewOnce, + margin: inViewMargin, + }); + const isInView = !inView || inViewResult; + return { ref: localRef, isInView }; +} + +export { useIsInView, type UseIsInViewOptions }; diff --git a/app/routes.ts b/app/routes.ts index ab5923e..103f06e 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -9,4 +9,5 @@ export default [ route("/settings", "routes/settings.tsx"), route("/grades", "routes/grades.tsx"), route("/repas", "routes/repas.tsx"), + route("/stress", "routes/stress.tsx"), ] satisfies RouteConfig; diff --git a/app/routes/stress.tsx b/app/routes/stress.tsx new file mode 100644 index 0000000..f28b775 --- /dev/null +++ b/app/routes/stress.tsx @@ -0,0 +1,435 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Info } from "lucide-react"; +import { Button } from "~/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "~/components/ui/dialog"; +import { Label } from "~/components/ui/label"; +import { RadioGroup, RadioGroupItem } from "~/components/ui/radio-group"; +import { TypingText, TypingTextCursor } from "~/components/ui/text-typing"; + +const ENCOURAGING_MESSAGES = [ + // --- Les Classiques (Gardés) --- + "Tu es plus fort(e) que tu ne le penses. Continue comme ça ! 💪", + "Chaque jour est un pas de plus vers ton objectif. Bravo ! 🌟", + "Ta persévérance est admirable. Tu vas réussir ! 🎯", + "N'oublie pas de te reposer, le sommeil fixe la mémoire ! 😌", + "Les meilleurs résultats viennent avec le temps. Patience ! ⏰", + "Tu fais un travail incroyable. Continue ainsi ! 🚀", + "Crois en toi, tu as tout pour réussir ! ✨", + "Chaque exercice résolu te rapproche de ton rêve ! 📚", + "Ton travail acharné paiera, garde confiance ! 🌈", + "Tu es sur la bonne voie. Reste concentré(e) ! 🎓", + "Prends soin de toi, ta santé mentale est ta meilleure arme ! 💙", + "Ta détermination est inspirante ! 🔥", + "Les concours ne sont qu'une étape, tu es déjà quelqu'un de formidable ! 🌸", + "N'aie pas peur des erreurs, c'est là que tu apprends le plus ! 📝", + "Tu progresses chaque jour, même les jours où tu as l'impression de reculer ! 🌱", + "Ta famille et tes amis sont fiers de toi ! 💖", + "Le meilleur est à venir, l'école de tes rêves t'attend ! 🌅", + "Chaque difficulté surmontée te rend plus fort(e) ! 💎", + "Tu as choisi un chemin difficile, sois fier(e) de le parcourir ! 🏔️", + "N'oublie pas pourquoi tu as commencé. Tu vas y arriver ! 🎪", + + // --- Les Nouveaux (Spécial Prépa) --- + "C'est un marathon, pas un sprint. Respire. 🏃", + "Même les major de promo doutent. Toi, tu avances. 🛡️", + "Une mauvaise note en khôlle ne définit pas ta valeur. Relève-toi. 🦋", + "Rappelle-toi la sensation de comprendre enfin une démo complexe. Cherche ça ! 💡", + "Tu es en train de te forger une capacité de travail pour la vie. 🧠", + "L'effort d'aujourd'hui est le sourire de toi-même en juillet. ☀️", + "Fais-le pour le 'toi' qui recevra son affectation SIGEM. 📩", + "La douleur de la discipline pèse moins lourd que celle du regret. ⚖️", + "Tu n'as pas besoin d'être un génie, juste d'être constant. 🐢", + "Visualise ton nom sur la liste des admis. Ça arrive. 👁️", + "Ce chapitre qui te résiste finira par céder. Insiste. 🔨", + "Tu n'es pas seul(e), des milliers d'autres galèrent aussi. Courage ! 🤝", + "Prends 5 minutes pour t'aérer, tu seras plus efficace après. 🌳", + "La prépa, c'est l'école de l'humilité et de la résilience. Tu gères. 🧘", + "Chaque théorème appris est une munition pour le jour J. 🔫", + "Ne te compare pas aux autres, compare-toi à toi hier. 📈", + "Tu construis ton avenir, brique par brique, DM par DM. 🏗️", + "C'est normal d'être fatigué(e), tu accomplis quelque chose de grand. 🛌", + "Garde la tête froide et le cœur chaud. ❤️", + "Il n'y a pas de question bête, il n'y a que des points à gagner. 🙋", + "Le déclic arrive souvent quand on s'y attend le moins. ⚡", + "Sois bienveillant(e) avec toi-même, tu es ton propre moteur. 🚗", + "Regarde tout le chemin parcouru depuis le début de l'année ! 🗺️", + "Tu es capable de comprendre des choses que 99% des gens ignorent. 🌌", + "L'intégration n'est pas une question de chance, c'est une question de ténacité. 🍀", + "Buvard ou Major, l'important c'est de tout donner. 🦁", + "Ton cerveau est un muscle, et là, tu fais de la muscu de haut niveau. 🏋️", + "Rien n'est joué jusqu'à la dernière épreuve. Fonce ! 🏁", + "La réussite, c'est tomber 7 fois et se relever 8. 🩹", + "Fais confiance à ta préparation. Tu as bossé pour ça. 🧱", + "Un jour, tu riras de cette période avec tes potes d'école. 🍻", + "Tu n'étudies pas pour le prof, tu étudies pour ta liberté future. 🕊️", + "Accroche-toi, la vue est superbe depuis le sommet. ⛰️", + "Ta capacité à encaisser la charge de travail est impressionnante. 🔋", + "Ne laisse pas un DS raté gâcher ta semaine. Reset. 🔄", + "Tu es l'architecte de ta propre réussite. 📐", + "Chaque minute investie maintenant te rapportera des années de confort. 🏦", + "Tu as le droit de craquer, mais tu as le devoir de revenir. 🥊", + "Pense à la fierté de porter le tricorne, le képi ou l'uniforme. 🧢", + "La prépa t'apprend à réfléchir, personne ne pourra t'enlever ça. 🗝️", + "C'est dur ? C'est que tu es en train de monter de niveau. 🆙", + "Garde le cap, la tempête finit toujours par passer. ⛵", + "Tu es bien plus intelligent(e) que tu ne le crois sous stress. 🧠", + "Fais-toi un bon thé/café et remets-toi en selle. ☕", + "Le succès est la somme de petits efforts répétés jour après jour. ➕", + "Ne lâche rien, l'admissibilité se joue parfois à des détails. 🔍", + "Tu as le potentiel pour intégrer l'école qui te correspond. 🧩", + "Respire par le ventre. Tout va bien se passer. 🌬️", + "Ta volonté est ton plus grand talent. ⭐", + "Allez, encore un petit effort. Tu es un(e) guerrier(e). ⚔️", +]; + +const STRESSFUL_MESSAGES = [ + // --- Les Classiques (Gardés) --- + "Pendant que tu scroll, tes concurrents bossent les intégrales. 📉", + "Netflix ou les concours ? Choisis vite. ⏳", + "Les places à l'X ne se gagnent pas sur TikTok. 📱", + "Ton futur toi te regarde avec déception là. 👀", + "Les autres sont déjà au chapitre suivant... 📖", + "Ce DM ne va pas se faire tout seul. Allez hop ! 📝", + "Chaque minute perdue est une place de perdue au classement. ⚠️", + "Tu veux vraiment passer 5/2 ? Bouge-toi ! 🏃", + "Tes notes ne vont pas s'améliorer par magie. 📊", + "La médiocrité est confortable, mais l'X c'est mieux. 🤔", + "Tes concurrents ne prennent pas de pause, eux. ⚡", + "Le niveau monte, et toi tu stagnes. Réagis ! 📈", + "À ce rythme, tu vas finir à la fac (non je rigole... ou pas). 🔄", + "Les colles arrivent, es-tu prêt(e) ? Spoiler : non. 😬", + "Tu sais combien il reste de jours ? C'est terrifiant, non ? ⏰", + "Pendant que tu lis ce message, 3 exos auraient pu être pliés. 🤷", + "La motivation c'est pour les faibles, la discipline c'est pour les forts. 💼", + "Tu te rappelles pourquoi tu es en prépa ? Prouve-le ! 🎯", + "Les profs ont raison : tu gâches ton potentiel là. 📚", + "C'est pas avec des 'demain je m'y mets' que tu vas intégrer Centrale. 🚫", + + // --- Les Nouveaux (Spécial "Violence") --- + "Tu vises l'ENS ou l'E3A là ? Ton attitude me met le doute. 🤨", + "Ton major de promo a déjà fini le sujet de 2018, lui. 🏎️", + "C'est pas en regardant le plafond que le théorème va rentrer. 🧱", + "Si tu ne bosses pas maintenant, prépare ton dossier Parcoursup. 📂", + "Dors ou travaille. Tout le reste est une perte de temps. 💤", + "Arrête de te mentir, tu n'es pas en 'pause', tu procrastines. 🤥", + "L'examinateur s'en fiche que tu sois fatigué. Il veut le résultat. 👹", + "Tu crois que Cauchy a procrastiné pour ses suites ? Non. 🔢", + "Ta mention au bac ne te sauvera pas aux concours. 🔥", + "La 5/2, c'est bien, mais intégrer en 3/2 c'est mieux. 🦄", + "Imagine ta tête devant le sujet si tu continues comme ça. 😱", + "Ton téléphone est ton pire ennemi. Éteins-le. 📵", + "Moins de story, plus de théorie. 📸", + "Tu veux finir ingénieur ou touriste ? 👷", + "Les Mines de Paris ne recrutent pas sur Candy Crush. 🍬", + "C'est le moment de souffrir pour ne pas pleurer en juillet. 😭", + "Tu connais tes formules de trigo ? J'en doute. Vérifie. 📐", + "Le jury ne fera pas de cadeau. Pourquoi tu t'en fais ? 🎁", + "Tu as vu le sujet de l'an dernier ? T'es pas prêt. 📉", + "Arrête de rêver ta vie, va étudier ta physique. ⚛️", + "La chimie orga ne s'apprend pas par osmose. 🧪", + "Chaque heure de perdue est une victoire pour ton concurrent direct. 🤺", + "C'est le moment de passer en mode machine. 🤖", + "Tu te reposeras après les écrits. Pour l'instant, au charbon. ⛏️", + "Si c'était facile, tout le monde serait à Polytechnique. 🏰", + "Ton excuse pour ne pas bosser est nulle. Retourne travailler. 🚮", + "L'avenir appartient à ceux qui se lèvent tôt (et qui font des maths). 🌅", + "Tu préfères la douleur de l'effort ou la douleur de l'échec ? 🎭", + "C'est pas le moment de flancher, c'est le money time ! 💰", + "Regarde tes notes de la dernière khôlle. Ça te suffit ? 📉", + "Tu n'es pas fatigué, tu es juste démotivé. Change ça. 🔋", + "La chance ne sourit qu'aux esprits bien préparés. 🍀", + "Tu veux voir tes amis intégrer pendant que tu redoubles ? 👯", + "Fais-le pour humilier ceux qui n'ont pas cru en toi. 😈", + "Pas de bras, pas de chocolat. Pas de travail, pas d'école. 🍫", + "Ton avenir se joue dans les 30 prochaines minutes. ⏳", + "L'excellence est une habitude, pas un acte isolé. 🏆", + "Arrête de chouiner et va faire un DL. 🔢", + "Tu as déjà oublié ton cours d'hier ? C'est grave. 🧠", + "Le concours blanc approche. La panique aussi. 🚨", + "Tu penses que tu as le niveau ? Prouve-le sur une feuille blanche. 📄", + "C'est pas le moment de faire du social. C'est la guerre. ⚔️", + "Tu veux une médaille ? Va la chercher avec tes dents. 🏅", + "La prépa c'est la jungle. Mange ou sois mangé. 🦁", + "Ton lit est confortable, mais le chômage l'est moins. 🛋️", + "Si tu lis ça, c'est que tu ne bosses pas. Allez, oust ! 👉", + "Tu as 5 minutes pour t'y mettre, sinon c'est mort pour aujourd'hui. ⏲️", + "Ne sois pas celui qui dit 'j'aurais dû'. Sois celui qui l'a fait. ✅", + "La physique quantique t'attend. Elle n'est pas patiente. 🌌", + "Arrête de scroller, ça ne va pas augmenter ton admissibilité. 🛑", +]; + +export default function StressPrepaPage() { + const [timeLeft, setTimeLeft] = useState({ + days: 0, + hours: 0, + minutes: 0, + seconds: 0, + }); + const [progress, setProgress] = useState(0); + const [messageMode, setMessageMode] = useState< + "encouraging" | "stressful" | "none" + >("none"); + const [messages, setMessages] = useState([]); + + useEffect(() => { + const examDate = new Date("2026-04-13T07:00:00"); + const schoolYearStart = new Date("2025-09-01T00:00:00"); + const storedMode = localStorage.getItem("stress-prepa-message-mode"); + if ( + storedMode === "encouraging" || + storedMode === "stressful" || + storedMode === "none" + ) { + setMessageMode(storedMode); + } + + const updateCountdown = () => { + const now = new Date(); + const difference = examDate.getTime() - now.getTime(); + + if (difference > 0) { + const days = Math.floor(difference / (1000 * 60 * 60 * 24)); + const hours = Math.floor((difference / (1000 * 60 * 60)) % 24); + const minutes = Math.floor((difference / 1000 / 60) % 60); + const seconds = Math.floor((difference / 1000) % 60); + + setTimeLeft({ days, hours, minutes, seconds }); + + // Calculate progress percentage + const totalDuration = examDate.getTime() - schoolYearStart.getTime(); + const elapsed = now.getTime() - schoolYearStart.getTime(); + const progressPercent = Math.min( + Math.max((elapsed / totalDuration) * 100, 0), + 100 + ); + setProgress(progressPercent); + } else { + setTimeLeft({ days: 0, hours: 0, minutes: 0, seconds: 0 }); + setProgress(100); + } + }; + + updateCountdown(); + const interval = setInterval(updateCountdown, 1000); + + return () => clearInterval(interval); + }, []); + + useEffect(() => { + if (messageMode === "none") { + setMessages([]); + return; + } + const messages = + messageMode === "encouraging" ? ENCOURAGING_MESSAGES : STRESSFUL_MESSAGES; + setMessages(messages); + }, [messageMode]); + + const formatNumber = (num: number) => num.toString().padStart(2, "0"); + + return ( +
+ {/* Animated gradient background */} +
+ + {/* Enhanced grid pattern */} +
+
+
+ + + + + + + + + Stress Prépa + + +
+

+ Compte à rebours jusqu'aux concours +

+
+
+ +
+
+ + Début des concours + + + 13 Avril 2026 + +
+
+ + Début de l'année + + + 1er Sept. 2025 + +
+
+ +
+
+
+ + { + localStorage.setItem("stress-prepa-message-mode", value); + setMessageMode( + value as "encouraging" | "stressful" | "none" + ); + }} + className="space-y-0" + > +
+ + +
+
+ + +
+
+ + +
+
+
+
+ +
+
+

+ Tous droits réservés Khollisé © {new Date().getFullYear()} — + Messages générés avec l'intelligence artificielle. +

+
+ + + +
+ +
+ {/* Enhanced title section */} +
+

+ Temps avant les concours +

+
+ +
+
+
+ {timeLeft.days} +
+
+ Jour{timeLeft.days > 1 ? "s" : ""} +
+
+
+
+ {formatNumber(timeLeft.hours)} +
+
+ Heure{timeLeft.hours > 1 ? "s" : ""} +
+
+
+
+ {formatNumber(timeLeft.minutes)} +
+
+ Minute{timeLeft.minutes > 1 ? "s" : ""} +
+
+
+
+ {formatNumber(timeLeft.seconds)} +
+
+ Seconde{timeLeft.seconds > 1 ? "s" : ""} +
+
+
+ +
+ {messages.length > 0 && ( + + + + )} +
+ +
+
+
+
+
+ + Progression de l'année + + + {progress.toFixed(2)}% + +
+
+
+
+ ); +} diff --git a/package.json b/package.json index 1a5c65f..0e64462 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@radix-ui/react-label": "^2.1.6", "@radix-ui/react-navigation-menu": "^1.2.14", "@radix-ui/react-popover": "^1.1.14", + "@radix-ui/react-radio-group": "^1.3.8", "@radix-ui/react-select": "^2.2.5", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", @@ -41,6 +42,7 @@ "katex": "^0.16.22", "lucide-react": "^0.511.0", "luxon": "^3.7.1", + "motion": "^12.23.26", "netlify-cli": "^22.1.3", "react": "^19.1.0", "react-day-picker": "^9.8.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 99a9daf..497e747 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,6 +38,9 @@ importers: '@radix-ui/react-popover': specifier: ^1.1.14 version: 1.1.14(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-radio-group': + specifier: ^1.3.8 + version: 1.3.8(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@radix-ui/react-select': specifier: ^2.2.5 version: 2.2.5(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) @@ -101,6 +104,9 @@ importers: luxon: specifier: ^3.7.1 version: 3.7.1 + motion: + specifier: ^12.23.26 + version: 12.23.26(react-dom@19.1.1(react@19.1.1))(react@19.1.1) netlify-cli: specifier: ^22.1.3 version: 22.4.0(@types/node@20.19.9)(idb-keyval@6.2.2)(picomatch@4.0.3)(rollup@2.79.2) @@ -1965,6 +1971,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-radio-group@1.3.8': + resolution: {integrity: sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-roving-focus@1.1.10': resolution: {integrity: sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==} peerDependencies: @@ -1978,6 +1997,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-roving-focus@1.1.11': + resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-select@2.2.5': resolution: {integrity: sha512-HnMTdXEVuuyzx63ME0ut4+sEMYW6oouHWNGUZc7ddvUWIcfCva/AMoqEW/3wnEllriMWBa0RHspCYnfCWJQYmA==} peerDependencies: @@ -3871,6 +3903,20 @@ packages: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} + framer-motion@12.23.26: + resolution: {integrity: sha512-cPcIhgR42xBn1Uj+PzOyheMtZ73H927+uWPDVhUMqxy8UHt6Okavb6xIz9J/phFUHUj0OncR6UvMfJTXoc/LKA==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + fresh@0.5.2: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} @@ -4876,6 +4922,26 @@ packages: resolution: {integrity: sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==} engines: {node: '>= 0.8.0'} + motion-dom@12.23.23: + resolution: {integrity: sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==} + + motion-utils@12.23.6: + resolution: {integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==} + + motion@12.23.26: + resolution: {integrity: sha512-Ll8XhVxY8LXMVYTCfme27WH2GjBrCIzY4+ndr5QKxsK+YwCtOi2B/oBi5jcIbik5doXuWT/4KKDOVAZJkeY5VQ==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + move-file@3.1.0: resolution: {integrity: sha512-4aE3U7CCBWgrQlQDMq8da4woBWDGHioJFiOZ8Ie6Yq2uwYQ9V2kGhTz4x3u6Wc+OU17nw0yc3rJ/lQ4jIiPe3A==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -8694,6 +8760,24 @@ snapshots: '@types/react': 19.1.8 '@types/react-dom': 19.1.6(@types/react@19.1.8) + '@radix-ui/react-radio-group@1.3.8(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.1.8)(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + optionalDependencies: + '@types/react': 19.1.8 + '@types/react-dom': 19.1.6(@types/react@19.1.8) + '@radix-ui/react-roving-focus@1.1.10(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: '@radix-ui/primitive': 1.1.2 @@ -8711,6 +8795,23 @@ snapshots: '@types/react': 19.1.8 '@types/react-dom': 19.1.6(@types/react@19.1.8) + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.8)(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + optionalDependencies: + '@types/react': 19.1.8 + '@types/react-dom': 19.1.6(@types/react@19.1.8) + '@radix-ui/react-select@2.2.5(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: '@radix-ui/number': 1.1.1 @@ -10755,6 +10856,15 @@ snapshots: forwarded@0.2.0: {} + framer-motion@12.23.26(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + dependencies: + motion-dom: 12.23.23 + motion-utils: 12.23.6 + tslib: 2.8.1 + optionalDependencies: + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + fresh@0.5.2: {} from2@2.3.0: @@ -11740,6 +11850,20 @@ snapshots: transitivePeerDependencies: - supports-color + motion-dom@12.23.23: + dependencies: + motion-utils: 12.23.6 + + motion-utils@12.23.6: {} + + motion@12.23.26(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + dependencies: + framer-motion: 12.23.26(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + tslib: 2.8.1 + optionalDependencies: + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + move-file@3.1.0: dependencies: path-exists: 5.0.0