diff --git a/apps/timo-web/app/[locale]/login/_containers/LoginContainer.tsx b/apps/timo-web/app/[locale]/login/_containers/LoginContainer.tsx new file mode 100644 index 00000000..d998201d --- /dev/null +++ b/apps/timo-web/app/[locale]/login/_containers/LoginContainer.tsx @@ -0,0 +1,74 @@ +"use client"; + +import timoTextLogo from "@repo/timo-design-system/assets/images/logo/timo-text-logo.svg"; +import Image from "next/image"; +import { useTranslations } from "next-intl"; + +import { OnboardingGoogleButtonContainer } from "@/app/[locale]/onboarding/_containers/OnboardingGoogleButtonContainer"; +import { LottiePlayer } from "@/components/lottie/LottiePlayer"; + +export const LoginContainer = () => { + const t = useTranslations("Login"); + + return ( +
+ + +
+
+
+ Timo +
+

+ Less Chaos More Focus +

+ + {t("headline")} + +
+
+

+ {t("description")} +

+
+ +
+
+

+ {t("connectLabel")} +

+ { + // TODO: 백엔드 OAuth 로그인 URL 확정 후 리다이렉트 + }} + /> +
+ +

+ {t.rich("termsNotice", { + // TODO: 실제 이용약관 페이지 URL 나오면 href 교체 + terms: (chunks) => ( + // eslint-disable-next-line jsx-a11y/anchor-is-valid + + {chunks} + + ), + // TODO: 실제 개인정보 처리방침 페이지 URL 나오면 href 교체 + privacy: (chunks) => ( + // eslint-disable-next-line jsx-a11y/anchor-is-valid + + {chunks} + + ), + })} +

+
+
+
+ ); +}; diff --git a/apps/timo-web/app/[locale]/login/page.tsx b/apps/timo-web/app/[locale]/login/page.tsx new file mode 100644 index 00000000..4abaaf3e --- /dev/null +++ b/apps/timo-web/app/[locale]/login/page.tsx @@ -0,0 +1,5 @@ +import { LoginContainer } from "@/app/[locale]/login/_containers/LoginContainer"; + +export default function LoginPage() { + return ; +} diff --git a/apps/timo-web/app/[locale]/onboarding/_components/OnboardingButton.tsx b/apps/timo-web/app/[locale]/onboarding/_components/OnboardingButton.tsx index 7f4251ab..fc491756 100644 --- a/apps/timo-web/app/[locale]/onboarding/_components/OnboardingButton.tsx +++ b/apps/timo-web/app/[locale]/onboarding/_components/OnboardingButton.tsx @@ -5,7 +5,7 @@ import { } from "@repo/timo-design-system/icons"; import { cn } from "@repo/timo-design-system/utils"; -export interface OnboardingButtonProps { +interface OnboardingButtonProps { variant: "next" | "prev" | "start"; label: string; isActive?: boolean; @@ -53,7 +53,7 @@ export const OnboardingButton = ({ onClick={onClick} disabled={isDisabled} className={cn( - "flex items-center justify-center gap-2 rounded-[4px] px-4 py-2", + "flex items-center justify-center gap-2 rounded-[4px] px-4 py-2 transition-colors duration-200 ease-in-out", isActive ? "bg-timo-blue-300" : "bg-timo-gray-200", )} > diff --git a/apps/timo-web/app/[locale]/onboarding/_components/OnboardingSelectCard.tsx b/apps/timo-web/app/[locale]/onboarding/_components/OnboardingSelectCard.tsx index 3f7c0a99..3cff299a 100644 --- a/apps/timo-web/app/[locale]/onboarding/_components/OnboardingSelectCard.tsx +++ b/apps/timo-web/app/[locale]/onboarding/_components/OnboardingSelectCard.tsx @@ -1,6 +1,6 @@ import { cn } from "@repo/timo-design-system/utils"; -export interface OnboardingSelectCardProps { +interface OnboardingSelectCardProps { size: "sm" | "lg"; selected: boolean; label: string; @@ -31,7 +31,7 @@ export const OnboardingSelectCard = ({ type="button" onClick={onClick} className={cn( - "flex rounded-[4px] border px-4 py-2 text-left", + "flex rounded-[4px] border px-4 py-2 text-left transition-colors duration-200 ease-in-out", selected ? "border-timo-blue-300 bg-timo-blue-50" : "border-timo-gray-500 bg-white", diff --git a/apps/timo-web/app/[locale]/onboarding/_components/OnboardingStepButton.tsx b/apps/timo-web/app/[locale]/onboarding/_components/OnboardingStepButton.tsx index d2be88f1..afda1b95 100644 --- a/apps/timo-web/app/[locale]/onboarding/_components/OnboardingStepButton.tsx +++ b/apps/timo-web/app/[locale]/onboarding/_components/OnboardingStepButton.tsx @@ -19,7 +19,7 @@ export const OnboardingStepButton = ({ step }: OnboardingStepButtonProps) => { > diff --git a/apps/timo-web/app/[locale]/onboarding/_containers/CalendarConnectStepContainer.tsx b/apps/timo-web/app/[locale]/onboarding/_containers/CalendarConnectStepContainer.tsx new file mode 100644 index 00000000..45401d42 --- /dev/null +++ b/apps/timo-web/app/[locale]/onboarding/_containers/CalendarConnectStepContainer.tsx @@ -0,0 +1,65 @@ +"use client"; + +import { useTranslations } from "next-intl"; +import { useState } from "react"; + +import { OnboardingButtonContainer } from "@/app/[locale]/onboarding/_containers/OnboardingButtonContainer"; +import { OnboardingGoogleButtonContainer } from "@/app/[locale]/onboarding/_containers/OnboardingGoogleButtonContainer"; + +interface CalendarConnectStepContainerProps { + onPrev: () => void; + onStart: () => void; +} + +export const CalendarConnectStepContainer = ({ + onPrev, + onStart, +}: CalendarConnectStepContainerProps) => { + const t = useTranslations("Onboarding"); + const [isCalendarConnected, setIsCalendarConnected] = useState(false); + + return ( + <> +
+
+

+ {t("calendarConnect.title")} +

+

+ {t("calendarConnect.description")} +

+
+ +
+
+

+ {t("calendarConnect.connectLabel")} +

+ { + // TODO: 실제 구글 캘린더 OAuth 연동 (백엔드 API 확정 후) + setIsCalendarConnected(true); + }} + /> +
+ +
+

+ {t("calendarConnect.consentNotice")} +

+

+ {t("calendarConnect.permissionNotice")} +

+
+
+
+ +
+ + +
+ + ); +}; diff --git a/apps/timo-web/app/[locale]/onboarding/_containers/LanguageStepContainer.tsx b/apps/timo-web/app/[locale]/onboarding/_containers/LanguageStepContainer.tsx new file mode 100644 index 00000000..d6983ad9 --- /dev/null +++ b/apps/timo-web/app/[locale]/onboarding/_containers/LanguageStepContainer.tsx @@ -0,0 +1,61 @@ +"use client"; + +import { useTranslations } from "next-intl"; + +import { OnboardingSelectCard } from "@/app/[locale]/onboarding/_components/OnboardingSelectCard"; +import { OnboardingButtonContainer } from "@/app/[locale]/onboarding/_containers/OnboardingButtonContainer"; + +interface LanguageStepContainerProps { + language?: "ko" | "en"; + onSelect: (language: "ko" | "en" | undefined) => void; + onNext: () => void; +} + +export const LanguageStepContainer = ({ + language, + onSelect, + onNext, +}: LanguageStepContainerProps) => { + const t = useTranslations("Onboarding"); + + return ( + <> +
+
+

+ {t("language.title")} +

+

+ {t("language.description")} +

+
+ +
+ onSelect(language === "en" ? undefined : "en")} + /> + onSelect(language === "ko" ? undefined : "ko")} + /> +
+
+ +
+ +
+ + ); +}; diff --git a/apps/timo-web/app/[locale]/onboarding/_containers/LifePatternStepContainer.tsx b/apps/timo-web/app/[locale]/onboarding/_containers/LifePatternStepContainer.tsx new file mode 100644 index 00000000..1fdd46b8 --- /dev/null +++ b/apps/timo-web/app/[locale]/onboarding/_containers/LifePatternStepContainer.tsx @@ -0,0 +1,98 @@ +"use client"; + +import { AlertIcon, MoonIcon, SunIcon } from "@repo/timo-design-system/icons"; +import { useTranslations } from "next-intl"; + +import { OnboardingTimeDropdown } from "@/app/[locale]/onboarding/_components/OnboardingTimeDropdown"; +import { OnboardingButtonContainer } from "@/app/[locale]/onboarding/_containers/OnboardingButtonContainer"; + +interface LifePatternStepContainerProps { + wakeUpTime?: string; + bedTime?: string; + onSelectWakeUpTime: (time: string) => void; + onSelectBedTime: (time: string) => void; + onPrev: () => void; + onNext: () => void; +} + +export const LifePatternStepContainer = ({ + wakeUpTime, + bedTime, + onSelectWakeUpTime, + onSelectBedTime, + onPrev, + onNext, +}: LifePatternStepContainerProps) => { + const t = useTranslations("Onboarding"); + + const isBedTimeInvalid = Boolean( + wakeUpTime && bedTime && bedTime <= wakeUpTime, + ); + const canProceed = Boolean(wakeUpTime && bedTime && !isBedTimeInvalid); + + return ( + <> +
+
+

+ {t("lifePattern.title")} +

+

+ {t("lifePattern.description")} +

+
+ +
+
+
+
+ + + {t("lifePattern.wakeUpTime")} + +
+ +
+ +
+
+ + + {t("lifePattern.bedTime")} + +
+ +
+
+ + {isBedTimeInvalid && ( +
+ + + {t("lifePattern.bedTimeError")} + +
+ )} +
+
+ +
+ + +
+ + ); +}; diff --git a/apps/timo-web/app/[locale]/onboarding/_containers/OnboardingFunnelContainer.tsx b/apps/timo-web/app/[locale]/onboarding/_containers/OnboardingFunnelContainer.tsx new file mode 100644 index 00000000..634d1301 --- /dev/null +++ b/apps/timo-web/app/[locale]/onboarding/_containers/OnboardingFunnelContainer.tsx @@ -0,0 +1,121 @@ +"use client"; + +import { useFunnel } from "@use-funnel/browser"; +import { useState } from "react"; + +import { OnboardingStepButton } from "@/app/[locale]/onboarding/_components/OnboardingStepButton"; +import { CalendarConnectStepContainer } from "@/app/[locale]/onboarding/_containers/CalendarConnectStepContainer"; +import { LanguageStepContainer } from "@/app/[locale]/onboarding/_containers/LanguageStepContainer"; +import { LifePatternStepContainer } from "@/app/[locale]/onboarding/_containers/LifePatternStepContainer"; +import { TimePredictionStepContainer } from "@/app/[locale]/onboarding/_containers/TimePredictionStepContainer"; +import { OnboardingFunnelSteps } from "@/app/[locale]/onboarding/_types/onboarding-funnel"; +import { LottiePlayer } from "@/components/lottie/LottiePlayer"; + +const STEP_NUMBER: Record = { + Language: 1, + TimePrediction: 2, + LifePattern: 3, + CalendarConnect: 4, +}; + +export const OnboardingFunnelContainer = () => { + const [answers, setAnswers] = useState< + Partial + >({}); + + const funnel = useFunnel({ + id: "onboarding", + initial: { step: "Language", context: {} }, + }); + + return ( +
+ +
+
+ + + ( + + setAnswers((prev) => ({ ...prev, language })) + } + onNext={() => { + if (!answers.language) return; + history.push("TimePrediction", { + language: answers.language, + }); + }} + /> + )} + TimePrediction={({ history }) => ( + + setAnswers((prev) => ({ ...prev, predictionAccuracy })) + } + onPrev={() => history.back()} + onNext={() => { + if (!answers.language || !answers.predictionAccuracy) return; + history.push("LifePattern", { + language: answers.language, + predictionAccuracy: answers.predictionAccuracy, + }); + }} + /> + )} + LifePattern={({ history }) => ( + + setAnswers((prev) => ({ ...prev, wakeUpTime })) + } + onSelectBedTime={(bedTime) => + setAnswers((prev) => ({ ...prev, bedTime })) + } + onPrev={() => history.back()} + onNext={() => { + if ( + !answers.language || + !answers.predictionAccuracy || + !answers.wakeUpTime || + !answers.bedTime + ) + return; + history.push("CalendarConnect", { + language: answers.language, + predictionAccuracy: answers.predictionAccuracy, + wakeUpTime: answers.wakeUpTime, + bedTime: answers.bedTime, + }); + }} + /> + )} + CalendarConnect={({ history }) => ( + history.back()} + onStart={() => { + if ( + !answers.language || + !answers.predictionAccuracy || + !answers.wakeUpTime || + !answers.bedTime + ) + return; + // TODO: 실제 제출 API 연동 + 완료 후 answers.language로 locale 리다이렉트 + }} + /> + )} + /> +
+
+
+ ); +}; diff --git a/apps/timo-web/app/[locale]/onboarding/_containers/TimePredictionStepContainer.tsx b/apps/timo-web/app/[locale]/onboarding/_containers/TimePredictionStepContainer.tsx new file mode 100644 index 00000000..81a6488f --- /dev/null +++ b/apps/timo-web/app/[locale]/onboarding/_containers/TimePredictionStepContainer.tsx @@ -0,0 +1,65 @@ +"use client"; + +import { useTranslations } from "next-intl"; + +import { OnboardingSelectCard } from "@/app/[locale]/onboarding/_components/OnboardingSelectCard"; +import { OnboardingButtonContainer } from "@/app/[locale]/onboarding/_containers/OnboardingButtonContainer"; +import { PredictionAccuracy } from "@/app/[locale]/onboarding/_types/onboarding-funnel"; + +interface TimePredictionStepContainerProps { + predictionAccuracy?: PredictionAccuracy; + onSelect: (predictionAccuracy: PredictionAccuracy | undefined) => void; + onPrev: () => void; + onNext: () => void; +} + +const PREDICTION_ACCURACY_OPTIONS: PredictionAccuracy[] = [4, 3, 2, 1]; + +export const TimePredictionStepContainer = ({ + predictionAccuracy, + onSelect, + onPrev, + onNext, +}: TimePredictionStepContainerProps) => { + const t = useTranslations("Onboarding"); + + return ( + <> +
+
+

+ {t("timePrediction.title")} +

+

+ {t("timePrediction.description")} +

+
+ +
+ {PREDICTION_ACCURACY_OPTIONS.map((accuracy) => ( + + onSelect(predictionAccuracy === accuracy ? undefined : accuracy) + } + /> + ))} +
+
+ +
+ + +
+ + ); +}; diff --git a/apps/timo-web/app/[locale]/onboarding/_types/onboarding-funnel.ts b/apps/timo-web/app/[locale]/onboarding/_types/onboarding-funnel.ts new file mode 100644 index 00000000..65397303 --- /dev/null +++ b/apps/timo-web/app/[locale]/onboarding/_types/onboarding-funnel.ts @@ -0,0 +1,26 @@ +export type PredictionAccuracy = 1 | 2 | 3 | 4; + +// use-funnel의 CompareMergeContext가 keyof로 필드를 비교하는데, interface는 +// keyof가 예상대로 계산되지 않아 타입 추론이 깨짐 (반드시 type이어야 함) +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type OnboardingFunnelSteps = { + Language: { + language?: "ko" | "en"; + }; + TimePrediction: { + language: "ko" | "en"; + predictionAccuracy?: PredictionAccuracy; + }; + LifePattern: { + language: "ko" | "en"; + predictionAccuracy: PredictionAccuracy; + wakeUpTime?: string; + bedTime?: string; + }; + CalendarConnect: { + language: "ko" | "en"; + predictionAccuracy: PredictionAccuracy; + wakeUpTime: string; + bedTime: string; + }; +}; diff --git a/apps/timo-web/app/[locale]/onboarding/page.tsx b/apps/timo-web/app/[locale]/onboarding/page.tsx index e765d714..f862cf18 100644 --- a/apps/timo-web/app/[locale]/onboarding/page.tsx +++ b/apps/timo-web/app/[locale]/onboarding/page.tsx @@ -1,12 +1,5 @@ -import { LottiePlayer } from "@/components/lottie/LottiePlayer"; +import { OnboardingFunnelContainer } from "@/app/[locale]/onboarding/_containers/OnboardingFunnelContainer"; export default function OnboardingPage() { - return ( -
- -
- ); + return ; } diff --git a/apps/timo-web/messages/en.json b/apps/timo-web/messages/en.json index 10d34dab..7602db49 100644 --- a/apps/timo-web/messages/en.json +++ b/apps/timo-web/messages/en.json @@ -65,6 +65,13 @@ "todoLimitLine1": "You can add up to 20 incomplete to-dos.", "todoLimitLine2": "To add a new to-do, please complete an existing one." }, + "Login": { + "animationLabel": "Login animation", + "headline": "Manage your day with clear timeboxes.", + "description": "Please log in to your timo account.", + "connectLabel": "Log in with Google", + "termsNotice": "By continuing, you agree to the timo Terms of Service and Privacy Policy." + }, "Error": { "title": "A problem has occurred.", "description": "We are currently undergoing maintenance for a better experience. Please try again later.", @@ -89,6 +96,46 @@ "onboardingGoogleButton": { "login": "Continue with your Google account", "connectCalendar": "Connect Google Calendar" + }, + "language": { + "title": "Please select a language to use.", + "description": "timo will be displayed in your selected language.\nYou can change it later in settings." + }, + "timePrediction": { + "title": "Do you estimate your time well?", + "description": "timo provides personalized recommendations.", + "options": { + "4": { + "label": "Very accurate.", + "sublabel": "The estimated time matches the actual time." + }, + "3": { + "label": "Generally accurate.", + "sublabel": "Sometimes incorrect but generally accurate." + }, + "2": { + "label": "Often incorrect.", + "sublabel": "Always goes differently than expected." + }, + "1": { + "label": "I'm not sure.", + "sublabel": "I've never thought about it." + } + } + }, + "lifePattern": { + "title": "Set your wake-up and bedtime.", + "description": "The information you provide will only be used for time pattern analysis.", + "wakeUpTime": "Wake-up time", + "bedTime": "Bedtime", + "bedTimeError": "Please set your bedtime later than your wake-up time." + }, + "calendarConnect": { + "title": "Connect your Google Calendar.", + "description": "You can import your existing schedules into timo and manage them in one place.", + "connectLabel": "Google Calendar integration", + "consentNotice": "Clicking will display the Google authentication and calendar access permission screen.", + "permissionNotice": "timo only requests permission to view your schedules." } } } diff --git a/apps/timo-web/messages/ko.json b/apps/timo-web/messages/ko.json index 4a8ab223..6df0911b 100644 --- a/apps/timo-web/messages/ko.json +++ b/apps/timo-web/messages/ko.json @@ -65,6 +65,13 @@ "todoLimitLine1": "완료되지 않은 투두는 최대 20개까지 추가할 수 있어요.", "todoLimitLine2": "새로운 투두를 추가하려면 기존 투두를 완료해주세요." }, + "Login": { + "animationLabel": "로그인 애니메이션", + "headline": "흩어진 시간을, 실행 가능한 흐름으로.", + "description": "timo 계정에 로그인하세요.", + "connectLabel": "Google 로그인", + "termsNotice": "계속하면 timo 이용약관개인정보 처리방침에 동의하게 됩니다." + }, "Error": { "title": "잠시 문제가 발생했어요.", "description": "더 나은 경험을 위해 잠시 점검 중입니다. 잠시후 다시 이용해 주세요.", @@ -89,6 +96,43 @@ "onboardingGoogleButton": { "login": "Google 계정으로 계속하기", "connectCalendar": "Google 캘린더 연결하기" + }, + "language": { + "title": "사용할 언어를 선택해 주세요.", + "description": "선택한 언어로 timo의 모든 텍스트가 표시돼요.\n나중에 설정에서 변경할 수 있어요." + }, + "timePrediction": { + "title": "평소 작업 시간을\n잘 예측하는 편인가요?", + "description": "timo가 맞춤 추천을 해드려요.", + "options": { + "4": { + "label": "매우 정확해요.", + "sublabel": "예상 시간과 실제 시간이 일치해요." + }, + "3": { + "label": "대체로 맞는 편이에요.", + "sublabel": "가끔 틀리지만 대체로 맞는 편이에요." + }, + "2": { + "label": "자주 틀리는 편이에요.", + "sublabel": "예상과 항상 다르게 흘러가요." + }, + "1": { "label": "잘 모르겠어요.", "sublabel": "생각해본 적이 없어요." } + } + }, + "lifePattern": { + "title": "평소 생활 패턴을 알려주세요.", + "description": "입력한 정보는 시간 패턴 분석에만 사용돼요.", + "wakeUpTime": "기상 시간", + "bedTime": "취침 시간", + "bedTimeError": "취침 시간은 기상 시간보다 늦게 설정해 주세요." + }, + "calendarConnect": { + "title": "Google 캘린더를 연결하면\n일정을 함께 관리할 수 있어요.", + "description": "기존 일정을 timo에 불러와 한 곳에서 관리할 수 있어요.", + "connectLabel": "Google 캘린더 연동", + "consentNotice": "클릭 시 Google 인증 및 캘린더 접근 권한 동의 화면이 표시됩니다.", + "permissionNotice": "timo는 일정 조회 권한만 요청합니다." } } } diff --git a/apps/timo-web/package.json b/apps/timo-web/package.json index 40fa31eb..67203a10 100644 --- a/apps/timo-web/package.json +++ b/apps/timo-web/package.json @@ -20,6 +20,7 @@ "@sentry/nextjs": "^10.61.0", "@tanstack/react-query": "^5.101.1", "@tanstack/react-query-devtools": "^5.101.1", + "@use-funnel/browser": "^0.0.17", "axios": "^1.18.1", "focus-trap-react": "^12.0.3", "lottie-react": "^2.4.1", diff --git a/packages/timo-design-system/src/icons/source/alert.svg b/packages/timo-design-system/src/icons/source/alert.svg new file mode 100644 index 00000000..35fd0a99 --- /dev/null +++ b/packages/timo-design-system/src/icons/source/alert.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/timo-design-system/src/icons/source/moon.svg b/packages/timo-design-system/src/icons/source/moon.svg new file mode 100644 index 00000000..507d46f3 --- /dev/null +++ b/packages/timo-design-system/src/icons/source/moon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/packages/timo-design-system/src/icons/source/sun.svg b/packages/timo-design-system/src/icons/source/sun.svg new file mode 100644 index 00000000..08218ff5 --- /dev/null +++ b/packages/timo-design-system/src/icons/source/sun.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d66ff232..16020939 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -59,6 +59,9 @@ importers: '@tanstack/react-query-devtools': specifier: ^5.101.1 version: 5.101.1(@tanstack/react-query@5.101.1(react@19.2.0))(react@19.2.0) + '@use-funnel/browser': + specifier: ^0.0.17 + version: 0.0.17(react@19.2.0) axios: specifier: ^1.18.1 version: 1.18.1 @@ -2123,6 +2126,16 @@ packages: resolution: {integrity: sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@use-funnel/browser@0.0.17': + resolution: {integrity: sha512-saexLJSpTwm02If0I6oY6s1Z7cPwkEGqHvBg+IVokDywcrkhZA9MpxDHT27GSrNlhJHRxis/3g/xIQ48UYfp2Q==} + peerDependencies: + react: '>=16.8' + + '@use-funnel/core@0.1.0': + resolution: {integrity: sha512-KEjBKTbOh5wd6CQZ1jYWhI6KuGrI9Pjn9CkmUgAYzonsyZE0lsiZ2/1RwsaWcxVO5RJBgfwCM8dRpibwm7An9w==} + peerDependencies: + react: '>=16.8' + '@vitest/expect@2.0.5': resolution: {integrity: sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==} @@ -6308,6 +6321,15 @@ snapshots: '@typescript-eslint/types': 8.50.0 eslint-visitor-keys: 4.2.1 + '@use-funnel/browser@0.0.17(react@19.2.0)': + dependencies: + '@use-funnel/core': 0.1.0(react@19.2.0) + react: 19.2.0 + + '@use-funnel/core@0.1.0(react@19.2.0)': + dependencies: + react: 19.2.0 + '@vitest/expect@2.0.5': dependencies: '@vitest/spy': 2.0.5