Pushkar02-n commited on
Commit
a4d9fc2
·
1 Parent(s): 617515c

Make NextJS UI changes along with Upstash redis counter for usage limit

Browse files
.gitignore CHANGED
@@ -4,6 +4,42 @@ __pycache__
4
  data
5
  .gradio
6
  supabase_dump.sql
7
- src/ui/node_modules/
8
- src/ui/.next/
9
- src/ui/.env.local
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  data
5
  .gradio
6
  supabase_dump.sql
7
+ ui/node_modules/
8
+ ui/.next/
9
+ ui/.env.local
10
+ /node_modules
11
+ /.pnp
12
+ .pnp.js
13
+ .yarn/install-state.gz
14
+
15
+ # testing
16
+ /coverage
17
+
18
+ # next.js
19
+ /.next/
20
+ /out/
21
+
22
+ # production
23
+ /build
24
+
25
+ # misc
26
+ .DS_Store
27
+ *.pem
28
+
29
+ # debug
30
+ npm-debug.log*
31
+ yarn-debug.log*
32
+ yarn-error.log*
33
+
34
+ # env files — NEVER commit these
35
+ .env.local
36
+ .env.development.local
37
+ .env.test.local
38
+ .env.production.local
39
+
40
+ # vercel
41
+ .vercel
42
+
43
+ # typescript
44
+ *.tsbuildinfo
45
+ next-env.d.ts
ui/.env.example ADDED
File without changes
ui/Readme.md ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AniFind — Next.js Frontend
2
+
3
+ AI-powered anime recommendation frontend. Communicates with the FastAPI backend on Hugging Face Spaces.
4
+
5
+ ---
6
+
7
+ ## Project Structure
8
+
9
+ ```
10
+ anifind/
11
+ ├── app/
12
+ │ ├── api/
13
+ │ │ └── recommend/
14
+ │ │ └── route.ts ← Server-side proxy (API key never reaches browser)
15
+ │ ├── globals.css ← Global styles + fonts
16
+ │ ├── layout.tsx ← Root HTML layout + metadata
17
+ │ └── page.tsx ← Main UI (search, filters, results)
18
+ ├── lib/
19
+ │ └── types.ts ← TypeScript types shared across app
20
+ ├── .env.example ← Copy to .env.local with real values
21
+ ├── .gitignore ← Includes .env.local — never commit secrets!
22
+ ├── next.config.mjs
23
+ ├── tailwind.config.ts
24
+ ├── tsconfig.json
25
+ └── package.json
26
+ ```
27
+
28
+ ---
29
+
30
+ ## Local Development
31
+
32
+ ### 1. Install dependencies
33
+ ```bash
34
+ npm install
35
+ ```
36
+
37
+ ### 2. Set up environment variables
38
+ ```bash
39
+ cp .env.example .env.local
40
+ # Edit .env.local with your actual values:
41
+ # API_URL=https://your-username-your-space.hf.space
42
+ # API_KEY=your_secret_key
43
+ ```
44
+
45
+ ### 3. Run dev server
46
+ ```bash
47
+ npm run dev
48
+ # Open http://localhost:3000
49
+ ```
50
+
51
+ ---
52
+
53
+ ## Deploy to Vercel
54
+
55
+ ### Step 1 — Push to GitHub
56
+ ```bash
57
+ git init
58
+ git add .
59
+ git commit -m "initial commit"
60
+ git remote add origin https://github.com/your-username/anifind.git
61
+ git push -u origin main
62
+ ```
63
+
64
+ ### Step 2 — Connect to Vercel
65
+ 1. Go to https://vercel.com → "Add New Project"
66
+ 2. Import your GitHub repo
67
+ 3. Vercel auto-detects Next.js — no build config needed
68
+
69
+ ### Step 3 — Add Environment Variables in Vercel
70
+ In your Vercel project → Settings → Environment Variables, add:
71
+
72
+ | Name | Value |
73
+ |-----------|------------------------------------------------|
74
+ | `API_URL` | `https://your-username-your-space.hf.space` |
75
+ | `API_KEY` | `your_secret_key` |
76
+
77
+ ⚠️ Do NOT prefix with NEXT_PUBLIC_ — these must stay server-side only.
78
+
79
+ ### Step 4 — Deploy
80
+ Click Deploy. Every future `git push` to `main` auto-deploys.
81
+
82
+ ---
83
+
84
+ ## How the API key is protected
85
+
86
+ ```
87
+ Browser → /api/recommend (Next.js server route) → HF Spaces FastAPI
88
+
89
+ API_KEY lives here (Vercel env var, server-side only)
90
+ Browser never sees it
91
+ ```
92
+
93
+ The browser only ever calls `/api/recommend` on your own domain.
94
+ The actual HF Spaces URL and API key are injected server-side by Vercel.
95
+
96
+ ---
97
+
98
+ ## Environment Variables Reference
99
+
100
+ | Variable | Where set | Description |
101
+ |-----------|------------------|--------------------------------|
102
+ | `API_URL` | Vercel + .env.local | HF Spaces FastAPI base URL |
103
+ | `API_KEY` | Vercel + .env.local | Shared secret with FastAPI |
ui/app/api/recommend/route.ts ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+
3
+ // ─── Config ───────────────────────────────────────────────────────────────────
4
+ const DAILY_LIMIT = 10;
5
+ const COOLDOWN_HRS = 2;
6
+ const COOLDOWN_MS = COOLDOWN_HRS * 60 * 60 * 1000;
7
+
8
+ const REDIS_COUNT_KEY = "anifind:global_count";
9
+ const REDIS_RESET_KEY = "anifind:reset_at";
10
+
11
+ // ─── Tiny Upstash REST helper (no SDK needed) ─────────────────────────────────
12
+ async function redis(command: string[]): Promise<unknown> {
13
+ const url = process.env.UPSTASH_REDIS_REST_URL;
14
+ const token = process.env.UPSTASH_REDIS_REST_TOKEN;
15
+
16
+ if (!url || !token) throw new Error("Missing Upstash env vars");
17
+
18
+ const res = await fetch(`${url}/${command.map(encodeURIComponent).join("/")}`, {
19
+ headers: { Authorization: `Bearer ${token}` },
20
+ });
21
+
22
+ const json = await res.json() as { result: unknown };
23
+ return json.result;
24
+ }
25
+
26
+ // ─── GET — frontend polls this to show live counter ───────────────────────────
27
+ export async function GET() {
28
+ try {
29
+ const [count, resetAt] = await Promise.all([
30
+ redis(["GET", REDIS_COUNT_KEY]),
31
+ redis(["GET", REDIS_RESET_KEY]),
32
+ ]);
33
+
34
+ const currentCount = parseInt(String(count ?? "0"));
35
+ const resetTime = resetAt ? parseInt(String(resetAt)) : null;
36
+ const now = Date.now();
37
+ const isLocked = currentCount >= DAILY_LIMIT && resetTime !== null && now < resetTime;
38
+ const remainingMs = isLocked && resetTime ? resetTime - now : 0;
39
+
40
+ return NextResponse.json({
41
+ count: isLocked ? DAILY_LIMIT : currentCount,
42
+ limit: DAILY_LIMIT,
43
+ isLocked,
44
+ remainingMs,
45
+ remainingHrs: Math.ceil(remainingMs / (1000 * 60 * 60)),
46
+ });
47
+ } catch {
48
+ return NextResponse.json({ count: 0, limit: DAILY_LIMIT, isLocked: false, remainingMs: 0, remainingHrs: 0 });
49
+ }
50
+ }
51
+
52
+ // ─── POST — actual search proxy ───────────────────────────────────────────────
53
+ export async function POST(req: NextRequest) {
54
+ try {
55
+ const apiUrl = process.env.API_URL;
56
+ const apiKey = process.env.API_KEY;
57
+
58
+ if (!apiUrl || !apiKey) {
59
+ return NextResponse.json(
60
+ { error: "Server misconfiguration: missing API_URL or API_KEY" },
61
+ { status: 500 }
62
+ );
63
+ }
64
+
65
+ // ── Rate limit check ────────────────────────────────────────────────────
66
+ try {
67
+ const [count, resetAt] = await Promise.all([
68
+ redis(["GET", REDIS_COUNT_KEY]),
69
+ redis(["GET", REDIS_RESET_KEY]),
70
+ ]);
71
+
72
+ const currentCount = parseInt(String(count ?? "0"));
73
+ const resetTime = resetAt ? parseInt(String(resetAt)) : null;
74
+ const now = Date.now();
75
+
76
+ if (currentCount >= DAILY_LIMIT) {
77
+ if (resetTime && now < resetTime) {
78
+ const remainingMs = resetTime - now;
79
+ const remainingHrs = Math.ceil(remainingMs / (1000 * 60 * 60));
80
+ return NextResponse.json(
81
+ {
82
+ error: `Global search limit reached (${DAILY_LIMIT} searches). Resets in ${remainingHrs} hr${remainingHrs !== 1 ? "s" : ""}.`,
83
+ isLocked: true,
84
+ remainingHrs,
85
+ remainingMs,
86
+ },
87
+ { status: 429 }
88
+ );
89
+ } else {
90
+ await Promise.all([
91
+ redis(["SET", REDIS_COUNT_KEY, "0"]),
92
+ redis(["DEL", REDIS_RESET_KEY]),
93
+ ]);
94
+ }
95
+ }
96
+ } catch (redisErr) {
97
+ console.warn("Redis rate limit check failed, allowing request:", redisErr);
98
+ }
99
+
100
+ // ── Forward to FastAPI ──────────────────────────────────────────────────
101
+ const body = await req.json();
102
+
103
+ const response = await fetch(`${apiUrl}/recommend`, {
104
+ method: "POST",
105
+ headers: {
106
+ "Content-Type": "application/json",
107
+ "X-API-Key": apiKey,
108
+ },
109
+ body: JSON.stringify(body),
110
+ });
111
+
112
+ if (!response.ok) {
113
+ const err = await response.text();
114
+ return NextResponse.json(
115
+ { error: `Backend error: ${err}` },
116
+ { status: response.status }
117
+ );
118
+ }
119
+
120
+ const data = await response.json();
121
+
122
+ // ── Increment counter AFTER successful response ─────────────────────────
123
+ try {
124
+ const newCount = await redis(["INCR", REDIS_COUNT_KEY]) as number;
125
+ if (newCount >= DAILY_LIMIT) {
126
+ const resetAt = Date.now() + COOLDOWN_MS;
127
+ await redis(["SET", REDIS_RESET_KEY, String(resetAt)]);
128
+ }
129
+ } catch (redisErr) {
130
+ console.warn("Redis increment failed:", redisErr);
131
+ }
132
+
133
+ return NextResponse.json(data);
134
+
135
+ } catch (err) {
136
+ console.error("Proxy error:", err);
137
+ return NextResponse.json(
138
+ { error: "Failed to reach backend" },
139
+ { status: 502 }
140
+ );
141
+ }
142
+ }
ui/app/globals.css ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=DM+Sans:wght@300;400;500;600&display=swap');
2
+
3
+ @tailwind base;
4
+ @tailwind components;
5
+ @tailwind utilities;
6
+
7
+ :root {
8
+ --bg: #070c12;
9
+ --bg2: #0f1923;
10
+ --bg3: #19263a;
11
+ --bdr: rgba(255,255,255,0.07);
12
+ --acc: #00c896;
13
+ --acc2: #00ffc8;
14
+ --adim: rgba(0,200,150,0.10);
15
+ --aglow: rgba(0,200,150,0.28);
16
+ --amb: #fbbf24;
17
+ --red: #f87171;
18
+ --txt: #cdd8ee;
19
+ --mut: #4e6580;
20
+ }
21
+
22
+ * { box-sizing: border-box; margin: 0; padding: 0; }
23
+
24
+ html, body {
25
+ background: var(--bg);
26
+ color: var(--txt);
27
+ font-family: 'DM Sans', sans-serif;
28
+ min-height: 100vh;
29
+ overflow-x: hidden;
30
+ }
31
+
32
+ /* Scrollbar */
33
+ ::-webkit-scrollbar { width: 5px; }
34
+ ::-webkit-scrollbar-track { background: transparent; }
35
+ ::-webkit-scrollbar-thumb { background: var(--bg3); border-radius: 99px; }
36
+
37
+ /* Animations */
38
+ @keyframes fadeUp {
39
+ from { opacity: 0; transform: translateY(18px); }
40
+ to { opacity: 1; transform: translateY(0); }
41
+ }
42
+ @keyframes shimmer {
43
+ 0% { background-position: -200% center; }
44
+ 100% { background-position: 200% center; }
45
+ }
46
+ @keyframes pulse-ring {
47
+ 0%, 100% { opacity: 0.5; transform: scale(1); }
48
+ 50% { opacity: 1; transform: scale(1.05); }
49
+ }
50
+ @keyframes scanline {
51
+ 0% { transform: translateY(-100%); }
52
+ 100% { transform: translateY(100vh); }
53
+ }
54
+
55
+ .fade-up {
56
+ opacity: 0;
57
+ animation: fadeUp 0.5s cubic-bezier(0.16,1,0.3,1) forwards;
58
+ }
59
+
60
+ /* Noise texture overlay */
61
+ body::before {
62
+ content: '';
63
+ position: fixed; inset: 0;
64
+ background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.03'/%3E%3C/svg%3E");
65
+ background-size: 150px;
66
+ pointer-events: none;
67
+ z-index: 9999;
68
+ opacity: 0.4;
69
+ }
70
+
71
+ /* Gradient mesh background */
72
+ body::after {
73
+ content: '';
74
+ position: fixed; inset: 0;
75
+ background:
76
+ radial-gradient(ellipse 60% 50% at 80% -10%, rgba(0,200,150,0.06) 0%, transparent 60%),
77
+ radial-gradient(ellipse 40% 40% at -10% 80%, rgba(0,150,200,0.04) 0%, transparent 60%);
78
+ pointer-events: none;
79
+ z-index: 0;
80
+ }
ui/app/layout.tsx ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Metadata } from "next";
2
+ import "./globals.css";
3
+
4
+ export const metadata: Metadata = {
5
+ title: "AniFind — AI Anime Recommendations",
6
+ description: "Describe a vibe, a mood, a feeling — find your next favourite anime with AI.",
7
+ };
8
+
9
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
10
+ return (
11
+ <html lang="en">
12
+ <body>{children}</body>
13
+ </html>
14
+ );
15
+ }
ui/app/page.tsx ADDED
@@ -0,0 +1,744 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState, useRef, useCallback, useEffect } from "react";
4
+ import type { Anime, RecommendationResponse, SearchFilters } from "@/lib/types";
5
+
6
+ interface RateLimit {
7
+ count: number;
8
+ limit: number;
9
+ isLocked: boolean;
10
+ remainingHrs: number;
11
+ remainingMs: number;
12
+ }
13
+
14
+ // ─── Constants ────────────────────────────────────────────────────────────────
15
+
16
+ const SUGGESTIONS = [
17
+ { label: "⚔️ Dark fantasy", query: "Dark fantasy with an unreliable narrator" },
18
+ { label: "🤖 Cyberpunk", query: "Fast-paced cyberpunk action with great animation" },
19
+ { label: "💀 Psychological", query: "Psychological thriller that messes with your head" },
20
+ { label: "💘 Romance drama", query: "Bittersweet romance that makes you cry" },
21
+ { label: "🌸 Like Naruto", query: "Action packed themed anime similar to Naruto" },
22
+ ];
23
+
24
+ const ANIME_TYPES = ["Any","TV","Movie","OVA","ONA","Special","Music","CM","PV","TV Special"];
25
+
26
+ const GENRES = [
27
+ "Any","Action","Adventure","Avant Garde","Award Winning","Boys Love","Comedy",
28
+ "Drama","Ecchi","Fantasy","Girls Love","Gourmet","Horror","Mystery","Romance",
29
+ "Sci-Fi","Slice of Life","Sports","Supernatural","Suspense",
30
+ ];
31
+
32
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
33
+
34
+ function getImg(images?: Anime["images"]): string {
35
+ return images?.webp?.image_url || images?.jpg?.image_url || "";
36
+ }
37
+
38
+ function scoreColor(score: number | string | undefined): string {
39
+ const s = parseFloat(String(score));
40
+ if (isNaN(s)) return "#4e6580";
41
+ if (s >= 8) return "#00e5a0";
42
+ if (s >= 7) return "#fbbf24";
43
+ return "#f87171";
44
+ }
45
+
46
+ // Parses the LLM markdown into [{title, body}] pairs
47
+ function parseRecommendations(text: string): { title: string; body: string; pre?: string }[] {
48
+ const sections = text.split(/\*\*(.+?)\*\*/);
49
+ if (sections.length <= 1) return [];
50
+ const pre = sections[0].trim();
51
+ const pairs = Array.from({ length: Math.floor((sections.length - 1) / 2) }, (_, i) => ({
52
+ title: sections[1 + i * 2].trim(),
53
+ body: sections[2 + i * 2].trim().replace(/^[\n—-]+|[\n—-]+$/g, "").trim(),
54
+ pre: i === 0 ? pre : undefined,
55
+ }));
56
+ return pairs;
57
+ }
58
+
59
+ function matchAnime(title: string, pool: Anime[]): Anime | undefined {
60
+ const tl = title.toLowerCase();
61
+ return (
62
+ pool.find(a => (a.title || "").toLowerCase() === tl) ||
63
+ pool.find(a => (a.title_english || "").toLowerCase() === tl) ||
64
+ pool.find(a => {
65
+ const k = (a.title || "").toLowerCase();
66
+ const e = (a.title_english || "").toLowerCase();
67
+ return (k && (k.includes(tl) || tl.includes(k))) ||
68
+ (e && (e.includes(tl) || tl.includes(e)));
69
+ })
70
+ );
71
+ }
72
+
73
+ // ─── Sub-components ───────────────────────────────────────────────────────────
74
+
75
+ function SpotlightCard({ anime }: { anime: Anime }) {
76
+ const img = getImg(anime.images);
77
+ const score = anime.score;
78
+ const sc = scoreColor(score);
79
+ const synopsis = (anime.synopsis || "").slice(0, 240);
80
+ const hasMore = (anime.synopsis || "").length > 240;
81
+ const genres = (anime.genres || []).slice(0, 4);
82
+ const studios = (anime.studios || []).slice(0, 2).join(", ") || "Unknown Studio";
83
+ const engTitle = anime.title_english && anime.title_english !== anime.title ? anime.title_english : null;
84
+
85
+ return (
86
+ <a
87
+ href={anime.mal_url || "#"}
88
+ target="_blank"
89
+ rel="noopener noreferrer"
90
+ className="group flex rounded-2xl overflow-hidden border transition-all duration-300 no-underline"
91
+ style={{
92
+ background: "var(--bg2)",
93
+ borderColor: "var(--bdr)",
94
+ color: "inherit",
95
+ textDecoration: "none",
96
+ }}
97
+ onMouseEnter={e => {
98
+ (e.currentTarget as HTMLElement).style.borderColor = "rgba(0,200,150,0.4)";
99
+ (e.currentTarget as HTMLElement).style.boxShadow = "0 12px 36px rgba(0,200,150,0.12), 0 2px 8px rgba(0,0,0,0.3)";
100
+ (e.currentTarget as HTMLElement).style.transform = "translateY(-3px)";
101
+ }}
102
+ onMouseLeave={e => {
103
+ (e.currentTarget as HTMLElement).style.borderColor = "var(--bdr)";
104
+ (e.currentTarget as HTMLElement).style.boxShadow = "none";
105
+ (e.currentTarget as HTMLElement).style.transform = "translateY(0)";
106
+ }}
107
+ >
108
+ {/* Poster */}
109
+ <div className="relative flex-shrink-0 overflow-hidden" style={{ width: 140, minHeight: 195, background: "var(--bg3)" }}>
110
+ {img && (
111
+ <img
112
+ src={img} alt={anime.title}
113
+ className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
114
+ style={{ display: "block" }}
115
+ onError={e => { (e.target as HTMLImageElement).src = "https://placehold.co/280x400/0d1520/00c896?text=?"; }}
116
+ />
117
+ )}
118
+ <div
119
+ className="absolute top-2 left-2 text-xs font-bold px-2 py-1 rounded-full"
120
+ style={{ background: "rgba(7,12,18,0.85)", color: sc, border: "1px solid rgba(255,255,255,0.1)", backdropFilter: "blur(8px)" }}
121
+ >
122
+ ★ {score ?? "—"}
123
+ </div>
124
+ </div>
125
+
126
+ {/* Body */}
127
+ <div className="flex flex-col gap-2 p-4 flex-1 min-w-0">
128
+ {/* Meta chips */}
129
+ <div className="flex gap-1 flex-wrap">
130
+ {[anime.type, anime.year, `${anime.episodes ?? "?"} ep`, `${Number(anime.scored_by || 0).toLocaleString()} votes`].map((v, i) => v && (
131
+ <span key={i} className="text-xs font-bold px-2 py-0.5 rounded-md uppercase tracking-wide" style={{ background: "var(--adim)", color: "var(--acc)", border: "1px solid rgba(0,200,150,0.2)" }}>
132
+ {v}
133
+ </span>
134
+ ))}
135
+ </div>
136
+
137
+ <div>
138
+ <h3 className="font-bold leading-tight" style={{ fontFamily: "'Space Mono', monospace", fontSize: 17, color: "#fff", letterSpacing: "-0.3px" }}>
139
+ {anime.title}
140
+ </h3>
141
+ {engTitle && <p className="text-xs italic mt-0.5" style={{ color: "#7a9bb8" }}>{engTitle}</p>}
142
+ <p className="text-xs mt-1" style={{ color: "#6b8299" }}>{studios}</p>
143
+ </div>
144
+
145
+ <p className="text-sm leading-relaxed flex-1" style={{ color: "var(--txt)", fontSize: 13 }}>
146
+ {synopsis}{hasMore && "…"}
147
+ </p>
148
+
149
+ <div className="flex gap-1 flex-wrap mt-1">
150
+ {genres.map(g => (
151
+ <span key={g} className="text-xs px-3 py-0.5 rounded-full" style={{ background: "var(--bg3)", border: "1px solid var(--bdr)", color: "var(--txt)" }}>
152
+ {g}
153
+ </span>
154
+ ))}
155
+ </div>
156
+
157
+ <span
158
+ className="text-xs font-bold uppercase tracking-wider px-3 py-2 rounded-lg self-start mt-2 transition-all duration-200 group-hover:text-black"
159
+ style={{ background: "var(--adim)", color: "var(--acc)", border: "1px solid rgba(0,200,150,0.3)" }}
160
+ onMouseEnter={e => { (e.currentTarget as HTMLElement).style.background = "var(--acc)"; (e.currentTarget as HTMLElement).style.color = "#000"; }}
161
+ onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = "var(--adim)"; (e.currentTarget as HTMLElement).style.color = "var(--acc)"; }}
162
+ >
163
+ View on MAL ↗
164
+ </span>
165
+ </div>
166
+ </a>
167
+ );
168
+ }
169
+
170
+ function RetrievedRow({ anime, idx }: { anime: Anime; idx: number }) {
171
+ const img = getImg(anime.images);
172
+ const raw = anime.final_hybrid_score ?? 0;
173
+ const relPct = Math.max(5, Math.round(raw * 100));
174
+ const genres = (anime.genres || []).slice(0, 2).join(" · ");
175
+
176
+ return (
177
+ <a
178
+ href={anime.mal_url || "#"}
179
+ target="_blank"
180
+ rel="noopener noreferrer"
181
+ className="grid items-center gap-2 px-2 py-2 rounded-xl transition-all duration-200 no-underline"
182
+ style={{ gridTemplateColumns: "22px 40px 1fr auto", color: "inherit", textDecoration: "none" }}
183
+ onMouseEnter={e => { (e.currentTarget as HTMLElement).style.background = "var(--bg3)"; (e.currentTarget as HTMLElement).style.transform = "translateX(3px)"; }}
184
+ onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = "transparent"; (e.currentTarget as HTMLElement).style.transform = "translateX(0)"; }}
185
+ >
186
+ <span className="text-center font-bold text-xs" style={{ color: "var(--mut)" }}>{String(idx).padStart(2, "0")}</span>
187
+ <img
188
+ src={img || "https://placehold.co/44x62/0d1520/00c896?text=?"}
189
+ alt={anime.title}
190
+ className="rounded object-cover"
191
+ style={{ width: 40, height: 56 }}
192
+ onError={e => { (e.target as HTMLImageElement).src = "https://placehold.co/44x62/0d1520/00c896?text=?"; }}
193
+ />
194
+ <div className="flex flex-col gap-1 overflow-hidden">
195
+ <span className="text-xs font-bold truncate" style={{ color: "#fff" }}>{anime.title}</span>
196
+ <span className="text-xs truncate" style={{ color: "#6b8299" }}>{anime.type} · {anime.year} · {genres}</span>
197
+ <div className="h-1 rounded-full overflow-hidden" style={{ background: "var(--bg3)" }}>
198
+ <div className="h-full rounded-full" style={{ width: `${relPct}%`, background: "linear-gradient(90deg, var(--acc), var(--acc2))" }} />
199
+ </div>
200
+ </div>
201
+ <div className="flex flex-col items-end gap-1">
202
+ <span className="text-xs font-bold" style={{ color: "var(--amb)" }}>★ {anime.score ?? "—"}</span>
203
+ <span className="text-xs" style={{ color: "var(--mut)" }}>{relPct}%</span>
204
+ </div>
205
+ </a>
206
+ );
207
+ }
208
+
209
+ // ─── Main Page ────────────────────────────────────────────────────────────────
210
+
211
+ export default function Home() {
212
+ const [query, setQuery] = useState("");
213
+ const [animeType, setAnimeType] = useState("Any");
214
+ const [genres, setGenres] = useState<string[]>(["Any"]);
215
+ const [minScore, setMinScore] = useState(7.5);
216
+ const [nResults, setNResults] = useState(3);
217
+ const [loading, setLoading] = useState(false);
218
+ const [result, setResult] = useState<RecommendationResponse | null>(null);
219
+ const [error, setError] = useState<string | null>(null);
220
+ const [filtersOpen, setFiltersOpen] = useState(false);
221
+ const [rateLimit, setRateLimit] = useState<RateLimit>({ count: 0, limit: 10, isLocked: false, remainingHrs: 0, remainingMs: 0 });
222
+ const inputRef = useRef<HTMLTextAreaElement>(null);
223
+
224
+ // Poll rate limit counter every 15 seconds
225
+ useEffect(() => {
226
+ const fetchLimit = async () => {
227
+ try {
228
+ const res = await fetch("/api/recommend");
229
+ if (res.ok) setRateLimit(await res.json());
230
+ } catch { /* silent fail */ }
231
+ };
232
+ fetchLimit();
233
+ const interval = setInterval(fetchLimit, 15000);
234
+ return () => clearInterval(interval);
235
+ }, []);
236
+
237
+ const handleSearch = useCallback(async (q?: string) => {
238
+ const searchQuery = (q ?? query).trim();
239
+ if (!searchQuery) return;
240
+
241
+ // Block immediately if we already know it's locked
242
+ if (rateLimit.isLocked) {
243
+ setError(`Global search limit reached. Resets in ${rateLimit.remainingHrs} hr${rateLimit.remainingHrs !== 1 ? "s" : ""}.`);
244
+ return;
245
+ }
246
+
247
+ setLoading(true);
248
+ setError(null);
249
+ setResult(null);
250
+
251
+ try {
252
+ const payload: SearchFilters = { query: searchQuery, n_results: nResults };
253
+ if (minScore > 0) payload.min_score = minScore;
254
+ if (!genres.includes("Any") && genres.length > 0) payload.genre_filter = genres;
255
+ if (animeType !== "Any") payload.anime_type = animeType;
256
+
257
+ const res = await fetch("/api/recommend", {
258
+ method: "POST",
259
+ headers: { "Content-Type": "application/json" },
260
+ body: JSON.stringify(payload),
261
+ });
262
+
263
+ // Refresh counter after every attempt (success or 429)
264
+ fetch("/api/recommend").then(r => r.ok ? r.json() : null).then(d => { if (d) setRateLimit(d); }).catch(() => {});
265
+
266
+ if (!res.ok) {
267
+ const err = await res.json().catch(() => ({ error: "Unknown error" }));
268
+ throw new Error(err.error || `HTTP ${res.status}`);
269
+ }
270
+
271
+ const data: RecommendationResponse = await res.json();
272
+ setResult(data);
273
+ } catch (e) {
274
+ setError(e instanceof Error ? e.message : "Something went wrong");
275
+ } finally {
276
+ setLoading(false);
277
+ }
278
+ }, [query, animeType, genres, minScore, nResults, rateLimit]);
279
+
280
+ const handleChipClick = (q: string) => {
281
+ setQuery(q);
282
+ handleSearch(q);
283
+ inputRef.current?.focus();
284
+ };
285
+
286
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
287
+ if (e.key === "Enter" && !e.shiftKey) {
288
+ e.preventDefault();
289
+ handleSearch();
290
+ }
291
+ };
292
+
293
+ const toggleGenre = (g: string) => {
294
+ if (g === "Any") { setGenres(["Any"]); return; }
295
+ setGenres(prev => {
296
+ const next = prev.filter(x => x !== "Any");
297
+ return next.includes(g) ? (next.filter(x => x !== g) || ["Any"]) : [...next, g];
298
+ });
299
+ };
300
+
301
+ const parsedRecs = result ? parseRecommendations(result.recommendations) : [];
302
+ const retrievedAnimes = result?.retrieved_animes ?? [];
303
+ const timeStr = result?.metadata["Time taken for LLM + vector search"]?.slice(0, 5) ?? "—";
304
+
305
+ return (
306
+ <div className="flex min-h-screen relative z-10">
307
+
308
+ {/* ── Fixed Left Sidebar ─────────────────────────────── */}
309
+ <aside
310
+ className="fixed top-0 left-0 h-full flex flex-col overflow-y-auto"
311
+ style={{
312
+ width: 230,
313
+ background: "var(--bg)",
314
+ borderRight: "1px solid var(--bdr)",
315
+ padding: "32px 20px",
316
+ zIndex: 50,
317
+ scrollbarWidth: "none",
318
+ boxShadow: "4px 0 24px rgba(0,0,0,0.25)",
319
+ }}
320
+ >
321
+ {/* Logo */}
322
+ <div className="mb-6">
323
+ <div className="flex items-center gap-3 mb-2">
324
+ <div
325
+ className="flex items-center justify-center rounded-xl flex-shrink-0"
326
+ style={{
327
+ width: 44, height: 44, fontSize: 24,
328
+ background: "linear-gradient(135deg, var(--acc), var(--acc2))",
329
+ color: "#070c12",
330
+ boxShadow: "0 0 0 1px rgba(0,200,150,0.3), 0 4px 20px var(--aglow)",
331
+ }}
332
+ >
333
+
334
+ </div>
335
+ <div>
336
+ <div style={{ fontFamily: "'Space Mono', monospace", fontSize: 22, fontWeight: 700, color: "#fff", letterSpacing: "-0.5px", lineHeight: 1 }}>
337
+ Ani<span style={{ color: "var(--acc)" }}>Find</span>
338
+ </div>
339
+ <div style={{ fontFamily: "'Space Mono', monospace", fontSize: 10, color: "var(--acc)", letterSpacing: "2px", marginTop: 3, opacity: 0.7 }}>
340
+ AI · ANIME · RAG
341
+ </div>
342
+ </div>
343
+ </div>
344
+ {/* Accent underline */}
345
+ <div style={{ height: 1, background: "linear-gradient(90deg, var(--acc), transparent)", marginTop: 8, opacity: 0.4 }} />
346
+ </div>
347
+
348
+ <p className="text-sm leading-relaxed mb-8" style={{ color: "#8faabf" }}>
349
+ Describe a vibe, a mood — AI matches it to the perfect title.
350
+ </p>
351
+
352
+ <p className="text-xs font-bold uppercase tracking-widest mb-3" style={{ color: "var(--mut)", letterSpacing: "1.4px" }}>
353
+ Quick searches
354
+ </p>
355
+
356
+ <div className="flex flex-col gap-2">
357
+ {SUGGESTIONS.map(s => (
358
+ <button
359
+ key={s.query}
360
+ onClick={() => handleChipClick(s.query)}
361
+ className="text-left text-sm font-semibold px-3 py-2.5 rounded-xl transition-all duration-300"
362
+ style={{ background: "var(--bg2)", border: "1px solid var(--bdr)", color: "var(--txt)", cursor: "pointer" }}
363
+ onMouseEnter={e => {
364
+ (e.currentTarget as HTMLElement).style.color = "var(--acc)";
365
+ (e.currentTarget as HTMLElement).style.background = "var(--bg3)";
366
+ (e.currentTarget as HTMLElement).style.borderColor = "rgba(0,200,150,0.4)";
367
+ (e.currentTarget as HTMLElement).style.transform = "translateX(4px)";
368
+ }}
369
+ onMouseLeave={e => {
370
+ (e.currentTarget as HTMLElement).style.color = "var(--txt)";
371
+ (e.currentTarget as HTMLElement).style.background = "var(--bg2)";
372
+ (e.currentTarget as HTMLElement).style.borderColor = "var(--bdr)";
373
+ (e.currentTarget as HTMLElement).style.transform = "translateX(0)";
374
+ }}
375
+ >
376
+ {s.label}
377
+ </button>
378
+ ))}
379
+ </div>
380
+
381
+ {/* Global rate limit counter */}
382
+ <div className="mt-auto pt-6">
383
+ <div
384
+ className="rounded-xl p-3"
385
+ style={{
386
+ background: rateLimit.isLocked ? "rgba(248,113,113,0.08)" : "var(--bg2)",
387
+ border: `1px solid ${rateLimit.isLocked ? "rgba(248,113,113,0.3)" : "var(--bdr)"}`,
388
+ }}
389
+ >
390
+ <div className="flex justify-between items-center mb-2">
391
+ <span className="text-xs font-bold uppercase tracking-widest" style={{ color: "var(--mut)", letterSpacing: "1px" }}>
392
+ Global searches
393
+ </span>
394
+ <span
395
+ className="text-xs font-bold px-2 py-0.5 rounded-full"
396
+ style={{
397
+ background: rateLimit.isLocked ? "rgba(248,113,113,0.15)" : rateLimit.count >= 8 ? "rgba(251,191,36,0.12)" : "var(--adim)",
398
+ color: rateLimit.isLocked ? "var(--red)" : rateLimit.count >= 8 ? "var(--amb)" : "var(--acc)",
399
+ }}
400
+ >
401
+ {rateLimit.count}/{rateLimit.limit}
402
+ </span>
403
+ </div>
404
+
405
+ {/* Progress bar */}
406
+ <div className="rounded-full overflow-hidden" style={{ height: 4, background: "var(--bg3)" }}>
407
+ <div
408
+ className="h-full rounded-full transition-all duration-500"
409
+ style={{
410
+ width: `${(rateLimit.count / rateLimit.limit) * 100}%`,
411
+ background: rateLimit.isLocked
412
+ ? "var(--red)"
413
+ : rateLimit.count >= 8
414
+ ? "var(--amb)"
415
+ : "linear-gradient(90deg, var(--acc), var(--acc2))",
416
+ }}
417
+ />
418
+ </div>
419
+
420
+ {rateLimit.isLocked ? (
421
+ <p className="text-xs mt-2" style={{ color: "var(--red)" }}>
422
+ Locked · resets in {rateLimit.remainingHrs}h
423
+ </p>
424
+ ) : (
425
+ <p className="text-xs mt-2" style={{ color: "var(--mut)" }}>
426
+ {rateLimit.limit - rateLimit.count} remaining · resets after {rateLimit.limit} total
427
+ </p>
428
+ )}
429
+ </div>
430
+
431
+ {/* Footer credit */}
432
+ <div className="pt-4 text-xs" style={{ color: "var(--mut)" }}>
433
+ Powered by Qdrant + Supabase<br />
434
+ <span style={{ color: "rgba(0,200,150,0.6)" }}>RAG · Cross-Encoder · LLM</span>
435
+ </div>
436
+ </div>
437
+ </aside>
438
+
439
+ {/* ── Main Content ───────────────────────────────────── */}
440
+ <main className="flex flex-col flex-1" style={{ marginLeft: 230 }}>
441
+
442
+ {/* Sticky Search Bar */}
443
+ <div
444
+ className="sticky top-0 z-40 flex items-center gap-3 px-6 py-4"
445
+ style={{
446
+ background: "rgba(7,12,18,0.88)",
447
+ backdropFilter: "blur(20px)",
448
+ borderBottom: "1px solid var(--bdr)",
449
+ }}
450
+ >
451
+ <div
452
+ className="flex-1 flex items-start rounded-2xl transition-all duration-300"
453
+ style={{
454
+ background: "var(--bg2)",
455
+ border: `2px solid ${rateLimit.isLocked ? "rgba(248,113,113,0.3)" : "var(--bdr)"}`,
456
+ opacity: rateLimit.isLocked ? 0.6 : 1,
457
+ }}
458
+ onFocusCapture={e => { if (!rateLimit.isLocked) { (e.currentTarget as HTMLElement).style.borderColor = "var(--acc)"; (e.currentTarget as HTMLElement).style.boxShadow = "0 0 0 3px var(--adim)"; } }}
459
+ onBlurCapture={e => { (e.currentTarget as HTMLElement).style.borderColor = rateLimit.isLocked ? "rgba(248,113,113,0.3)" : "var(--bdr)"; (e.currentTarget as HTMLElement).style.boxShadow = "none"; }}
460
+ >
461
+ <textarea
462
+ ref={inputRef}
463
+ rows={1}
464
+ value={query}
465
+ onChange={e => setQuery(e.target.value)}
466
+ onKeyDown={handleKeyDown}
467
+ disabled={rateLimit.isLocked}
468
+ placeholder={rateLimit.isLocked ? `Search locked — resets in ${rateLimit.remainingHrs}h` : "e.g. A melancholic sci-fi about memory and loss..."}
469
+ className="flex-1 resize-none bg-transparent outline-none px-4 py-3 text-sm"
470
+ style={{ color: rateLimit.isLocked ? "var(--red)" : "#fff", fontFamily: "DM Sans, sans-serif", fontSize: 14, lineHeight: 1.5, cursor: rateLimit.isLocked ? "not-allowed" : "text" }}
471
+ />
472
+ </div>
473
+
474
+ {/* Filter toggle */}
475
+ <button
476
+ onClick={() => setFiltersOpen(o => !o)}
477
+ className="px-4 py-3 rounded-xl text-sm font-semibold transition-all duration-200"
478
+ style={{
479
+ background: filtersOpen ? "var(--adim)" : "var(--bg2)",
480
+ border: `1px solid ${filtersOpen ? "rgba(0,200,150,0.4)" : "var(--bdr)"}`,
481
+ color: filtersOpen ? "var(--acc)" : "var(--txt)",
482
+ cursor: "pointer",
483
+ }}
484
+ >
485
+ ⚙ Filters
486
+ </button>
487
+
488
+ {/* Search button */}
489
+ <button
490
+ onClick={() => handleSearch()}
491
+ disabled={loading}
492
+ className="flex items-center justify-center rounded-xl font-black text-lg transition-all duration-300"
493
+ style={{
494
+ width: 48, height: 48,
495
+ background: rateLimit.isLocked
496
+ ? "rgba(248,113,113,0.2)"
497
+ : loading ? "rgba(0,200,150,0.3)"
498
+ : "linear-gradient(135deg, var(--acc), var(--acc2))",
499
+ color: rateLimit.isLocked ? "var(--red)" : "#000",
500
+ border: rateLimit.isLocked ? "1px solid rgba(248,113,113,0.3)" : "none",
501
+ cursor: (loading || rateLimit.isLocked) ? "not-allowed" : "pointer",
502
+ boxShadow: (loading || rateLimit.isLocked) ? "none" : "0 4px 16px rgba(0,200,150,0.4)",
503
+ flexShrink: 0,
504
+ }}
505
+ onMouseEnter={e => { if (!loading && !rateLimit.isLocked) { (e.currentTarget as HTMLElement).style.transform = "translateY(-2px) scale(1.05)"; (e.currentTarget as HTMLElement).style.boxShadow = "0 8px 24px rgba(0,200,150,0.5)"; } }}
506
+ onMouseLeave={e => { (e.currentTarget as HTMLElement).style.transform = "none"; (e.currentTarget as HTMLElement).style.boxShadow = (loading || rateLimit.isLocked) ? "none" : "0 4px 16px rgba(0,200,150,0.4)"; }}
507
+ >
508
+ {rateLimit.isLocked ? "✕" : loading ? "⟳" : "➔"}
509
+ </button>
510
+ </div>
511
+
512
+ {/* Filters Panel */}
513
+ {filtersOpen && (
514
+ <div
515
+ className="px-6 py-5 flex flex-wrap gap-6 items-start"
516
+ style={{ background: "var(--bg2)", borderBottom: "1px solid var(--bdr)" }}
517
+ >
518
+ {/* Anime Type */}
519
+ <div>
520
+ <label className="block text-xs font-bold uppercase tracking-widest mb-2" style={{ color: "var(--mut)" }}>Type</label>
521
+ <div className="flex flex-wrap gap-1">
522
+ {ANIME_TYPES.map(t => (
523
+ <button
524
+ key={t}
525
+ onClick={() => setAnimeType(t)}
526
+ className="text-xs font-semibold px-3 py-1 rounded-lg transition-all duration-150"
527
+ style={{
528
+ background: animeType === t ? "var(--adim)" : "var(--bg3)",
529
+ border: `1px solid ${animeType === t ? "rgba(0,200,150,0.4)" : "var(--bdr)"}`,
530
+ color: animeType === t ? "var(--acc)" : "var(--txt)",
531
+ cursor: "pointer",
532
+ }}
533
+ >
534
+ {t}
535
+ </button>
536
+ ))}
537
+ </div>
538
+ </div>
539
+
540
+ {/* Genre */}
541
+ <div>
542
+ <label className="block text-xs font-bold uppercase tracking-widest mb-2" style={{ color: "var(--mut)" }}>Genre</label>
543
+ <div className="flex flex-wrap gap-1" style={{ maxWidth: 420 }}>
544
+ {GENRES.map(g => {
545
+ const active = genres.includes(g);
546
+ return (
547
+ <button
548
+ key={g}
549
+ onClick={() => toggleGenre(g)}
550
+ className="text-xs font-semibold px-3 py-1 rounded-lg transition-all duration-150"
551
+ style={{
552
+ background: active ? "var(--adim)" : "var(--bg3)",
553
+ border: `1px solid ${active ? "rgba(0,200,150,0.4)" : "var(--bdr)"}`,
554
+ color: active ? "var(--acc)" : "var(--txt)",
555
+ cursor: "pointer",
556
+ }}
557
+ >
558
+ {g}
559
+ </button>
560
+ );
561
+ })}
562
+ </div>
563
+ </div>
564
+
565
+ {/* Min Score */}
566
+ <div style={{ minWidth: 180 }}>
567
+ <label className="block text-xs font-bold uppercase tracking-widest mb-2" style={{ color: "var(--mut)" }}>
568
+ Min Rating: <span style={{ color: "var(--acc)" }}>{minScore > 0 ? minScore : "Any"}</span>
569
+ </label>
570
+ <input
571
+ type="range" min={0} max={10} step={0.5} value={minScore}
572
+ onChange={e => setMinScore(parseFloat(e.target.value))}
573
+ className="w-full accent-emerald-400"
574
+ />
575
+ </div>
576
+
577
+ {/* N Results */}
578
+ <div style={{ minWidth: 160 }}>
579
+ <label className="block text-xs font-bold uppercase tracking-widest mb-2" style={{ color: "var(--mut)" }}>
580
+ Results: <span style={{ color: "var(--acc)" }}>{nResults}</span>
581
+ </label>
582
+ <input
583
+ type="range" min={1} max={8} step={1} value={nResults}
584
+ onChange={e => setNResults(parseInt(e.target.value))}
585
+ className="w-full accent-emerald-400"
586
+ />
587
+ </div>
588
+ </div>
589
+ )}
590
+
591
+ {/* Output Zone */}
592
+ <div className="flex-1 px-6 py-6">
593
+
594
+ {/* HUD Bar */}
595
+ {(loading || result || error) && (
596
+ <div
597
+ className="flex items-center justify-between rounded-xl px-5 py-3 mb-6"
598
+ style={{ background: "var(--bg2)", border: "1px solid var(--bdr)" }}
599
+ >
600
+ {loading && (
601
+ <div className="flex items-center gap-3">
602
+ <div className="w-2 h-2 rounded-full" style={{ background: "var(--acc)", animation: "pulse-ring 1.2s ease-in-out infinite" }} />
603
+ <span className="text-sm font-semibold" style={{ color: "var(--acc)" }}>Searching through anime universe…</span>
604
+ </div>
605
+ )}
606
+ {error && (
607
+ <span className="text-sm font-bold px-4 py-1.5 rounded-full" style={{ background: "rgba(248,113,113,0.1)", color: "var(--red)", border: "1px solid rgba(248,113,113,0.2)" }}>
608
+ ✗ {error}
609
+ </span>
610
+ )}
611
+ {result && !loading && (
612
+ <>
613
+ <span className="text-sm font-bold px-4 py-1.5 rounded-full" style={{ background: "var(--adim)", color: "var(--acc)", border: "1px solid rgba(0,200,150,0.3)" }}>
614
+ ✦ Results ready
615
+ </span>
616
+ <div className="flex items-center gap-5">
617
+ <div className="flex flex-col items-end">
618
+ <span className="font-black text-lg leading-none" style={{ color: "#fff", fontFamily: "'Space Mono', monospace" }}>{result.retrieved_count}</span>
619
+ <span className="text-xs uppercase tracking-wider mt-0.5" style={{ color: "var(--mut)" }}>titles scanned</span>
620
+ </div>
621
+ <div className="w-px h-7" style={{ background: "var(--bdr)" }} />
622
+ <div className="flex flex-col items-end">
623
+ <span className="font-black text-lg leading-none" style={{ color: "#fff", fontFamily: "'Space Mono', monospace" }}>{timeStr}s</span>
624
+ <span className="text-xs uppercase tracking-wider mt-0.5" style={{ color: "var(--mut)" }}>search time</span>
625
+ </div>
626
+ </div>
627
+ </>
628
+ )}
629
+ </div>
630
+ )}
631
+
632
+ {/* Empty state */}
633
+ {!loading && !result && !error && (
634
+ <div className="flex flex-col items-center justify-center py-24 gap-4 text-center">
635
+ <div
636
+ className="text-5xl mb-2"
637
+ style={{ filter: "drop-shadow(0 4px 16px var(--aglow))", animation: "pulse-ring 3s ease-in-out infinite" }}
638
+ >
639
+
640
+ </div>
641
+ <h2 className="text-2xl font-black" style={{ fontFamily: "'Space Mono', monospace", color: "#fff" }}>
642
+ What are you in the mood for?
643
+ </h2>
644
+ <p className="text-sm max-w-xs" style={{ color: "var(--mut)" }}>
645
+ Type a vibe, a feeling, or a genre above — or pick a quick search from the sidebar.
646
+ </p>
647
+ </div>
648
+ )}
649
+
650
+ {/* Loading skeleton */}
651
+ {loading && (
652
+ <div className="flex gap-6">
653
+ <div className="flex-1 flex flex-col gap-4">
654
+ {Array.from({ length: nResults }).map((_, i) => (
655
+ <div key={i} className="flex rounded-2xl overflow-hidden" style={{ background: "var(--bg2)", border: "1px solid var(--bdr)", height: 200, opacity: 1 - i * 0.2 }}>
656
+ <div className="flex-shrink-0" style={{ width: 140, background: "var(--bg3)" }} />
657
+ <div className="flex-1 p-4 flex flex-col gap-3">
658
+ <div className="flex gap-2">
659
+ {[60, 50, 80, 90].map((w, j) => <div key={j} className="rounded-md h-5" style={{ width: w, background: "var(--bg3)" }} />)}
660
+ </div>
661
+ <div className="rounded-lg h-5 w-3/4" style={{ background: "var(--bg3)" }} />
662
+ <div className="rounded-lg h-4 w-1/3" style={{ background: "var(--bg3)" }} />
663
+ <div className="flex-1 rounded-lg" style={{ background: "var(--bg3)" }} />
664
+ </div>
665
+ </div>
666
+ ))}
667
+ </div>
668
+ <div className="w-72 rounded-2xl flex-shrink-0" style={{ background: "var(--bg2)", border: "1px solid var(--bdr)", height: 400 }} />
669
+ </div>
670
+ )}
671
+
672
+ {/* Results */}
673
+ {result && !loading && (
674
+ <div className="flex gap-6 items-start">
675
+
676
+ {/* Recommendations column */}
677
+ <div className="flex-1 min-w-0">
678
+ <p className="text-xs font-bold uppercase tracking-widest mb-5 pb-3" style={{ color: "var(--mut)", borderBottom: "1px solid var(--bdr)", letterSpacing: "1.5px" }}>
679
+ Recommendations
680
+ </p>
681
+
682
+ {parsedRecs.length === 0 ? (
683
+ <p className="text-sm leading-relaxed px-1" style={{ color: "#b0c8e0", fontSize: 15 }}>{result.recommendations}</p>
684
+ ) : (
685
+ parsedRecs.map((rec, i) => {
686
+ const anime = matchAnime(rec.title, retrievedAnimes);
687
+ return (
688
+ <div
689
+ key={i}
690
+ className="fade-up rounded-2xl overflow-hidden mb-5"
691
+ style={{
692
+ animationDelay: `${i * 0.1}s`,
693
+ border: "1px solid var(--bdr)",
694
+ background: "var(--bg2)",
695
+ }}
696
+ >
697
+ {rec.pre && i === 0 && (
698
+ <p className="px-5 pt-4 pb-2 text-sm italic" style={{ color: "#b0c8e0" }}>{rec.pre}</p>
699
+ )}
700
+ {anime
701
+ ? <SpotlightCard anime={anime} />
702
+ : <div className="px-5 py-4 font-bold" style={{ fontFamily: "'Space Mono', monospace", color: "#fff" }}>{rec.title}</div>
703
+ }
704
+ <div className="px-5 py-4 text-sm leading-relaxed" style={{ borderTop: "1px solid var(--bdr)", background: "rgba(255,255,255,0.015)", color: "#b8cde0" }}>
705
+ {rec.body}
706
+ </div>
707
+ </div>
708
+ );
709
+ })
710
+ )}
711
+ </div>
712
+
713
+ {/* Retrieved sidebar */}
714
+ {retrievedAnimes.length > 0 && (
715
+ <aside
716
+ className="flex-shrink-0 rounded-2xl p-4 sticky overflow-y-auto"
717
+ style={{
718
+ width: 280,
719
+ top: 80,
720
+ maxHeight: "calc(100vh - 120px)",
721
+ background: "var(--bg2)",
722
+ border: "1px solid var(--bdr)",
723
+ scrollbarWidth: "thin",
724
+ scrollbarColor: "var(--bg3) transparent",
725
+ }}
726
+ >
727
+ <div className="flex justify-between items-center mb-4 pb-3" style={{ borderBottom: "1px solid var(--bdr)" }}>
728
+ <span className="text-xs font-bold uppercase tracking-widest" style={{ color: "var(--mut)" }}>Vector search</span>
729
+ <span className="text-xs font-bold px-2 py-0.5 rounded-full" style={{ background: "var(--adim)", color: "var(--acc)" }}>{result.retrieved_count}</span>
730
+ </div>
731
+ <div className="flex flex-col gap-1">
732
+ {retrievedAnimes.map((a, i) => (
733
+ <RetrievedRow key={i} anime={a} idx={i + 1} />
734
+ ))}
735
+ </div>
736
+ </aside>
737
+ )}
738
+ </div>
739
+ )}
740
+ </div>
741
+ </main>
742
+ </div>
743
+ );
744
+ }
ui/lib/types.ts ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface AnimeImages {
2
+ webp?: { image_url?: string };
3
+ jpg?: { image_url?: string };
4
+ }
5
+
6
+ export interface Anime {
7
+ title: string;
8
+ title_english?: string;
9
+ score?: number | string;
10
+ scored_by?: number | string;
11
+ year?: number | string;
12
+ type?: string;
13
+ episodes?: number | string;
14
+ genres?: string[];
15
+ studios?: string[];
16
+ synopsis?: string;
17
+ mal_url?: string;
18
+ images?: AnimeImages;
19
+ final_hybrid_score?: number;
20
+ }
21
+
22
+ export interface RecommendationResponse {
23
+ query: string;
24
+ recommendations: string;
25
+ retrieved_count: number;
26
+ retrieved_animes: Anime[];
27
+ metadata: {
28
+ model: string;
29
+ retriever_k: number;
30
+ "Time taken for LLM + vector search": string;
31
+ };
32
+ }
33
+
34
+ export interface SearchFilters {
35
+ query: string;
36
+ n_results: number;
37
+ min_score?: number;
38
+ genre_filter?: string[];
39
+ anime_type?: string;
40
+ }
ui/next.config.mjs CHANGED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {};
3
+ export default nextConfig;
ui/package-lock.json ADDED
@@ -0,0 +1,1929 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "anifind",
3
+ "version": "0.1.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "anifind",
9
+ "version": "0.1.0",
10
+ "dependencies": {
11
+ "next": "15.1.0",
12
+ "react": "^19.0.0",
13
+ "react-dom": "^19.0.0"
14
+ },
15
+ "devDependencies": {
16
+ "@types/node": "^20",
17
+ "@types/react": "^19",
18
+ "@types/react-dom": "^19",
19
+ "autoprefixer": "^10.0.1",
20
+ "postcss": "^8",
21
+ "tailwindcss": "^3.4.1",
22
+ "typescript": "^5"
23
+ }
24
+ },
25
+ "node_modules/@alloc/quick-lru": {
26
+ "version": "5.2.0",
27
+ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
28
+ "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
29
+ "dev": true,
30
+ "engines": {
31
+ "node": ">=10"
32
+ },
33
+ "funding": {
34
+ "url": "https://github.com/sponsors/sindresorhus"
35
+ }
36
+ },
37
+ "node_modules/@emnapi/runtime": {
38
+ "version": "1.8.1",
39
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz",
40
+ "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==",
41
+ "optional": true,
42
+ "dependencies": {
43
+ "tslib": "^2.4.0"
44
+ }
45
+ },
46
+ "node_modules/@img/sharp-darwin-arm64": {
47
+ "version": "0.33.5",
48
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
49
+ "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==",
50
+ "cpu": [
51
+ "arm64"
52
+ ],
53
+ "optional": true,
54
+ "os": [
55
+ "darwin"
56
+ ],
57
+ "engines": {
58
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
59
+ },
60
+ "funding": {
61
+ "url": "https://opencollective.com/libvips"
62
+ },
63
+ "optionalDependencies": {
64
+ "@img/sharp-libvips-darwin-arm64": "1.0.4"
65
+ }
66
+ },
67
+ "node_modules/@img/sharp-darwin-x64": {
68
+ "version": "0.33.5",
69
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz",
70
+ "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==",
71
+ "cpu": [
72
+ "x64"
73
+ ],
74
+ "optional": true,
75
+ "os": [
76
+ "darwin"
77
+ ],
78
+ "engines": {
79
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
80
+ },
81
+ "funding": {
82
+ "url": "https://opencollective.com/libvips"
83
+ },
84
+ "optionalDependencies": {
85
+ "@img/sharp-libvips-darwin-x64": "1.0.4"
86
+ }
87
+ },
88
+ "node_modules/@img/sharp-libvips-darwin-arm64": {
89
+ "version": "1.0.4",
90
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz",
91
+ "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==",
92
+ "cpu": [
93
+ "arm64"
94
+ ],
95
+ "optional": true,
96
+ "os": [
97
+ "darwin"
98
+ ],
99
+ "funding": {
100
+ "url": "https://opencollective.com/libvips"
101
+ }
102
+ },
103
+ "node_modules/@img/sharp-libvips-darwin-x64": {
104
+ "version": "1.0.4",
105
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz",
106
+ "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==",
107
+ "cpu": [
108
+ "x64"
109
+ ],
110
+ "optional": true,
111
+ "os": [
112
+ "darwin"
113
+ ],
114
+ "funding": {
115
+ "url": "https://opencollective.com/libvips"
116
+ }
117
+ },
118
+ "node_modules/@img/sharp-libvips-linux-arm": {
119
+ "version": "1.0.5",
120
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz",
121
+ "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==",
122
+ "cpu": [
123
+ "arm"
124
+ ],
125
+ "optional": true,
126
+ "os": [
127
+ "linux"
128
+ ],
129
+ "funding": {
130
+ "url": "https://opencollective.com/libvips"
131
+ }
132
+ },
133
+ "node_modules/@img/sharp-libvips-linux-arm64": {
134
+ "version": "1.0.4",
135
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz",
136
+ "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==",
137
+ "cpu": [
138
+ "arm64"
139
+ ],
140
+ "optional": true,
141
+ "os": [
142
+ "linux"
143
+ ],
144
+ "funding": {
145
+ "url": "https://opencollective.com/libvips"
146
+ }
147
+ },
148
+ "node_modules/@img/sharp-libvips-linux-s390x": {
149
+ "version": "1.0.4",
150
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz",
151
+ "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==",
152
+ "cpu": [
153
+ "s390x"
154
+ ],
155
+ "optional": true,
156
+ "os": [
157
+ "linux"
158
+ ],
159
+ "funding": {
160
+ "url": "https://opencollective.com/libvips"
161
+ }
162
+ },
163
+ "node_modules/@img/sharp-libvips-linux-x64": {
164
+ "version": "1.0.4",
165
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz",
166
+ "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==",
167
+ "cpu": [
168
+ "x64"
169
+ ],
170
+ "optional": true,
171
+ "os": [
172
+ "linux"
173
+ ],
174
+ "funding": {
175
+ "url": "https://opencollective.com/libvips"
176
+ }
177
+ },
178
+ "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
179
+ "version": "1.0.4",
180
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz",
181
+ "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==",
182
+ "cpu": [
183
+ "arm64"
184
+ ],
185
+ "optional": true,
186
+ "os": [
187
+ "linux"
188
+ ],
189
+ "funding": {
190
+ "url": "https://opencollective.com/libvips"
191
+ }
192
+ },
193
+ "node_modules/@img/sharp-libvips-linuxmusl-x64": {
194
+ "version": "1.0.4",
195
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz",
196
+ "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==",
197
+ "cpu": [
198
+ "x64"
199
+ ],
200
+ "optional": true,
201
+ "os": [
202
+ "linux"
203
+ ],
204
+ "funding": {
205
+ "url": "https://opencollective.com/libvips"
206
+ }
207
+ },
208
+ "node_modules/@img/sharp-linux-arm": {
209
+ "version": "0.33.5",
210
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz",
211
+ "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==",
212
+ "cpu": [
213
+ "arm"
214
+ ],
215
+ "optional": true,
216
+ "os": [
217
+ "linux"
218
+ ],
219
+ "engines": {
220
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
221
+ },
222
+ "funding": {
223
+ "url": "https://opencollective.com/libvips"
224
+ },
225
+ "optionalDependencies": {
226
+ "@img/sharp-libvips-linux-arm": "1.0.5"
227
+ }
228
+ },
229
+ "node_modules/@img/sharp-linux-arm64": {
230
+ "version": "0.33.5",
231
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz",
232
+ "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==",
233
+ "cpu": [
234
+ "arm64"
235
+ ],
236
+ "optional": true,
237
+ "os": [
238
+ "linux"
239
+ ],
240
+ "engines": {
241
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
242
+ },
243
+ "funding": {
244
+ "url": "https://opencollective.com/libvips"
245
+ },
246
+ "optionalDependencies": {
247
+ "@img/sharp-libvips-linux-arm64": "1.0.4"
248
+ }
249
+ },
250
+ "node_modules/@img/sharp-linux-s390x": {
251
+ "version": "0.33.5",
252
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz",
253
+ "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==",
254
+ "cpu": [
255
+ "s390x"
256
+ ],
257
+ "optional": true,
258
+ "os": [
259
+ "linux"
260
+ ],
261
+ "engines": {
262
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
263
+ },
264
+ "funding": {
265
+ "url": "https://opencollective.com/libvips"
266
+ },
267
+ "optionalDependencies": {
268
+ "@img/sharp-libvips-linux-s390x": "1.0.4"
269
+ }
270
+ },
271
+ "node_modules/@img/sharp-linux-x64": {
272
+ "version": "0.33.5",
273
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz",
274
+ "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==",
275
+ "cpu": [
276
+ "x64"
277
+ ],
278
+ "optional": true,
279
+ "os": [
280
+ "linux"
281
+ ],
282
+ "engines": {
283
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
284
+ },
285
+ "funding": {
286
+ "url": "https://opencollective.com/libvips"
287
+ },
288
+ "optionalDependencies": {
289
+ "@img/sharp-libvips-linux-x64": "1.0.4"
290
+ }
291
+ },
292
+ "node_modules/@img/sharp-linuxmusl-arm64": {
293
+ "version": "0.33.5",
294
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz",
295
+ "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==",
296
+ "cpu": [
297
+ "arm64"
298
+ ],
299
+ "optional": true,
300
+ "os": [
301
+ "linux"
302
+ ],
303
+ "engines": {
304
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
305
+ },
306
+ "funding": {
307
+ "url": "https://opencollective.com/libvips"
308
+ },
309
+ "optionalDependencies": {
310
+ "@img/sharp-libvips-linuxmusl-arm64": "1.0.4"
311
+ }
312
+ },
313
+ "node_modules/@img/sharp-linuxmusl-x64": {
314
+ "version": "0.33.5",
315
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz",
316
+ "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==",
317
+ "cpu": [
318
+ "x64"
319
+ ],
320
+ "optional": true,
321
+ "os": [
322
+ "linux"
323
+ ],
324
+ "engines": {
325
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
326
+ },
327
+ "funding": {
328
+ "url": "https://opencollective.com/libvips"
329
+ },
330
+ "optionalDependencies": {
331
+ "@img/sharp-libvips-linuxmusl-x64": "1.0.4"
332
+ }
333
+ },
334
+ "node_modules/@img/sharp-wasm32": {
335
+ "version": "0.33.5",
336
+ "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz",
337
+ "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==",
338
+ "cpu": [
339
+ "wasm32"
340
+ ],
341
+ "optional": true,
342
+ "dependencies": {
343
+ "@emnapi/runtime": "^1.2.0"
344
+ },
345
+ "engines": {
346
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
347
+ },
348
+ "funding": {
349
+ "url": "https://opencollective.com/libvips"
350
+ }
351
+ },
352
+ "node_modules/@img/sharp-win32-ia32": {
353
+ "version": "0.33.5",
354
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz",
355
+ "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==",
356
+ "cpu": [
357
+ "ia32"
358
+ ],
359
+ "optional": true,
360
+ "os": [
361
+ "win32"
362
+ ],
363
+ "engines": {
364
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
365
+ },
366
+ "funding": {
367
+ "url": "https://opencollective.com/libvips"
368
+ }
369
+ },
370
+ "node_modules/@img/sharp-win32-x64": {
371
+ "version": "0.33.5",
372
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz",
373
+ "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==",
374
+ "cpu": [
375
+ "x64"
376
+ ],
377
+ "optional": true,
378
+ "os": [
379
+ "win32"
380
+ ],
381
+ "engines": {
382
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
383
+ },
384
+ "funding": {
385
+ "url": "https://opencollective.com/libvips"
386
+ }
387
+ },
388
+ "node_modules/@jridgewell/gen-mapping": {
389
+ "version": "0.3.13",
390
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
391
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
392
+ "dev": true,
393
+ "dependencies": {
394
+ "@jridgewell/sourcemap-codec": "^1.5.0",
395
+ "@jridgewell/trace-mapping": "^0.3.24"
396
+ }
397
+ },
398
+ "node_modules/@jridgewell/resolve-uri": {
399
+ "version": "3.1.2",
400
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
401
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
402
+ "dev": true,
403
+ "engines": {
404
+ "node": ">=6.0.0"
405
+ }
406
+ },
407
+ "node_modules/@jridgewell/sourcemap-codec": {
408
+ "version": "1.5.5",
409
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
410
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
411
+ "dev": true
412
+ },
413
+ "node_modules/@jridgewell/trace-mapping": {
414
+ "version": "0.3.31",
415
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
416
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
417
+ "dev": true,
418
+ "dependencies": {
419
+ "@jridgewell/resolve-uri": "^3.1.0",
420
+ "@jridgewell/sourcemap-codec": "^1.4.14"
421
+ }
422
+ },
423
+ "node_modules/@next/env": {
424
+ "version": "15.1.0",
425
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-15.1.0.tgz",
426
+ "integrity": "sha512-UcCO481cROsqJuszPPXJnb7GGuLq617ve4xuAyyNG4VSSocJNtMU5Fsx+Lp6mlN8c7W58aZLc5y6D/2xNmaK+w=="
427
+ },
428
+ "node_modules/@next/swc-darwin-arm64": {
429
+ "version": "15.1.0",
430
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.1.0.tgz",
431
+ "integrity": "sha512-ZU8d7xxpX14uIaFC3nsr4L++5ZS/AkWDm1PzPO6gD9xWhFkOj2hzSbSIxoncsnlJXB1CbLOfGVN4Zk9tg83PUw==",
432
+ "cpu": [
433
+ "arm64"
434
+ ],
435
+ "optional": true,
436
+ "os": [
437
+ "darwin"
438
+ ],
439
+ "engines": {
440
+ "node": ">= 10"
441
+ }
442
+ },
443
+ "node_modules/@next/swc-darwin-x64": {
444
+ "version": "15.1.0",
445
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.1.0.tgz",
446
+ "integrity": "sha512-DQ3RiUoW2XC9FcSM4ffpfndq1EsLV0fj0/UY33i7eklW5akPUCo6OX2qkcLXZ3jyPdo4sf2flwAED3AAq3Om2Q==",
447
+ "cpu": [
448
+ "x64"
449
+ ],
450
+ "optional": true,
451
+ "os": [
452
+ "darwin"
453
+ ],
454
+ "engines": {
455
+ "node": ">= 10"
456
+ }
457
+ },
458
+ "node_modules/@next/swc-linux-arm64-gnu": {
459
+ "version": "15.1.0",
460
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.1.0.tgz",
461
+ "integrity": "sha512-M+vhTovRS2F//LMx9KtxbkWk627l5Q7AqXWWWrfIzNIaUFiz2/NkOFkxCFyNyGACi5YbA8aekzCLtbDyfF/v5Q==",
462
+ "cpu": [
463
+ "arm64"
464
+ ],
465
+ "optional": true,
466
+ "os": [
467
+ "linux"
468
+ ],
469
+ "engines": {
470
+ "node": ">= 10"
471
+ }
472
+ },
473
+ "node_modules/@next/swc-linux-arm64-musl": {
474
+ "version": "15.1.0",
475
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.1.0.tgz",
476
+ "integrity": "sha512-Qn6vOuwaTCx3pNwygpSGtdIu0TfS1KiaYLYXLH5zq1scoTXdwYfdZtwvJTpB1WrLgiQE2Ne2kt8MZok3HlFqmg==",
477
+ "cpu": [
478
+ "arm64"
479
+ ],
480
+ "optional": true,
481
+ "os": [
482
+ "linux"
483
+ ],
484
+ "engines": {
485
+ "node": ">= 10"
486
+ }
487
+ },
488
+ "node_modules/@next/swc-linux-x64-gnu": {
489
+ "version": "15.1.0",
490
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.1.0.tgz",
491
+ "integrity": "sha512-yeNh9ofMqzOZ5yTOk+2rwncBzucc6a1lyqtg8xZv0rH5znyjxHOWsoUtSq4cUTeeBIiXXX51QOOe+VoCjdXJRw==",
492
+ "cpu": [
493
+ "x64"
494
+ ],
495
+ "optional": true,
496
+ "os": [
497
+ "linux"
498
+ ],
499
+ "engines": {
500
+ "node": ">= 10"
501
+ }
502
+ },
503
+ "node_modules/@next/swc-linux-x64-musl": {
504
+ "version": "15.1.0",
505
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.1.0.tgz",
506
+ "integrity": "sha512-t9IfNkHQs/uKgPoyEtU912MG6a1j7Had37cSUyLTKx9MnUpjj+ZDKw9OyqTI9OwIIv0wmkr1pkZy+3T5pxhJPg==",
507
+ "cpu": [
508
+ "x64"
509
+ ],
510
+ "optional": true,
511
+ "os": [
512
+ "linux"
513
+ ],
514
+ "engines": {
515
+ "node": ">= 10"
516
+ }
517
+ },
518
+ "node_modules/@next/swc-win32-arm64-msvc": {
519
+ "version": "15.1.0",
520
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.1.0.tgz",
521
+ "integrity": "sha512-WEAoHyG14t5sTavZa1c6BnOIEukll9iqFRTavqRVPfYmfegOAd5MaZfXgOGG6kGo1RduyGdTHD4+YZQSdsNZXg==",
522
+ "cpu": [
523
+ "arm64"
524
+ ],
525
+ "optional": true,
526
+ "os": [
527
+ "win32"
528
+ ],
529
+ "engines": {
530
+ "node": ">= 10"
531
+ }
532
+ },
533
+ "node_modules/@next/swc-win32-x64-msvc": {
534
+ "version": "15.1.0",
535
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.0.tgz",
536
+ "integrity": "sha512-J1YdKuJv9xcixzXR24Dv+4SaDKc2jj31IVUEMdO5xJivMTXuE6MAdIi4qPjSymHuFG8O5wbfWKnhJUcHHpj5CA==",
537
+ "cpu": [
538
+ "x64"
539
+ ],
540
+ "optional": true,
541
+ "os": [
542
+ "win32"
543
+ ],
544
+ "engines": {
545
+ "node": ">= 10"
546
+ }
547
+ },
548
+ "node_modules/@nodelib/fs.scandir": {
549
+ "version": "2.1.5",
550
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
551
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
552
+ "dev": true,
553
+ "dependencies": {
554
+ "@nodelib/fs.stat": "2.0.5",
555
+ "run-parallel": "^1.1.9"
556
+ },
557
+ "engines": {
558
+ "node": ">= 8"
559
+ }
560
+ },
561
+ "node_modules/@nodelib/fs.stat": {
562
+ "version": "2.0.5",
563
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
564
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
565
+ "dev": true,
566
+ "engines": {
567
+ "node": ">= 8"
568
+ }
569
+ },
570
+ "node_modules/@nodelib/fs.walk": {
571
+ "version": "1.2.8",
572
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
573
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
574
+ "dev": true,
575
+ "dependencies": {
576
+ "@nodelib/fs.scandir": "2.1.5",
577
+ "fastq": "^1.6.0"
578
+ },
579
+ "engines": {
580
+ "node": ">= 8"
581
+ }
582
+ },
583
+ "node_modules/@swc/counter": {
584
+ "version": "0.1.3",
585
+ "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
586
+ "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="
587
+ },
588
+ "node_modules/@swc/helpers": {
589
+ "version": "0.5.15",
590
+ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
591
+ "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
592
+ "dependencies": {
593
+ "tslib": "^2.8.0"
594
+ }
595
+ },
596
+ "node_modules/@types/node": {
597
+ "version": "20.19.37",
598
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz",
599
+ "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==",
600
+ "dev": true,
601
+ "dependencies": {
602
+ "undici-types": "~6.21.0"
603
+ }
604
+ },
605
+ "node_modules/@types/react": {
606
+ "version": "19.2.14",
607
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
608
+ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
609
+ "dev": true,
610
+ "dependencies": {
611
+ "csstype": "^3.2.2"
612
+ }
613
+ },
614
+ "node_modules/@types/react-dom": {
615
+ "version": "19.2.3",
616
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
617
+ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
618
+ "dev": true,
619
+ "peerDependencies": {
620
+ "@types/react": "^19.2.0"
621
+ }
622
+ },
623
+ "node_modules/any-promise": {
624
+ "version": "1.3.0",
625
+ "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
626
+ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
627
+ "dev": true
628
+ },
629
+ "node_modules/anymatch": {
630
+ "version": "3.1.3",
631
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
632
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
633
+ "dev": true,
634
+ "dependencies": {
635
+ "normalize-path": "^3.0.0",
636
+ "picomatch": "^2.0.4"
637
+ },
638
+ "engines": {
639
+ "node": ">= 8"
640
+ }
641
+ },
642
+ "node_modules/arg": {
643
+ "version": "5.0.2",
644
+ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
645
+ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
646
+ "dev": true
647
+ },
648
+ "node_modules/autoprefixer": {
649
+ "version": "10.4.27",
650
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz",
651
+ "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==",
652
+ "dev": true,
653
+ "funding": [
654
+ {
655
+ "type": "opencollective",
656
+ "url": "https://opencollective.com/postcss/"
657
+ },
658
+ {
659
+ "type": "tidelift",
660
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
661
+ },
662
+ {
663
+ "type": "github",
664
+ "url": "https://github.com/sponsors/ai"
665
+ }
666
+ ],
667
+ "dependencies": {
668
+ "browserslist": "^4.28.1",
669
+ "caniuse-lite": "^1.0.30001774",
670
+ "fraction.js": "^5.3.4",
671
+ "picocolors": "^1.1.1",
672
+ "postcss-value-parser": "^4.2.0"
673
+ },
674
+ "bin": {
675
+ "autoprefixer": "bin/autoprefixer"
676
+ },
677
+ "engines": {
678
+ "node": "^10 || ^12 || >=14"
679
+ },
680
+ "peerDependencies": {
681
+ "postcss": "^8.1.0"
682
+ }
683
+ },
684
+ "node_modules/baseline-browser-mapping": {
685
+ "version": "2.10.0",
686
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz",
687
+ "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==",
688
+ "dev": true,
689
+ "bin": {
690
+ "baseline-browser-mapping": "dist/cli.cjs"
691
+ },
692
+ "engines": {
693
+ "node": ">=6.0.0"
694
+ }
695
+ },
696
+ "node_modules/binary-extensions": {
697
+ "version": "2.3.0",
698
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
699
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
700
+ "dev": true,
701
+ "engines": {
702
+ "node": ">=8"
703
+ },
704
+ "funding": {
705
+ "url": "https://github.com/sponsors/sindresorhus"
706
+ }
707
+ },
708
+ "node_modules/braces": {
709
+ "version": "3.0.3",
710
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
711
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
712
+ "dev": true,
713
+ "dependencies": {
714
+ "fill-range": "^7.1.1"
715
+ },
716
+ "engines": {
717
+ "node": ">=8"
718
+ }
719
+ },
720
+ "node_modules/browserslist": {
721
+ "version": "4.28.1",
722
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
723
+ "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
724
+ "dev": true,
725
+ "funding": [
726
+ {
727
+ "type": "opencollective",
728
+ "url": "https://opencollective.com/browserslist"
729
+ },
730
+ {
731
+ "type": "tidelift",
732
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
733
+ },
734
+ {
735
+ "type": "github",
736
+ "url": "https://github.com/sponsors/ai"
737
+ }
738
+ ],
739
+ "dependencies": {
740
+ "baseline-browser-mapping": "^2.9.0",
741
+ "caniuse-lite": "^1.0.30001759",
742
+ "electron-to-chromium": "^1.5.263",
743
+ "node-releases": "^2.0.27",
744
+ "update-browserslist-db": "^1.2.0"
745
+ },
746
+ "bin": {
747
+ "browserslist": "cli.js"
748
+ },
749
+ "engines": {
750
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
751
+ }
752
+ },
753
+ "node_modules/busboy": {
754
+ "version": "1.6.0",
755
+ "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
756
+ "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
757
+ "dependencies": {
758
+ "streamsearch": "^1.1.0"
759
+ },
760
+ "engines": {
761
+ "node": ">=10.16.0"
762
+ }
763
+ },
764
+ "node_modules/camelcase-css": {
765
+ "version": "2.0.1",
766
+ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
767
+ "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
768
+ "dev": true,
769
+ "engines": {
770
+ "node": ">= 6"
771
+ }
772
+ },
773
+ "node_modules/caniuse-lite": {
774
+ "version": "1.0.30001777",
775
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz",
776
+ "integrity": "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==",
777
+ "funding": [
778
+ {
779
+ "type": "opencollective",
780
+ "url": "https://opencollective.com/browserslist"
781
+ },
782
+ {
783
+ "type": "tidelift",
784
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
785
+ },
786
+ {
787
+ "type": "github",
788
+ "url": "https://github.com/sponsors/ai"
789
+ }
790
+ ]
791
+ },
792
+ "node_modules/chokidar": {
793
+ "version": "3.6.0",
794
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
795
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
796
+ "dev": true,
797
+ "dependencies": {
798
+ "anymatch": "~3.1.2",
799
+ "braces": "~3.0.2",
800
+ "glob-parent": "~5.1.2",
801
+ "is-binary-path": "~2.1.0",
802
+ "is-glob": "~4.0.1",
803
+ "normalize-path": "~3.0.0",
804
+ "readdirp": "~3.6.0"
805
+ },
806
+ "engines": {
807
+ "node": ">= 8.10.0"
808
+ },
809
+ "funding": {
810
+ "url": "https://paulmillr.com/funding/"
811
+ },
812
+ "optionalDependencies": {
813
+ "fsevents": "~2.3.2"
814
+ }
815
+ },
816
+ "node_modules/chokidar/node_modules/glob-parent": {
817
+ "version": "5.1.2",
818
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
819
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
820
+ "dev": true,
821
+ "dependencies": {
822
+ "is-glob": "^4.0.1"
823
+ },
824
+ "engines": {
825
+ "node": ">= 6"
826
+ }
827
+ },
828
+ "node_modules/client-only": {
829
+ "version": "0.0.1",
830
+ "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
831
+ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
832
+ },
833
+ "node_modules/color": {
834
+ "version": "4.2.3",
835
+ "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
836
+ "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
837
+ "optional": true,
838
+ "dependencies": {
839
+ "color-convert": "^2.0.1",
840
+ "color-string": "^1.9.0"
841
+ },
842
+ "engines": {
843
+ "node": ">=12.5.0"
844
+ }
845
+ },
846
+ "node_modules/color-convert": {
847
+ "version": "2.0.1",
848
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
849
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
850
+ "optional": true,
851
+ "dependencies": {
852
+ "color-name": "~1.1.4"
853
+ },
854
+ "engines": {
855
+ "node": ">=7.0.0"
856
+ }
857
+ },
858
+ "node_modules/color-name": {
859
+ "version": "1.1.4",
860
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
861
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
862
+ "optional": true
863
+ },
864
+ "node_modules/color-string": {
865
+ "version": "1.9.1",
866
+ "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
867
+ "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
868
+ "optional": true,
869
+ "dependencies": {
870
+ "color-name": "^1.0.0",
871
+ "simple-swizzle": "^0.2.2"
872
+ }
873
+ },
874
+ "node_modules/commander": {
875
+ "version": "4.1.1",
876
+ "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
877
+ "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
878
+ "dev": true,
879
+ "engines": {
880
+ "node": ">= 6"
881
+ }
882
+ },
883
+ "node_modules/cssesc": {
884
+ "version": "3.0.0",
885
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
886
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
887
+ "dev": true,
888
+ "bin": {
889
+ "cssesc": "bin/cssesc"
890
+ },
891
+ "engines": {
892
+ "node": ">=4"
893
+ }
894
+ },
895
+ "node_modules/csstype": {
896
+ "version": "3.2.3",
897
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
898
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
899
+ "dev": true
900
+ },
901
+ "node_modules/detect-libc": {
902
+ "version": "2.1.2",
903
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
904
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
905
+ "optional": true,
906
+ "engines": {
907
+ "node": ">=8"
908
+ }
909
+ },
910
+ "node_modules/didyoumean": {
911
+ "version": "1.2.2",
912
+ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
913
+ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
914
+ "dev": true
915
+ },
916
+ "node_modules/dlv": {
917
+ "version": "1.1.3",
918
+ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
919
+ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
920
+ "dev": true
921
+ },
922
+ "node_modules/electron-to-chromium": {
923
+ "version": "1.5.307",
924
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz",
925
+ "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==",
926
+ "dev": true
927
+ },
928
+ "node_modules/escalade": {
929
+ "version": "3.2.0",
930
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
931
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
932
+ "dev": true,
933
+ "engines": {
934
+ "node": ">=6"
935
+ }
936
+ },
937
+ "node_modules/fast-glob": {
938
+ "version": "3.3.3",
939
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
940
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
941
+ "dev": true,
942
+ "dependencies": {
943
+ "@nodelib/fs.stat": "^2.0.2",
944
+ "@nodelib/fs.walk": "^1.2.3",
945
+ "glob-parent": "^5.1.2",
946
+ "merge2": "^1.3.0",
947
+ "micromatch": "^4.0.8"
948
+ },
949
+ "engines": {
950
+ "node": ">=8.6.0"
951
+ }
952
+ },
953
+ "node_modules/fast-glob/node_modules/glob-parent": {
954
+ "version": "5.1.2",
955
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
956
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
957
+ "dev": true,
958
+ "dependencies": {
959
+ "is-glob": "^4.0.1"
960
+ },
961
+ "engines": {
962
+ "node": ">= 6"
963
+ }
964
+ },
965
+ "node_modules/fastq": {
966
+ "version": "1.20.1",
967
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
968
+ "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
969
+ "dev": true,
970
+ "dependencies": {
971
+ "reusify": "^1.0.4"
972
+ }
973
+ },
974
+ "node_modules/fill-range": {
975
+ "version": "7.1.1",
976
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
977
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
978
+ "dev": true,
979
+ "dependencies": {
980
+ "to-regex-range": "^5.0.1"
981
+ },
982
+ "engines": {
983
+ "node": ">=8"
984
+ }
985
+ },
986
+ "node_modules/fraction.js": {
987
+ "version": "5.3.4",
988
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
989
+ "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
990
+ "dev": true,
991
+ "engines": {
992
+ "node": "*"
993
+ },
994
+ "funding": {
995
+ "type": "github",
996
+ "url": "https://github.com/sponsors/rawify"
997
+ }
998
+ },
999
+ "node_modules/fsevents": {
1000
+ "version": "2.3.3",
1001
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
1002
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
1003
+ "dev": true,
1004
+ "hasInstallScript": true,
1005
+ "optional": true,
1006
+ "os": [
1007
+ "darwin"
1008
+ ],
1009
+ "engines": {
1010
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1011
+ }
1012
+ },
1013
+ "node_modules/function-bind": {
1014
+ "version": "1.1.2",
1015
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
1016
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
1017
+ "dev": true,
1018
+ "funding": {
1019
+ "url": "https://github.com/sponsors/ljharb"
1020
+ }
1021
+ },
1022
+ "node_modules/glob-parent": {
1023
+ "version": "6.0.2",
1024
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
1025
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
1026
+ "dev": true,
1027
+ "dependencies": {
1028
+ "is-glob": "^4.0.3"
1029
+ },
1030
+ "engines": {
1031
+ "node": ">=10.13.0"
1032
+ }
1033
+ },
1034
+ "node_modules/hasown": {
1035
+ "version": "2.0.2",
1036
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
1037
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
1038
+ "dev": true,
1039
+ "dependencies": {
1040
+ "function-bind": "^1.1.2"
1041
+ },
1042
+ "engines": {
1043
+ "node": ">= 0.4"
1044
+ }
1045
+ },
1046
+ "node_modules/is-arrayish": {
1047
+ "version": "0.3.4",
1048
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz",
1049
+ "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==",
1050
+ "optional": true
1051
+ },
1052
+ "node_modules/is-binary-path": {
1053
+ "version": "2.1.0",
1054
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
1055
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
1056
+ "dev": true,
1057
+ "dependencies": {
1058
+ "binary-extensions": "^2.0.0"
1059
+ },
1060
+ "engines": {
1061
+ "node": ">=8"
1062
+ }
1063
+ },
1064
+ "node_modules/is-core-module": {
1065
+ "version": "2.16.1",
1066
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
1067
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
1068
+ "dev": true,
1069
+ "dependencies": {
1070
+ "hasown": "^2.0.2"
1071
+ },
1072
+ "engines": {
1073
+ "node": ">= 0.4"
1074
+ },
1075
+ "funding": {
1076
+ "url": "https://github.com/sponsors/ljharb"
1077
+ }
1078
+ },
1079
+ "node_modules/is-extglob": {
1080
+ "version": "2.1.1",
1081
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
1082
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
1083
+ "dev": true,
1084
+ "engines": {
1085
+ "node": ">=0.10.0"
1086
+ }
1087
+ },
1088
+ "node_modules/is-glob": {
1089
+ "version": "4.0.3",
1090
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
1091
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
1092
+ "dev": true,
1093
+ "dependencies": {
1094
+ "is-extglob": "^2.1.1"
1095
+ },
1096
+ "engines": {
1097
+ "node": ">=0.10.0"
1098
+ }
1099
+ },
1100
+ "node_modules/is-number": {
1101
+ "version": "7.0.0",
1102
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
1103
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
1104
+ "dev": true,
1105
+ "engines": {
1106
+ "node": ">=0.12.0"
1107
+ }
1108
+ },
1109
+ "node_modules/jiti": {
1110
+ "version": "1.21.7",
1111
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
1112
+ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
1113
+ "dev": true,
1114
+ "bin": {
1115
+ "jiti": "bin/jiti.js"
1116
+ }
1117
+ },
1118
+ "node_modules/lilconfig": {
1119
+ "version": "3.1.3",
1120
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
1121
+ "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
1122
+ "dev": true,
1123
+ "engines": {
1124
+ "node": ">=14"
1125
+ },
1126
+ "funding": {
1127
+ "url": "https://github.com/sponsors/antonk52"
1128
+ }
1129
+ },
1130
+ "node_modules/lines-and-columns": {
1131
+ "version": "1.2.4",
1132
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
1133
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
1134
+ "dev": true
1135
+ },
1136
+ "node_modules/merge2": {
1137
+ "version": "1.4.1",
1138
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
1139
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
1140
+ "dev": true,
1141
+ "engines": {
1142
+ "node": ">= 8"
1143
+ }
1144
+ },
1145
+ "node_modules/micromatch": {
1146
+ "version": "4.0.8",
1147
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
1148
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
1149
+ "dev": true,
1150
+ "dependencies": {
1151
+ "braces": "^3.0.3",
1152
+ "picomatch": "^2.3.1"
1153
+ },
1154
+ "engines": {
1155
+ "node": ">=8.6"
1156
+ }
1157
+ },
1158
+ "node_modules/mz": {
1159
+ "version": "2.7.0",
1160
+ "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
1161
+ "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
1162
+ "dev": true,
1163
+ "dependencies": {
1164
+ "any-promise": "^1.0.0",
1165
+ "object-assign": "^4.0.1",
1166
+ "thenify-all": "^1.0.0"
1167
+ }
1168
+ },
1169
+ "node_modules/nanoid": {
1170
+ "version": "3.3.11",
1171
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
1172
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
1173
+ "funding": [
1174
+ {
1175
+ "type": "github",
1176
+ "url": "https://github.com/sponsors/ai"
1177
+ }
1178
+ ],
1179
+ "bin": {
1180
+ "nanoid": "bin/nanoid.cjs"
1181
+ },
1182
+ "engines": {
1183
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
1184
+ }
1185
+ },
1186
+ "node_modules/next": {
1187
+ "version": "15.1.0",
1188
+ "resolved": "https://registry.npmjs.org/next/-/next-15.1.0.tgz",
1189
+ "integrity": "sha512-QKhzt6Y8rgLNlj30izdMbxAwjHMFANnLwDwZ+WQh5sMhyt4lEBqDK9QpvWHtIM4rINKPoJ8aiRZKg5ULSybVHw==",
1190
+ "deprecated": "This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/CVE-2025-66478 for more details.",
1191
+ "dependencies": {
1192
+ "@next/env": "15.1.0",
1193
+ "@swc/counter": "0.1.3",
1194
+ "@swc/helpers": "0.5.15",
1195
+ "busboy": "1.6.0",
1196
+ "caniuse-lite": "^1.0.30001579",
1197
+ "postcss": "8.4.31",
1198
+ "styled-jsx": "5.1.6"
1199
+ },
1200
+ "bin": {
1201
+ "next": "dist/bin/next"
1202
+ },
1203
+ "engines": {
1204
+ "node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
1205
+ },
1206
+ "optionalDependencies": {
1207
+ "@next/swc-darwin-arm64": "15.1.0",
1208
+ "@next/swc-darwin-x64": "15.1.0",
1209
+ "@next/swc-linux-arm64-gnu": "15.1.0",
1210
+ "@next/swc-linux-arm64-musl": "15.1.0",
1211
+ "@next/swc-linux-x64-gnu": "15.1.0",
1212
+ "@next/swc-linux-x64-musl": "15.1.0",
1213
+ "@next/swc-win32-arm64-msvc": "15.1.0",
1214
+ "@next/swc-win32-x64-msvc": "15.1.0",
1215
+ "sharp": "^0.33.5"
1216
+ },
1217
+ "peerDependencies": {
1218
+ "@opentelemetry/api": "^1.1.0",
1219
+ "@playwright/test": "^1.41.2",
1220
+ "babel-plugin-react-compiler": "*",
1221
+ "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
1222
+ "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
1223
+ "sass": "^1.3.0"
1224
+ },
1225
+ "peerDependenciesMeta": {
1226
+ "@opentelemetry/api": {
1227
+ "optional": true
1228
+ },
1229
+ "@playwright/test": {
1230
+ "optional": true
1231
+ },
1232
+ "babel-plugin-react-compiler": {
1233
+ "optional": true
1234
+ },
1235
+ "sass": {
1236
+ "optional": true
1237
+ }
1238
+ }
1239
+ },
1240
+ "node_modules/next/node_modules/postcss": {
1241
+ "version": "8.4.31",
1242
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
1243
+ "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
1244
+ "funding": [
1245
+ {
1246
+ "type": "opencollective",
1247
+ "url": "https://opencollective.com/postcss/"
1248
+ },
1249
+ {
1250
+ "type": "tidelift",
1251
+ "url": "https://tidelift.com/funding/github/npm/postcss"
1252
+ },
1253
+ {
1254
+ "type": "github",
1255
+ "url": "https://github.com/sponsors/ai"
1256
+ }
1257
+ ],
1258
+ "dependencies": {
1259
+ "nanoid": "^3.3.6",
1260
+ "picocolors": "^1.0.0",
1261
+ "source-map-js": "^1.0.2"
1262
+ },
1263
+ "engines": {
1264
+ "node": "^10 || ^12 || >=14"
1265
+ }
1266
+ },
1267
+ "node_modules/node-releases": {
1268
+ "version": "2.0.36",
1269
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz",
1270
+ "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==",
1271
+ "dev": true
1272
+ },
1273
+ "node_modules/normalize-path": {
1274
+ "version": "3.0.0",
1275
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
1276
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
1277
+ "dev": true,
1278
+ "engines": {
1279
+ "node": ">=0.10.0"
1280
+ }
1281
+ },
1282
+ "node_modules/object-assign": {
1283
+ "version": "4.1.1",
1284
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
1285
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
1286
+ "dev": true,
1287
+ "engines": {
1288
+ "node": ">=0.10.0"
1289
+ }
1290
+ },
1291
+ "node_modules/object-hash": {
1292
+ "version": "3.0.0",
1293
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
1294
+ "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
1295
+ "dev": true,
1296
+ "engines": {
1297
+ "node": ">= 6"
1298
+ }
1299
+ },
1300
+ "node_modules/path-parse": {
1301
+ "version": "1.0.7",
1302
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
1303
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
1304
+ "dev": true
1305
+ },
1306
+ "node_modules/picocolors": {
1307
+ "version": "1.1.1",
1308
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
1309
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
1310
+ },
1311
+ "node_modules/picomatch": {
1312
+ "version": "2.3.1",
1313
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
1314
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
1315
+ "dev": true,
1316
+ "engines": {
1317
+ "node": ">=8.6"
1318
+ },
1319
+ "funding": {
1320
+ "url": "https://github.com/sponsors/jonschlinkert"
1321
+ }
1322
+ },
1323
+ "node_modules/pify": {
1324
+ "version": "2.3.0",
1325
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
1326
+ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
1327
+ "dev": true,
1328
+ "engines": {
1329
+ "node": ">=0.10.0"
1330
+ }
1331
+ },
1332
+ "node_modules/pirates": {
1333
+ "version": "4.0.7",
1334
+ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
1335
+ "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
1336
+ "dev": true,
1337
+ "engines": {
1338
+ "node": ">= 6"
1339
+ }
1340
+ },
1341
+ "node_modules/postcss": {
1342
+ "version": "8.5.8",
1343
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
1344
+ "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
1345
+ "dev": true,
1346
+ "funding": [
1347
+ {
1348
+ "type": "opencollective",
1349
+ "url": "https://opencollective.com/postcss/"
1350
+ },
1351
+ {
1352
+ "type": "tidelift",
1353
+ "url": "https://tidelift.com/funding/github/npm/postcss"
1354
+ },
1355
+ {
1356
+ "type": "github",
1357
+ "url": "https://github.com/sponsors/ai"
1358
+ }
1359
+ ],
1360
+ "dependencies": {
1361
+ "nanoid": "^3.3.11",
1362
+ "picocolors": "^1.1.1",
1363
+ "source-map-js": "^1.2.1"
1364
+ },
1365
+ "engines": {
1366
+ "node": "^10 || ^12 || >=14"
1367
+ }
1368
+ },
1369
+ "node_modules/postcss-import": {
1370
+ "version": "15.1.0",
1371
+ "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
1372
+ "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
1373
+ "dev": true,
1374
+ "dependencies": {
1375
+ "postcss-value-parser": "^4.0.0",
1376
+ "read-cache": "^1.0.0",
1377
+ "resolve": "^1.1.7"
1378
+ },
1379
+ "engines": {
1380
+ "node": ">=14.0.0"
1381
+ },
1382
+ "peerDependencies": {
1383
+ "postcss": "^8.0.0"
1384
+ }
1385
+ },
1386
+ "node_modules/postcss-js": {
1387
+ "version": "4.1.0",
1388
+ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
1389
+ "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
1390
+ "dev": true,
1391
+ "funding": [
1392
+ {
1393
+ "type": "opencollective",
1394
+ "url": "https://opencollective.com/postcss/"
1395
+ },
1396
+ {
1397
+ "type": "github",
1398
+ "url": "https://github.com/sponsors/ai"
1399
+ }
1400
+ ],
1401
+ "dependencies": {
1402
+ "camelcase-css": "^2.0.1"
1403
+ },
1404
+ "engines": {
1405
+ "node": "^12 || ^14 || >= 16"
1406
+ },
1407
+ "peerDependencies": {
1408
+ "postcss": "^8.4.21"
1409
+ }
1410
+ },
1411
+ "node_modules/postcss-load-config": {
1412
+ "version": "6.0.1",
1413
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
1414
+ "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
1415
+ "dev": true,
1416
+ "funding": [
1417
+ {
1418
+ "type": "opencollective",
1419
+ "url": "https://opencollective.com/postcss/"
1420
+ },
1421
+ {
1422
+ "type": "github",
1423
+ "url": "https://github.com/sponsors/ai"
1424
+ }
1425
+ ],
1426
+ "dependencies": {
1427
+ "lilconfig": "^3.1.1"
1428
+ },
1429
+ "engines": {
1430
+ "node": ">= 18"
1431
+ },
1432
+ "peerDependencies": {
1433
+ "jiti": ">=1.21.0",
1434
+ "postcss": ">=8.0.9",
1435
+ "tsx": "^4.8.1",
1436
+ "yaml": "^2.4.2"
1437
+ },
1438
+ "peerDependenciesMeta": {
1439
+ "jiti": {
1440
+ "optional": true
1441
+ },
1442
+ "postcss": {
1443
+ "optional": true
1444
+ },
1445
+ "tsx": {
1446
+ "optional": true
1447
+ },
1448
+ "yaml": {
1449
+ "optional": true
1450
+ }
1451
+ }
1452
+ },
1453
+ "node_modules/postcss-nested": {
1454
+ "version": "6.2.0",
1455
+ "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
1456
+ "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
1457
+ "dev": true,
1458
+ "funding": [
1459
+ {
1460
+ "type": "opencollective",
1461
+ "url": "https://opencollective.com/postcss/"
1462
+ },
1463
+ {
1464
+ "type": "github",
1465
+ "url": "https://github.com/sponsors/ai"
1466
+ }
1467
+ ],
1468
+ "dependencies": {
1469
+ "postcss-selector-parser": "^6.1.1"
1470
+ },
1471
+ "engines": {
1472
+ "node": ">=12.0"
1473
+ },
1474
+ "peerDependencies": {
1475
+ "postcss": "^8.2.14"
1476
+ }
1477
+ },
1478
+ "node_modules/postcss-selector-parser": {
1479
+ "version": "6.1.2",
1480
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
1481
+ "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
1482
+ "dev": true,
1483
+ "dependencies": {
1484
+ "cssesc": "^3.0.0",
1485
+ "util-deprecate": "^1.0.2"
1486
+ },
1487
+ "engines": {
1488
+ "node": ">=4"
1489
+ }
1490
+ },
1491
+ "node_modules/postcss-value-parser": {
1492
+ "version": "4.2.0",
1493
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
1494
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
1495
+ "dev": true
1496
+ },
1497
+ "node_modules/queue-microtask": {
1498
+ "version": "1.2.3",
1499
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
1500
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
1501
+ "dev": true,
1502
+ "funding": [
1503
+ {
1504
+ "type": "github",
1505
+ "url": "https://github.com/sponsors/feross"
1506
+ },
1507
+ {
1508
+ "type": "patreon",
1509
+ "url": "https://www.patreon.com/feross"
1510
+ },
1511
+ {
1512
+ "type": "consulting",
1513
+ "url": "https://feross.org/support"
1514
+ }
1515
+ ]
1516
+ },
1517
+ "node_modules/react": {
1518
+ "version": "19.2.4",
1519
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
1520
+ "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
1521
+ "engines": {
1522
+ "node": ">=0.10.0"
1523
+ }
1524
+ },
1525
+ "node_modules/react-dom": {
1526
+ "version": "19.2.4",
1527
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
1528
+ "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
1529
+ "dependencies": {
1530
+ "scheduler": "^0.27.0"
1531
+ },
1532
+ "peerDependencies": {
1533
+ "react": "^19.2.4"
1534
+ }
1535
+ },
1536
+ "node_modules/read-cache": {
1537
+ "version": "1.0.0",
1538
+ "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
1539
+ "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
1540
+ "dev": true,
1541
+ "dependencies": {
1542
+ "pify": "^2.3.0"
1543
+ }
1544
+ },
1545
+ "node_modules/readdirp": {
1546
+ "version": "3.6.0",
1547
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
1548
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
1549
+ "dev": true,
1550
+ "dependencies": {
1551
+ "picomatch": "^2.2.1"
1552
+ },
1553
+ "engines": {
1554
+ "node": ">=8.10.0"
1555
+ }
1556
+ },
1557
+ "node_modules/resolve": {
1558
+ "version": "1.22.11",
1559
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
1560
+ "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
1561
+ "dev": true,
1562
+ "dependencies": {
1563
+ "is-core-module": "^2.16.1",
1564
+ "path-parse": "^1.0.7",
1565
+ "supports-preserve-symlinks-flag": "^1.0.0"
1566
+ },
1567
+ "bin": {
1568
+ "resolve": "bin/resolve"
1569
+ },
1570
+ "engines": {
1571
+ "node": ">= 0.4"
1572
+ },
1573
+ "funding": {
1574
+ "url": "https://github.com/sponsors/ljharb"
1575
+ }
1576
+ },
1577
+ "node_modules/reusify": {
1578
+ "version": "1.1.0",
1579
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
1580
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
1581
+ "dev": true,
1582
+ "engines": {
1583
+ "iojs": ">=1.0.0",
1584
+ "node": ">=0.10.0"
1585
+ }
1586
+ },
1587
+ "node_modules/run-parallel": {
1588
+ "version": "1.2.0",
1589
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
1590
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
1591
+ "dev": true,
1592
+ "funding": [
1593
+ {
1594
+ "type": "github",
1595
+ "url": "https://github.com/sponsors/feross"
1596
+ },
1597
+ {
1598
+ "type": "patreon",
1599
+ "url": "https://www.patreon.com/feross"
1600
+ },
1601
+ {
1602
+ "type": "consulting",
1603
+ "url": "https://feross.org/support"
1604
+ }
1605
+ ],
1606
+ "dependencies": {
1607
+ "queue-microtask": "^1.2.2"
1608
+ }
1609
+ },
1610
+ "node_modules/scheduler": {
1611
+ "version": "0.27.0",
1612
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
1613
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="
1614
+ },
1615
+ "node_modules/semver": {
1616
+ "version": "7.7.4",
1617
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
1618
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
1619
+ "optional": true,
1620
+ "bin": {
1621
+ "semver": "bin/semver.js"
1622
+ },
1623
+ "engines": {
1624
+ "node": ">=10"
1625
+ }
1626
+ },
1627
+ "node_modules/sharp": {
1628
+ "version": "0.33.5",
1629
+ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz",
1630
+ "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==",
1631
+ "hasInstallScript": true,
1632
+ "optional": true,
1633
+ "dependencies": {
1634
+ "color": "^4.2.3",
1635
+ "detect-libc": "^2.0.3",
1636
+ "semver": "^7.6.3"
1637
+ },
1638
+ "engines": {
1639
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
1640
+ },
1641
+ "funding": {
1642
+ "url": "https://opencollective.com/libvips"
1643
+ },
1644
+ "optionalDependencies": {
1645
+ "@img/sharp-darwin-arm64": "0.33.5",
1646
+ "@img/sharp-darwin-x64": "0.33.5",
1647
+ "@img/sharp-libvips-darwin-arm64": "1.0.4",
1648
+ "@img/sharp-libvips-darwin-x64": "1.0.4",
1649
+ "@img/sharp-libvips-linux-arm": "1.0.5",
1650
+ "@img/sharp-libvips-linux-arm64": "1.0.4",
1651
+ "@img/sharp-libvips-linux-s390x": "1.0.4",
1652
+ "@img/sharp-libvips-linux-x64": "1.0.4",
1653
+ "@img/sharp-libvips-linuxmusl-arm64": "1.0.4",
1654
+ "@img/sharp-libvips-linuxmusl-x64": "1.0.4",
1655
+ "@img/sharp-linux-arm": "0.33.5",
1656
+ "@img/sharp-linux-arm64": "0.33.5",
1657
+ "@img/sharp-linux-s390x": "0.33.5",
1658
+ "@img/sharp-linux-x64": "0.33.5",
1659
+ "@img/sharp-linuxmusl-arm64": "0.33.5",
1660
+ "@img/sharp-linuxmusl-x64": "0.33.5",
1661
+ "@img/sharp-wasm32": "0.33.5",
1662
+ "@img/sharp-win32-ia32": "0.33.5",
1663
+ "@img/sharp-win32-x64": "0.33.5"
1664
+ }
1665
+ },
1666
+ "node_modules/simple-swizzle": {
1667
+ "version": "0.2.4",
1668
+ "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
1669
+ "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==",
1670
+ "optional": true,
1671
+ "dependencies": {
1672
+ "is-arrayish": "^0.3.1"
1673
+ }
1674
+ },
1675
+ "node_modules/source-map-js": {
1676
+ "version": "1.2.1",
1677
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
1678
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
1679
+ "engines": {
1680
+ "node": ">=0.10.0"
1681
+ }
1682
+ },
1683
+ "node_modules/streamsearch": {
1684
+ "version": "1.1.0",
1685
+ "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
1686
+ "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
1687
+ "engines": {
1688
+ "node": ">=10.0.0"
1689
+ }
1690
+ },
1691
+ "node_modules/styled-jsx": {
1692
+ "version": "5.1.6",
1693
+ "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
1694
+ "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==",
1695
+ "dependencies": {
1696
+ "client-only": "0.0.1"
1697
+ },
1698
+ "engines": {
1699
+ "node": ">= 12.0.0"
1700
+ },
1701
+ "peerDependencies": {
1702
+ "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0"
1703
+ },
1704
+ "peerDependenciesMeta": {
1705
+ "@babel/core": {
1706
+ "optional": true
1707
+ },
1708
+ "babel-plugin-macros": {
1709
+ "optional": true
1710
+ }
1711
+ }
1712
+ },
1713
+ "node_modules/sucrase": {
1714
+ "version": "3.35.1",
1715
+ "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
1716
+ "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
1717
+ "dev": true,
1718
+ "dependencies": {
1719
+ "@jridgewell/gen-mapping": "^0.3.2",
1720
+ "commander": "^4.0.0",
1721
+ "lines-and-columns": "^1.1.6",
1722
+ "mz": "^2.7.0",
1723
+ "pirates": "^4.0.1",
1724
+ "tinyglobby": "^0.2.11",
1725
+ "ts-interface-checker": "^0.1.9"
1726
+ },
1727
+ "bin": {
1728
+ "sucrase": "bin/sucrase",
1729
+ "sucrase-node": "bin/sucrase-node"
1730
+ },
1731
+ "engines": {
1732
+ "node": ">=16 || 14 >=14.17"
1733
+ }
1734
+ },
1735
+ "node_modules/supports-preserve-symlinks-flag": {
1736
+ "version": "1.0.0",
1737
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
1738
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
1739
+ "dev": true,
1740
+ "engines": {
1741
+ "node": ">= 0.4"
1742
+ },
1743
+ "funding": {
1744
+ "url": "https://github.com/sponsors/ljharb"
1745
+ }
1746
+ },
1747
+ "node_modules/tailwindcss": {
1748
+ "version": "3.4.19",
1749
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
1750
+ "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
1751
+ "dev": true,
1752
+ "dependencies": {
1753
+ "@alloc/quick-lru": "^5.2.0",
1754
+ "arg": "^5.0.2",
1755
+ "chokidar": "^3.6.0",
1756
+ "didyoumean": "^1.2.2",
1757
+ "dlv": "^1.1.3",
1758
+ "fast-glob": "^3.3.2",
1759
+ "glob-parent": "^6.0.2",
1760
+ "is-glob": "^4.0.3",
1761
+ "jiti": "^1.21.7",
1762
+ "lilconfig": "^3.1.3",
1763
+ "micromatch": "^4.0.8",
1764
+ "normalize-path": "^3.0.0",
1765
+ "object-hash": "^3.0.0",
1766
+ "picocolors": "^1.1.1",
1767
+ "postcss": "^8.4.47",
1768
+ "postcss-import": "^15.1.0",
1769
+ "postcss-js": "^4.0.1",
1770
+ "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0",
1771
+ "postcss-nested": "^6.2.0",
1772
+ "postcss-selector-parser": "^6.1.2",
1773
+ "resolve": "^1.22.8",
1774
+ "sucrase": "^3.35.0"
1775
+ },
1776
+ "bin": {
1777
+ "tailwind": "lib/cli.js",
1778
+ "tailwindcss": "lib/cli.js"
1779
+ },
1780
+ "engines": {
1781
+ "node": ">=14.0.0"
1782
+ }
1783
+ },
1784
+ "node_modules/thenify": {
1785
+ "version": "3.3.1",
1786
+ "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
1787
+ "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
1788
+ "dev": true,
1789
+ "dependencies": {
1790
+ "any-promise": "^1.0.0"
1791
+ }
1792
+ },
1793
+ "node_modules/thenify-all": {
1794
+ "version": "1.6.0",
1795
+ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
1796
+ "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
1797
+ "dev": true,
1798
+ "dependencies": {
1799
+ "thenify": ">= 3.1.0 < 4"
1800
+ },
1801
+ "engines": {
1802
+ "node": ">=0.8"
1803
+ }
1804
+ },
1805
+ "node_modules/tinyglobby": {
1806
+ "version": "0.2.15",
1807
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
1808
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
1809
+ "dev": true,
1810
+ "dependencies": {
1811
+ "fdir": "^6.5.0",
1812
+ "picomatch": "^4.0.3"
1813
+ },
1814
+ "engines": {
1815
+ "node": ">=12.0.0"
1816
+ },
1817
+ "funding": {
1818
+ "url": "https://github.com/sponsors/SuperchupuDev"
1819
+ }
1820
+ },
1821
+ "node_modules/tinyglobby/node_modules/fdir": {
1822
+ "version": "6.5.0",
1823
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
1824
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
1825
+ "dev": true,
1826
+ "engines": {
1827
+ "node": ">=12.0.0"
1828
+ },
1829
+ "peerDependencies": {
1830
+ "picomatch": "^3 || ^4"
1831
+ },
1832
+ "peerDependenciesMeta": {
1833
+ "picomatch": {
1834
+ "optional": true
1835
+ }
1836
+ }
1837
+ },
1838
+ "node_modules/tinyglobby/node_modules/picomatch": {
1839
+ "version": "4.0.3",
1840
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
1841
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
1842
+ "dev": true,
1843
+ "engines": {
1844
+ "node": ">=12"
1845
+ },
1846
+ "funding": {
1847
+ "url": "https://github.com/sponsors/jonschlinkert"
1848
+ }
1849
+ },
1850
+ "node_modules/to-regex-range": {
1851
+ "version": "5.0.1",
1852
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
1853
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
1854
+ "dev": true,
1855
+ "dependencies": {
1856
+ "is-number": "^7.0.0"
1857
+ },
1858
+ "engines": {
1859
+ "node": ">=8.0"
1860
+ }
1861
+ },
1862
+ "node_modules/ts-interface-checker": {
1863
+ "version": "0.1.13",
1864
+ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
1865
+ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
1866
+ "dev": true
1867
+ },
1868
+ "node_modules/tslib": {
1869
+ "version": "2.8.1",
1870
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
1871
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
1872
+ },
1873
+ "node_modules/typescript": {
1874
+ "version": "5.9.3",
1875
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
1876
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
1877
+ "dev": true,
1878
+ "bin": {
1879
+ "tsc": "bin/tsc",
1880
+ "tsserver": "bin/tsserver"
1881
+ },
1882
+ "engines": {
1883
+ "node": ">=14.17"
1884
+ }
1885
+ },
1886
+ "node_modules/undici-types": {
1887
+ "version": "6.21.0",
1888
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
1889
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
1890
+ "dev": true
1891
+ },
1892
+ "node_modules/update-browserslist-db": {
1893
+ "version": "1.2.3",
1894
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
1895
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
1896
+ "dev": true,
1897
+ "funding": [
1898
+ {
1899
+ "type": "opencollective",
1900
+ "url": "https://opencollective.com/browserslist"
1901
+ },
1902
+ {
1903
+ "type": "tidelift",
1904
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
1905
+ },
1906
+ {
1907
+ "type": "github",
1908
+ "url": "https://github.com/sponsors/ai"
1909
+ }
1910
+ ],
1911
+ "dependencies": {
1912
+ "escalade": "^3.2.0",
1913
+ "picocolors": "^1.1.1"
1914
+ },
1915
+ "bin": {
1916
+ "update-browserslist-db": "cli.js"
1917
+ },
1918
+ "peerDependencies": {
1919
+ "browserslist": ">= 4.21.0"
1920
+ }
1921
+ },
1922
+ "node_modules/util-deprecate": {
1923
+ "version": "1.0.2",
1924
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
1925
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
1926
+ "dev": true
1927
+ }
1928
+ }
1929
+ }
ui/package.json CHANGED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "anifind",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "next lint"
10
+ },
11
+ "dependencies": {
12
+ "next": "15.1.0",
13
+ "react": "^19.0.0",
14
+ "react-dom": "^19.0.0"
15
+ },
16
+ "devDependencies": {
17
+ "@types/node": "^20",
18
+ "@types/react": "^19",
19
+ "@types/react-dom": "^19",
20
+ "autoprefixer": "^10.0.1",
21
+ "postcss": "^8",
22
+ "tailwindcss": "^3.4.1",
23
+ "typescript": "^5"
24
+ }
25
+ }
ui/postcss.config.mjs CHANGED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ const config = {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ };
7
+ export default config;
ui/tailwind.config.ts CHANGED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('tailwindcss').Config} */
2
+ const config = {
3
+ content: [
4
+ "./pages/**/*.{js,ts,jsx,tsx,mdx}",
5
+ "./components/**/*.{js,ts,jsx,tsx,mdx}",
6
+ "./app/**/*.{js,ts,jsx,tsx,mdx}",
7
+ ],
8
+ theme: { extend: {} },
9
+ plugins: [],
10
+ };
11
+ export default config;
ui/tsconfig.json CHANGED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "lib": [
4
+ "dom",
5
+ "dom.iterable",
6
+ "esnext"
7
+ ],
8
+ "allowJs": true,
9
+ "skipLibCheck": true,
10
+ "strict": true,
11
+ "noEmit": true,
12
+ "esModuleInterop": true,
13
+ "module": "esnext",
14
+ "moduleResolution": "bundler",
15
+ "resolveJsonModule": true,
16
+ "isolatedModules": true,
17
+ "jsx": "preserve",
18
+ "incremental": true,
19
+ "plugins": [
20
+ {
21
+ "name": "next"
22
+ }
23
+ ],
24
+ "paths": {
25
+ "@/*": [
26
+ "./*"
27
+ ]
28
+ },
29
+ "target": "ES2017"
30
+ },
31
+ "include": [
32
+ "next-env.d.ts",
33
+ "**/*.ts",
34
+ "**/*.tsx",
35
+ ".next/types/**/*.ts"
36
+ ],
37
+ "exclude": [
38
+ "node_modules"
39
+ ]
40
+ }