diff --git a/app/(game)/play/PlayClient.tsx b/app/(game)/play/PlayClient.tsx new file mode 100644 index 0000000..30110b2 --- /dev/null +++ b/app/(game)/play/PlayClient.tsx @@ -0,0 +1,230 @@ +"use client"; + +import Link from "next/link"; +import { FormEvent, useEffect, useState } from "react"; + +import KatexRenderer from "@/components/math/KatexRenderer"; +import { useGameStore } from "@/store/gameStore"; + +function parseAnswer(value: string) { + if (value.trim() === "") return null; + + const parsed = Number.parseInt(value, 10); + return Number.isNaN(parsed) ? null : parsed; +} + +export default function PlayClient() { + const session = useGameStore((state) => state.session); + const initSession = useGameStore((state) => state.initSession); + const submitAnswer = useGameStore((state) => state.submitAnswer); + const nextProblem = useGameStore((state) => state.nextProblem); + const resetSession = useGameStore((state) => state.resetSession); + + const [answerA, setAnswerA] = useState(""); + const [answerB, setAnswerB] = useState(""); + const [inputError, setInputError] = useState(""); + + const currentProblem = session?.problems[session.currentIndex] ?? null; + const currentResult = session?.results[session.currentIndex] ?? null; + const isComplete = Boolean( + session && session.currentIndex >= session.problems.length, + ); + const correctCount = + session?.results.filter((result) => result?.correct).length ?? 0; + const totalCount = session?.problems.length ?? 0; + + useEffect(() => { + if (session === null) { + initSession(); + } + }, [initSession, session]); + + useEffect(() => { + setAnswerA(""); + setAnswerB(""); + setInputError(""); + }, [currentProblem?.key]); + + function handleSubmit(event: FormEvent) { + event.preventDefault(); + + if (currentResult || !currentProblem) return; + + const parsedA = parseAnswer(answerA); + const parsedB = parseAnswer(answerB); + + if (parsedA === null || parsedB === null) { + setInputError("두 칸 모두 정수로 입력하세요."); + return; + } + + setInputError(""); + submitAnswer({ a: parsedA, b: parsedB }); + } + + if (!session) { + return ( +
+
+

+ 문제를 준비하고 있습니다. +

+
+
+ ); + } + + if (isComplete) { + return ( +
+
+
+

+ 괄호 음수 분배 +

+

+ 풀이 결과 +

+

+ {correctCount} / {totalCount} 정답 +

+
+ +
+ + + 처음으로 + +
+
+
+ ); + } + + if (!currentProblem) { + return ( +
+
+

+ 현재 문제를 불러올 수 없습니다. +

+
+
+ ); + } + + const submitted = currentResult !== null; + const isLastProblem = session.currentIndex === session.problems.length - 1; + + return ( +
+
+
+
+

+ 괄호 음수 분배 +

+

+ 문제 풀이 +

+
+

+ {session.currentIndex + 1} / {session.problems.length} +

+
+ +
+

+ 괄호를 풀어 두 항을 순서대로 입력하세요. +

+
+ +
+
+ +
+
+ + +
+ + {inputError ? ( +

+ {inputError} +

+ ) : null} + +
+ + + 처음으로 + +
+
+ + {currentResult ? ( +
+

+ {currentResult.correct ? "정답입니다." : "오답입니다."} +

+
+ +
+ +
+ ) : null} +
+
+ ); +} diff --git a/app/(game)/play/page.tsx b/app/(game)/play/page.tsx index b8bc5a4..eed65b4 100644 --- a/app/(game)/play/page.tsx +++ b/app/(game)/play/page.tsx @@ -1,30 +1,5 @@ -import Link from "next/link"; +import PlayClient from "./PlayClient"; export default function PlayPage() { - return ( -
-
-
-

- 괄호 음수 분배 -

-

- 문제 풀이 -

-

- 다음 단계에서 괄호 앞 음수 분배 문제가 이 화면에 표시됩니다. -

-
- -
- - 처음으로 - -
-
-
- ); + return ; } diff --git a/app/layout.tsx b/app/layout.tsx index 6290fa9..b00dd7d 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,5 +1,6 @@ import type { Metadata } from "next"; import localFont from "next/font/local"; +import "katex/dist/katex.min.css"; import "./globals.css"; const geistSans = localFont({ diff --git a/components/math/KatexRenderer.tsx b/components/math/KatexRenderer.tsx new file mode 100644 index 0000000..43788b8 --- /dev/null +++ b/components/math/KatexRenderer.tsx @@ -0,0 +1,15 @@ +"use client"; + +import { BlockMath, InlineMath } from "react-katex"; + +interface KatexRendererProps { + latex: string; + displayMode?: boolean; +} + +export default function KatexRenderer({ + latex, + displayMode = false, +}: KatexRendererProps) { + return displayMode ? : ; +} diff --git a/docs/vibe/02-simple-play.md b/docs/vibe/02-simple-play.md new file mode 100644 index 0000000..a02712a --- /dev/null +++ b/docs/vibe/02-simple-play.md @@ -0,0 +1,358 @@ +# 02. 괄호 앞 음수 분배 — 간단한 풀이 화면 + +## 계획 평가 + +Claude Code가 작성한 기존 계획은 방향이 맞다. `01-project-start.md`에서 만든 `/` -> `/play` 흐름 위에 문제 생성 엔진, KaTeX 표시, 답 입력, 채점, 10문제 세션을 붙이는 순서가 자연스럽다. + +다만 실제 구현 단계로 쓰기에는 몇 가지를 조정하는 편이 좋다. + +- 엔진 단위 테스트만 하는 단계에서는 `@vitejs/plugin-react`, `jsdom`, `@testing-library/react`가 아직 필요하지 않다. +- `generateSession()`의 중복 방지는 랜덤 `id`가 아니라 `multiplier/a/operator/b` 조합 기준이어야 한다. +- Zustand는 한 문제 풀이가 동작한 뒤 도입한다. 이 순서를 지키면 버그가 엔진, UI, 상태 관리 중 어디에서 생겼는지 분리하기 쉽다. +- `PlayClient`에서 세션을 초기화할 때 렌더링 중 `initSession()`을 직접 호출하지 않고 `useEffect`에서 호출해야 한다. +- 이번 문서는 “간단한 풀이 화면”이 목표이므로 Framer Motion, 정교한 진행 바, 배너 디자인, E2E는 다음 단계로 넘긴다. + +--- + +## 목표 + +`/play` 화면에서 **괄호 앞 음수 분배** 문제를 10개 풀고 점수를 확인할 수 있게 만든다. + +이번 단계의 최종 상태는 다음과 같다. + +- 자체 알고리즘으로 괄호 앞 음수 분배 문제를 생성한다. +- 문제와 정답 수식을 KaTeX로 렌더링한다. +- 사용자가 두 항의 답을 입력하고 채점 결과를 확인한다. +- 10문제 풀이가 끝나면 점수 요약을 보여준다. + +--- + +## 이번 단계에서 할 것 + +- `lib/engine/`에 문제 생성·채점 순수 함수와 Vitest 단위 테스트 추가 +- `components/math/`에 KaTeX 렌더링 컴포넌트 추가 +- `/play`에 한 문제 풀이 UI 구현 +- 한 문제 풀이 확인 후 `store/gameStore.ts`로 10문제 세션 구현 +- 세션 완료 후 간단한 점수 요약 표시 + +## 이번 단계에서 하지 않을 것 + +- Framer Motion 애니메이션 +- 정교한 ProgressBar, FeedbackBanner, ScoreSummary 디자인 컴포넌트화 +- DB, Prisma, Redis, NextAuth +- 유형 선택 화면, 모드 선택 화면 +- 스피드 퀴즈 모드 +- 미지수 `x`, `y`, `z`가 포함된 문제 +- Playwright E2E 테스트 + +--- + +## 의존성 + +필요한 시점에 나누어 설치한다. + +### 엔진 테스트 + +```bash +pnpm add -D vitest +``` + +### 수식 렌더링과 세션 상태 + +```bash +pnpm add katex react-katex zustand +pnpm add -D @types/katex +``` + +React 컴포넌트 테스트를 추가하지 않는 한 `@testing-library/react`, `jsdom`, `@vitejs/plugin-react`는 이번 단계에 넣지 않는다. + +--- + +## 생성 또는 수정 파일 + +### 신규 생성 + +```text +lib/engine/types.ts +lib/engine/parenthesis-sign.ts +lib/engine/parenthesis-sign.test.ts +vitest.config.ts +components/math/KatexRenderer.tsx +store/gameStore.ts +app/(game)/play/PlayClient.tsx +``` + +### 수정 + +```text +package.json # test script 추가 +app/layout.tsx # KaTeX CSS import 추가 +app/(game)/play/page.tsx # PlayClient 마운트 +``` + +--- + +## 작업 순서 + +### 2-A. 문제 생성 엔진 + +완료 기준: `pnpm test` 통과 + +`lib/engine/types.ts` + +```ts +export interface ParenthesisSignProblem { + id: string + key: string + multiplier: number + a: number + operator: '+' | '-' + b: number + distributedA: number + distributedB: number + latexBefore: string + latexAfter: string +} + +export interface UserAnswer { + a: number + b: number +} + +export interface GradeResult { + correct: boolean + correctAnswer: [number, number] +} +``` + +`lib/engine/parenthesis-sign.ts`에 구현할 함수: + +- `generateProblem(): ParenthesisSignProblem` + - `multiplier`: `-9`부터 `-1` 사이 정수 + - `a`, `b`: `1`부터 `9` 사이 정수 + - `a !== b` + - `operator`: `'+' | '-'` + - `distributedA = multiplier * a` + - `distributedB = operator === '-' ? multiplier * -b : multiplier * b` + - `key`는 `multiplier:a:operator:b` 형식으로 만든다. + - `latexBefore`: 예시 `-2(3 - 6)` + - `latexAfter`: 예시 `-6 + 12` + +- `generateSession(count = 10): ParenthesisSignProblem[]` + - `count`가 `1` 이상인지 확인한다. + - 가능한 문제 수는 `9 * 8 * 2 = 144`개이므로 `count > 144`이면 에러를 던진다. + - 중복 방지는 `id`가 아니라 `key` 기준으로 한다. + +- `gradeAnswer(problem, answer): GradeResult` + - 두 항이 모두 일치할 때만 정답이다. + - 입력 순서를 바꾸어 쓴 답은 오답으로 처리한다. + +`lib/engine/parenthesis-sign.test.ts` 검증 항목: + +- `multiplier`는 항상 음수 정수다. +- `a`, `b`는 범위 안에 있고 서로 다르다. +- `distributedA`, `distributedB` 계산이 `operator`별로 맞다. +- `gradeAnswer()`는 정답과 오답을 정확히 구분한다. +- `generateSession(10)`은 10개 문제를 만들고 `key` 중복이 없다. +- `generateSession(145)`는 에러를 던진다. + +`vitest.config.ts` + +```ts +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + environment: 'node', + }, +}) +``` + +`package.json` + +```json +"test": "vitest run" +``` + +--- + +### 2-B. KaTeX 렌더링 + +완료 기준: 브라우저에서 문제 수식이 KaTeX로 표시됨 + +`app/layout.tsx`에 전역 CSS를 추가한다. + +```ts +import 'katex/dist/katex.min.css' +``` + +`components/math/KatexRenderer.tsx` + +```tsx +'use client' + +import { BlockMath, InlineMath } from 'react-katex' + +interface KatexRendererProps { + latex: string + displayMode?: boolean +} + +export default function KatexRenderer({ + latex, + displayMode = false, +}: KatexRendererProps) { + return displayMode ? : +} +``` + +이 단계에서는 `/play`에 임시 문제 하나를 표시해서 KaTeX 렌더링만 확인한다. + +--- + +### 2-C. 한 문제 풀이 UI + +완료 기준: 한 문제를 풀고 정답·오답 피드백이 표시됨 + +`app/(game)/play/PlayClient.tsx`를 클라이언트 컴포넌트로 만든다. + +```tsx +'use client' +``` + +구성: + +- 문제 수식: `KatexRenderer`로 `problem.latexBefore` 표시 +- 답 입력: 숫자 입력 두 칸 +- 제출 버튼 +- Enter 제출 +- 정답이면 간단한 성공 문구 +- 오답이면 정답 수식과 정답 두 항 표시 +- 새 문제 버튼 + +로컬 상태: + +```ts +const [problem, setProblem] = useState(() => generateProblem()) +const [answerA, setAnswerA] = useState('') +const [answerB, setAnswerB] = useState('') +const [result, setResult] = useState(null) +``` + +입력 처리: + +- 빈 문자열은 제출하지 않는다. +- `Number.parseInt(value, 10)` 결과가 `NaN`이면 제출하지 않는다. +- 채점 후에는 같은 문제에서 중복 제출하지 않도록 제출 버튼을 비활성화한다. + +이 단계에서는 Zustand를 사용하지 않는다. + +--- + +### 2-D. 10문제 세션 + +완료 기준: 10문제를 모두 풀면 점수 요약 화면이 표시됨 + +`store/gameStore.ts` + +```ts +interface GameSession { + problems: ParenthesisSignProblem[] + currentIndex: number + results: (GradeResult | null)[] +} + +interface GameState { + session: GameSession | null + initSession: (count?: number) => void + submitAnswer: (answer: UserAnswer) => void + nextProblem: () => void + resetSession: () => void + currentProblem: () => ParenthesisSignProblem | null + currentResult: () => GradeResult | null + isComplete: () => boolean + score: () => { correct: number; total: number } +} +``` + +상태 규칙: + +- `initSession(10)`은 10개 문제와 `null` 결과 배열을 만든다. +- `submitAnswer()`는 현재 문제의 결과가 `null`일 때만 채점한다. +- `nextProblem()`은 채점 후에만 다음 문제로 이동한다. +- 마지막 문제에서 `nextProblem()`을 호출하면 완료 상태가 된다. +- `resetSession()`은 기존 세션을 버리고 새 세션을 시작한다. + +`PlayClient.tsx` 전환: + +- `useEffect`에서 `session === null`이면 `initSession()` 호출 +- `currentProblem() === null`이면 로딩 또는 완료 화면 표시 +- 풀이 중에는 현재 문제 번호를 텍스트로 표시한다. 예: `3 / 10` +- 세션 완료 후 맞은 개수와 전체 개수를 표시한다. +- 다시 시작 버튼은 `resetSession()`을 호출한다. + +`app/(game)/play/page.tsx` + +```tsx +import PlayClient from './PlayClient' + +export default function PlayPage() { + return +} +``` + +--- + +## 화면 흐름 + +```text +/ 시작 화면 + - "연습 시작" 클릭 + + ↓ + +/play 풀이 화면 + ├─ 현재 문제 번호 + ├─ 문제 수식 (KaTeX) + ├─ 답 입력 두 칸 + ├─ 제출 버튼 + ├─ 정답/오답 피드백 + ├─ 다음 문제 버튼 + └─ 10문제 완료 후 점수 요약 + - n / 10 정답 + - 다시 시작 +``` + +--- + +## 완료 기준 + +- `pnpm test` 통과 +- `pnpm lint` 통과 +- 브라우저에서 `/play` 수식이 KaTeX로 렌더링된다. +- 정답 입력 시 정답 피드백이 표시된다. +- 오답 입력 시 오답 피드백과 정답이 표시된다. +- 10문제를 모두 풀면 점수 요약 화면이 표시된다. +- 다시 시작을 누르면 새 10문제 세션이 시작된다. + +## 확인 명령 + +```bash +pnpm test +pnpm lint +pnpm dev +``` + +브라우저 확인은 `pnpm dev` 실행 후 `http://localhost:3000/play`에서 한다. + +--- + +## 다음 단계 + +`03-ux-polish.md`에서 UI 품질을 높인다. + +- 진행률 바 +- 정답·오답 배너 컴포넌트 +- 점수 요약 컴포넌트 +- Framer Motion 기반 전환 애니메이션 +- 키보드 탐색과 포커스 처리 개선 diff --git a/lib/engine/parenthesis-sign.test.ts b/lib/engine/parenthesis-sign.test.ts new file mode 100644 index 0000000..ebd3952 --- /dev/null +++ b/lib/engine/parenthesis-sign.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, it } from "vitest"; + +import { + generateProblem, + generateSession, + gradeAnswer, +} from "./parenthesis-sign"; + +describe("parenthesis sign problem engine", () => { + it("generates a problem within the configured ranges", () => { + for (let index = 0; index < 100; index += 1) { + const problem = generateProblem(); + + expect(Number.isInteger(problem.multiplier)).toBe(true); + expect(problem.multiplier).toBeGreaterThanOrEqual(-9); + expect(problem.multiplier).toBeLessThanOrEqual(-1); + expect(problem.a).toBeGreaterThanOrEqual(1); + expect(problem.a).toBeLessThanOrEqual(9); + expect(problem.b).toBeGreaterThanOrEqual(1); + expect(problem.b).toBeLessThanOrEqual(9); + expect(problem.a).not.toBe(problem.b); + expect(["+", "-"]).toContain(problem.operator); + } + }); + + it("distributes the multiplier correctly for both operators", () => { + for (let index = 0; index < 100; index += 1) { + const problem = generateProblem(); + const signedB = problem.operator === "-" ? -problem.b : problem.b; + + expect(problem.distributedA).toBe(problem.multiplier * problem.a); + expect(problem.distributedB).toBe(problem.multiplier * signedB); + } + }); + + it("grades correct and incorrect answers", () => { + const problem = generateProblem(); + + expect( + gradeAnswer(problem, { + a: problem.distributedA, + b: problem.distributedB, + }).correct, + ).toBe(true); + + expect( + gradeAnswer(problem, { + a: problem.distributedB, + b: problem.distributedA, + }).correct, + ).toBe(false); + }); + + it("generates a session with unique problem keys", () => { + const problems = generateSession(10); + const keys = new Set(problems.map((problem) => problem.key)); + + expect(problems).toHaveLength(10); + expect(keys.size).toBe(10); + }); + + it("rejects impossible session sizes", () => { + expect(() => generateSession(0)).toThrow(); + expect(() => generateSession(145)).toThrow(); + }); +}); diff --git a/lib/engine/parenthesis-sign.ts b/lib/engine/parenthesis-sign.ts new file mode 100644 index 0000000..1db8d10 --- /dev/null +++ b/lib/engine/parenthesis-sign.ts @@ -0,0 +1,118 @@ +import type { + GradeResult, + ParenthesisSignProblem, + UserAnswer, +} from "./types"; + +const MIN_MULTIPLIER = -9; +const MAX_MULTIPLIER = -1; +const MIN_TERM = 1; +const MAX_TERM = 9; +const MAX_PROBLEM_COUNT = 9 * 8 * 2; + +type ProblemSeed = Pick< + ParenthesisSignProblem, + "multiplier" | "a" | "operator" | "b" +>; + +function randomInt(min: number, max: number) { + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +function formatLatexTermPair(first: number, second: number) { + const operator = second < 0 ? "-" : "+"; + + return `${first} ${operator} ${Math.abs(second)}`; +} + +function createProblem({ multiplier, a, operator, b }: ProblemSeed) { + const distributedA = multiplier * a; + const distributedB = operator === "-" ? multiplier * -b : multiplier * b; + const key = `${multiplier}:${a}:${operator}:${b}`; + + return { + id: `parenthesis-sign:${key}`, + key, + multiplier, + a, + operator, + b, + distributedA, + distributedB, + latexBefore: `${multiplier}\\left(${a} ${operator} ${b}\\right)`, + latexAfter: formatLatexTermPair(distributedA, distributedB), + }; +} + +function createAllProblemSeeds() { + const seeds: ProblemSeed[] = []; + + for ( + let multiplier = MIN_MULTIPLIER; + multiplier <= MAX_MULTIPLIER; + multiplier += 1 + ) { + for (let a = MIN_TERM; a <= MAX_TERM; a += 1) { + for (let b = MIN_TERM; b <= MAX_TERM; b += 1) { + if (a === b) continue; + + seeds.push({ multiplier, a, operator: "+", b }); + seeds.push({ multiplier, a, operator: "-", b }); + } + } + } + + return seeds; +} + +function shuffle(items: T[]) { + const shuffled = [...items]; + + for (let index = shuffled.length - 1; index > 0; index -= 1) { + const targetIndex = randomInt(0, index); + const current = shuffled[index]; + shuffled[index] = shuffled[targetIndex]; + shuffled[targetIndex] = current; + } + + return shuffled; +} + +export function generateProblem(): ParenthesisSignProblem { + const a = randomInt(MIN_TERM, MAX_TERM); + let b = randomInt(MIN_TERM, MAX_TERM); + + while (a === b) { + b = randomInt(MIN_TERM, MAX_TERM); + } + + return createProblem({ + multiplier: randomInt(MIN_MULTIPLIER, MAX_MULTIPLIER), + a, + operator: Math.random() < 0.5 ? "+" : "-", + b, + }); +} + +export function generateSession(count = 10): ParenthesisSignProblem[] { + if (!Number.isInteger(count) || count < 1) { + throw new Error("count must be a positive integer."); + } + + if (count > MAX_PROBLEM_COUNT) { + throw new Error(`count cannot exceed ${MAX_PROBLEM_COUNT}.`); + } + + return shuffle(createAllProblemSeeds()).slice(0, count).map(createProblem); +} + +export function gradeAnswer( + problem: ParenthesisSignProblem, + answer: UserAnswer, +): GradeResult { + return { + correct: + answer.a === problem.distributedA && answer.b === problem.distributedB, + correctAnswer: [problem.distributedA, problem.distributedB], + }; +} diff --git a/lib/engine/types.ts b/lib/engine/types.ts new file mode 100644 index 0000000..eeadc29 --- /dev/null +++ b/lib/engine/types.ts @@ -0,0 +1,22 @@ +export interface ParenthesisSignProblem { + id: string; + key: string; + multiplier: number; + a: number; + operator: "+" | "-"; + b: number; + distributedA: number; + distributedB: number; + latexBefore: string; + latexAfter: string; +} + +export interface UserAnswer { + a: number; + b: number; +} + +export interface GradeResult { + correct: boolean; + correctAnswer: [number, number]; +} diff --git a/package.json b/package.json index b2df4c3..4bf432e 100644 --- a/package.json +++ b/package.json @@ -6,21 +6,27 @@ "dev": "next dev", "build": "next build", "start": "next start", + "test": "vitest run", "lint": "next lint" }, "dependencies": { + "katex": "^0.17.0", + "next": "14.2.35", "react": "^18", "react-dom": "^18", - "next": "14.2.35" + "react-katex": "^3.1.0", + "zustand": "^5.0.13" }, "devDependencies": { - "typescript": "^5", + "@types/katex": "^0.16.8", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", + "eslint": "^8", + "eslint-config-next": "14.2.35", "postcss": "^8", "tailwindcss": "^3.4.1", - "eslint": "^8", - "eslint-config-next": "14.2.35" + "typescript": "^5", + "vitest": "^4.1.7" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 427e8c0..aabfea9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + katex: + specifier: ^0.17.0 + version: 0.17.0 next: specifier: 14.2.35 version: 14.2.35(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -17,7 +20,16 @@ importers: react-dom: specifier: ^18 version: 18.3.1(react@18.3.1) + react-katex: + specifier: ^3.1.0 + version: 3.1.0(prop-types@15.8.1)(react@18.3.1) + zustand: + specifier: ^5.0.13 + version: 5.0.13(@types/react@18.3.29)(react@18.3.1) devDependencies: + '@types/katex': + specifier: ^0.16.8 + version: 0.16.8 '@types/node': specifier: ^20 version: 20.19.41 @@ -42,6 +54,9 @@ importers: typescript: specifier: ^5 version: 5.9.3 + vitest: + specifier: ^4.1.7 + version: 4.1.7(@types/node@20.19.41)(vite@8.0.14(@types/node@20.19.41)(jiti@1.21.7)) packages: @@ -192,16 +207,120 @@ packages: resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} engines: {node: '>=12.4.0'} + '@oxc-project/types@0.132.0': + resolution: {integrity: sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@rolldown/binding-android-arm64@1.0.2': + resolution: {integrity: sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.2': + resolution: {integrity: sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.2': + resolution: {integrity: sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.2': + resolution: {integrity: sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.2': + resolution: {integrity: sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.2': + resolution: {integrity: sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.0.2': + resolution: {integrity: sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.0.2': + resolution: {integrity: sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.0.2': + resolution: {integrity: sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.0.2': + resolution: {integrity: sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.0.2': + resolution: {integrity: sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.0.2': + resolution: {integrity: sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.2': + resolution: {integrity: sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.2': + resolution: {integrity: sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.2': + resolution: {integrity: sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} '@rushstack/eslint-patch@1.16.1': resolution: {integrity: sha512-TvZbIpeKqGQQ7X0zSCvPH9riMSFQFSggnfBjFZ1mEoILW+UuXCKwOoPcgjMwiUtRqFZ8jWhPJc4um14vC6I4ag==} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} @@ -211,9 +330,21 @@ packages: '@tybys/wasm-util@0.10.2': resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/katex@0.16.8': + resolution: {integrity: sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==} + '@types/node@20.19.41': resolution: {integrity: sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==} @@ -410,6 +541,35 @@ packages: cpu: [x64] os: [win32] + '@vitest/expect@4.1.7': + resolution: {integrity: sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==} + + '@vitest/mocker@4.1.7': + resolution: {integrity: sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.7': + resolution: {integrity: sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw==} + + '@vitest/runner@4.1.7': + resolution: {integrity: sha512-BapjmAQ2aI78WdMEfeUWivnfVzB+VPGwWRQcJE0OUq7qEeEcBsCSf+0T5iREBNE5nBb4wA5Ya0W6IA+sghdEFw==} + + '@vitest/snapshot@4.1.7': + resolution: {integrity: sha512-ZacLzja+TmJeZ1h14xW2FB/WpeimUD3haBXQPyJqxvo8jQTmfeA8zv58mtjN2C7EHXZDYVcVYdYmAxjkWVvKCw==} + + '@vitest/spy@4.1.7': + resolution: {integrity: sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q==} + + '@vitest/utils@4.1.7': + resolution: {integrity: sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -488,6 +648,10 @@ packages: resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} engines: {node: '>= 0.4'} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + ast-types-flow@0.0.8: resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} @@ -559,6 +723,10 @@ packages: caniuse-lite@1.0.30001793: resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -581,9 +749,16 @@ packages: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} + commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -639,6 +814,10 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} @@ -682,6 +861,9 @@ packages: resolution: {integrity: sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw==} engines: {node: '>= 0.4'} + es-module-lexer@2.1.0: + resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -810,10 +992,17 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1150,6 +1339,14 @@ packages: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} + katex@0.16.47: + resolution: {integrity: sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==} + hasBin: true + + katex@0.17.0: + resolution: {integrity: sha512-Vdw0ATsQ9V+LuegM/BTwQqV/6cTl5lbGcIrU+BCgLxyf6bo38ybOr372tuSIxir3CN720flu1meYR6XzNMwQnw==} + hasBin: true + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -1164,6 +1361,80 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -1185,6 +1456,9 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -1296,6 +1570,9 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -1338,6 +1615,9 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1434,6 +1714,12 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-katex@3.1.0: + resolution: {integrity: sha512-At9uLOkC75gwn2N+ZXc5HD8TlATsB+3Hkp9OGs6uA8tM3dwZ3Wljn74Bk3JyHFPgSnesY/EMrIAB1WJwqZqejA==} + peerDependencies: + prop-types: ^15.8.1 + react: '>=15.3.2 <20' + react@18.3.1: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} @@ -1479,6 +1765,11 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true + rolldown@1.0.2: + resolution: {integrity: sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -1542,6 +1833,9 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} @@ -1553,6 +1847,12 @@ packages: stable-hash@0.0.5: resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} @@ -1649,10 +1949,21 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.1.2: + resolution: {integrity: sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==} + engines: {node: '>=18'} + tinyglobby@0.2.16: resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} engines: {node: '>=12.0.0'} + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -1717,6 +2028,90 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + vite@8.0.14: + resolution: {integrity: sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.18 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.7: + resolution: {integrity: sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.7 + '@vitest/browser-preview': 4.1.7 + '@vitest/browser-webdriverio': 4.1.7 + '@vitest/coverage-istanbul': 4.1.7 + '@vitest/coverage-v8': 4.1.7 + '@vitest/ui': 4.1.7 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -1738,6 +2133,11 @@ packages: engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -1757,6 +2157,24 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zustand@5.0.13: + resolution: {integrity: sha512-efI2tVaVQPqtOh114loML/Z80Y4NP3yc+Ff0fYiZJPauNeWZeIp/bRFD7I9bfmCOYBh/PHxlglQ9+wvlwnPikQ==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + snapshots: '@alloc/quick-lru@5.2.0': {} @@ -1889,13 +2307,68 @@ snapshots: '@nolyfill/is-core-module@1.0.39': {} + '@oxc-project/types@0.132.0': {} + '@pkgjs/parseargs@0.11.0': optional: true + '@rolldown/binding-android-arm64@1.0.2': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.2': + optional: true + + '@rolldown/binding-darwin-x64@1.0.2': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.2': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.2': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.2': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.2': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.2': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.2': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.2': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.2': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.2': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.2': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.2': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.2': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + '@rtsao/scc@1.1.0': {} '@rushstack/eslint-patch@1.16.1': {} + '@standard-schema/spec@1.1.0': {} + '@swc/counter@0.1.3': {} '@swc/helpers@0.5.5': @@ -1908,8 +2381,19 @@ snapshots: tslib: 2.8.1 optional: true + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + '@types/json5@0.0.29': {} + '@types/katex@0.16.8': {} + '@types/node@20.19.41': dependencies: undici-types: 6.21.0 @@ -2088,6 +2572,47 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.12.2': optional: true + '@vitest/expect@4.1.7': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.7 + '@vitest/utils': 4.1.7 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.7(vite@8.0.14(@types/node@20.19.41)(jiti@1.21.7))': + dependencies: + '@vitest/spy': 4.1.7 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.0.14(@types/node@20.19.41)(jiti@1.21.7) + + '@vitest/pretty-format@4.1.7': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.7': + dependencies: + '@vitest/utils': 4.1.7 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.7': + dependencies: + '@vitest/pretty-format': 4.1.7 + '@vitest/utils': 4.1.7 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.7': {} + + '@vitest/utils@4.1.7': + dependencies: + '@vitest/pretty-format': 4.1.7 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + acorn-jsx@5.3.2(acorn@8.16.0): dependencies: acorn: 8.16.0 @@ -2191,6 +2716,8 @@ snapshots: get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 + assertion-error@2.0.1: {} + ast-types-flow@0.0.8: {} async-function@1.0.0: {} @@ -2253,6 +2780,8 @@ snapshots: caniuse-lite@1.0.30001793: {} + chai@6.2.2: {} + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -2280,8 +2809,12 @@ snapshots: commander@4.1.1: {} + commander@8.3.0: {} + concat-map@0.0.1: {} + convert-source-map@2.0.0: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -2334,6 +2867,8 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 + detect-libc@2.1.2: {} + didyoumean@1.2.2: {} dlv@1.1.3: {} @@ -2438,6 +2973,8 @@ snapshots: iterator.prototype: 1.1.5 math-intrinsics: 1.1.0 + es-module-lexer@2.1.0: {} + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -2657,8 +3194,14 @@ snapshots: estraverse@5.3.0: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + esutils@2.0.3: {} + expect-type@1.3.0: {} + fast-deep-equal@3.1.3: {} fast-glob@3.3.3: @@ -3009,6 +3552,14 @@ snapshots: object.assign: 4.1.7 object.values: 1.2.1 + katex@0.16.47: + dependencies: + commander: 8.3.0 + + katex@0.17.0: + dependencies: + commander: 8.3.0 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -3024,6 +3575,55 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} @@ -3040,6 +3640,10 @@ snapshots: lru-cache@10.4.3: {} + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + math-intrinsics@1.1.0: {} merge2@1.4.1: {} @@ -3157,6 +3761,8 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + obug@2.1.1: {} + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -3201,6 +3807,8 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.3 + pathe@2.0.3: {} + picocolors@1.1.1: {} picomatch@2.3.2: {} @@ -3276,6 +3884,12 @@ snapshots: react-is@16.13.1: {} + react-katex@3.1.0(prop-types@15.8.1)(react@18.3.1): + dependencies: + katex: 0.16.47 + prop-types: 15.8.1 + react: 18.3.1 + react@18.3.1: dependencies: loose-envify: 1.4.0 @@ -3334,6 +3948,27 @@ snapshots: dependencies: glob: 7.2.3 + rolldown@1.0.2: + dependencies: + '@oxc-project/types': 0.132.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.2 + '@rolldown/binding-darwin-arm64': 1.0.2 + '@rolldown/binding-darwin-x64': 1.0.2 + '@rolldown/binding-freebsd-x64': 1.0.2 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.2 + '@rolldown/binding-linux-arm64-gnu': 1.0.2 + '@rolldown/binding-linux-arm64-musl': 1.0.2 + '@rolldown/binding-linux-ppc64-gnu': 1.0.2 + '@rolldown/binding-linux-s390x-gnu': 1.0.2 + '@rolldown/binding-linux-x64-gnu': 1.0.2 + '@rolldown/binding-linux-x64-musl': 1.0.2 + '@rolldown/binding-openharmony-arm64': 1.0.2 + '@rolldown/binding-wasm32-wasi': 1.0.2 + '@rolldown/binding-win32-arm64-msvc': 1.0.2 + '@rolldown/binding-win32-x64-msvc': 1.0.2 + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -3421,12 +4056,18 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} + signal-exit@4.1.0: {} source-map-js@1.2.1: {} stable-hash@0.0.5: {} + stackback@0.0.2: {} + + std-env@4.1.0: {} + stop-iteration-iterator@1.1.0: dependencies: es-errors: 1.3.0 @@ -3567,11 +4208,17 @@ snapshots: dependencies: any-promise: 1.3.0 + tinybench@2.9.0: {} + + tinyexec@1.1.2: {} + tinyglobby@0.2.16: dependencies: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 + tinyrainbow@3.1.0: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -3674,6 +4321,45 @@ snapshots: util-deprecate@1.0.2: {} + vite@8.0.14(@types/node@20.19.41)(jiti@1.21.7): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.15 + rolldown: 1.0.2 + tinyglobby: 0.2.16 + optionalDependencies: + '@types/node': 20.19.41 + fsevents: 2.3.3 + jiti: 1.21.7 + + vitest@4.1.7(@types/node@20.19.41)(vite@8.0.14(@types/node@20.19.41)(jiti@1.21.7)): + dependencies: + '@vitest/expect': 4.1.7 + '@vitest/mocker': 4.1.7(vite@8.0.14(@types/node@20.19.41)(jiti@1.21.7)) + '@vitest/pretty-format': 4.1.7 + '@vitest/runner': 4.1.7 + '@vitest/snapshot': 4.1.7 + '@vitest/spy': 4.1.7 + '@vitest/utils': 4.1.7 + es-module-lexer: 2.1.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.1.2 + tinyglobby: 0.2.16 + tinyrainbow: 3.1.0 + vite: 8.0.14(@types/node@20.19.41)(jiti@1.21.7) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 20.19.41 + transitivePeerDependencies: + - msw + which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 @@ -3719,6 +4405,11 @@ snapshots: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + word-wrap@1.2.5: {} wrap-ansi@7.0.0: @@ -3736,3 +4427,8 @@ snapshots: wrappy@1.0.2: {} yocto-queue@0.1.0: {} + + zustand@5.0.13(@types/react@18.3.29)(react@18.3.1): + optionalDependencies: + '@types/react': 18.3.29 + react: 18.3.1 diff --git a/store/gameStore.ts b/store/gameStore.ts new file mode 100644 index 0000000..ee866a6 --- /dev/null +++ b/store/gameStore.ts @@ -0,0 +1,109 @@ +import { create } from "zustand"; + +import { + generateSession, + gradeAnswer, +} from "@/lib/engine/parenthesis-sign"; +import type { + GradeResult, + ParenthesisSignProblem, + UserAnswer, +} from "@/lib/engine/types"; + +interface GameSession { + problems: ParenthesisSignProblem[]; + currentIndex: number; + results: (GradeResult | null)[]; +} + +interface GameState { + session: GameSession | null; + initSession: (count?: number) => void; + submitAnswer: (answer: UserAnswer) => void; + nextProblem: () => void; + resetSession: () => void; + currentProblem: () => ParenthesisSignProblem | null; + currentResult: () => GradeResult | null; + isComplete: () => boolean; + score: () => { correct: number; total: number }; +} + +const DEFAULT_SESSION_COUNT = 10; + +function createSession(count = DEFAULT_SESSION_COUNT): GameSession { + const problems = generateSession(count); + + return { + problems, + currentIndex: 0, + results: problems.map(() => null), + }; +} + +export const useGameStore = create((set, get) => ({ + session: null, + initSession: (count = DEFAULT_SESSION_COUNT) => { + set({ session: createSession(count) }); + }, + submitAnswer: (answer) => { + const { session } = get(); + + if (!session || session.currentIndex >= session.problems.length) return; + if (session.results[session.currentIndex] !== null) return; + + const problem = session.problems[session.currentIndex]; + const result = gradeAnswer(problem, answer); + const results = [...session.results]; + results[session.currentIndex] = result; + + set({ + session: { + ...session, + results, + }, + }); + }, + nextProblem: () => { + const { session } = get(); + + if (!session || session.currentIndex >= session.problems.length) return; + if (session.results[session.currentIndex] === null) return; + + set({ + session: { + ...session, + currentIndex: session.currentIndex + 1, + }, + }); + }, + resetSession: () => { + set({ session: createSession() }); + }, + currentProblem: () => { + const { session } = get(); + + if (!session) return null; + return session.problems[session.currentIndex] ?? null; + }, + currentResult: () => { + const { session } = get(); + + if (!session) return null; + return session.results[session.currentIndex] ?? null; + }, + isComplete: () => { + const { session } = get(); + + return Boolean(session && session.currentIndex >= session.problems.length); + }, + score: () => { + const { session } = get(); + + if (!session) return { correct: 0, total: 0 }; + + return { + correct: session.results.filter((result) => result?.correct).length, + total: session.problems.length, + }; + }, +})); diff --git a/types/react-katex.d.ts b/types/react-katex.d.ts new file mode 100644 index 0000000..d01bbec --- /dev/null +++ b/types/react-katex.d.ts @@ -0,0 +1,10 @@ +declare module "react-katex" { + import type { ComponentType } from "react"; + + interface MathComponentProps { + math: string; + } + + export const InlineMath: ComponentType; + export const BlockMath: ComponentType; +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..f624398 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + }, +});