ewdlop commited on
Commit
42ae0b9
·
1 Parent(s): fde182b
.dockerignore ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ frontend/node_modules
2
+ frontend/.pnpm-store
3
+ api/static/
4
+ **/__pycache__
5
+ **/*.pyc
6
+ **/*.pyo
7
+ .git
8
+ .gitignore
9
+ *.md
.gitignore ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ frontend/node_modules/
2
+ frontend/.pnpm-store/
3
+ api/static/
4
+ __pycache__/
5
+ *.pyc
6
+ *.pyo
7
+ .env
8
+ *.log
9
+ dist/
Dockerfile ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ # Install Node.js 22 for building the frontend
4
+ RUN apt-get update && apt-get install -y curl && \
5
+ curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \
6
+ apt-get install -y nodejs && \
7
+ npm install -g pnpm && \
8
+ apt-get clean && rm -rf /var/lib/apt/lists/*
9
+
10
+ WORKDIR /app
11
+
12
+ # Copy and install Python dependencies first (layer cache)
13
+ COPY requirements.txt .
14
+ RUN pip install --no-cache-dir -r requirements.txt
15
+
16
+ # Copy frontend source and build it
17
+ COPY frontend/ ./frontend/
18
+ RUN cd frontend && pnpm install --frozen-lockfile && pnpm build
19
+
20
+ # Copy the FastAPI app (static/ was built into api/static/ by vite)
21
+ COPY api/ ./api/
22
+
23
+ # HF Spaces runs on port 7860
24
+ EXPOSE 7860
25
+
26
+ # Run the FastAPI server
27
+ WORKDIR /app/api
28
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,11 +1,78 @@
1
  ---
2
- title: Structured Query Language Analyzer
3
- emoji: 📊
4
- colorFrom: gray
5
- colorTo: red
6
  sdk: docker
7
  pinned: false
8
- license: apache-2.0
9
  ---
10
 
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: SQL Analyzer
3
+ emoji: 🔍
4
+ colorFrom: blue
5
+ colorTo: indigo
6
  sdk: docker
7
  pinned: false
8
+ short_description: SQL linter, AST explorer, formatter & injection detector powered by SQLFluff
9
  ---
10
 
11
+ # SQL Analyzer Linter, AST Explorer & Injection Detector
12
+
13
+ An interactive SQL analysis tool powered by **SQLFluff 4.x**, built as a pure Python FastAPI backend serving a React frontend.
14
+
15
+ ## Features
16
+
17
+ - **SQL Linter** — SQLFluff rule violations with severity, rule codes, line/column info, and fixable indicators
18
+ - **AST Tree Explorer** — Interactive collapsible/expandable parse tree with search, filter, and color-coded node types
19
+ - **SQL Injection Detector** — Detects tautologies, stacked queries, UNION exfiltration, comment bypasses, and more
20
+ - **SQL Formatter** — Auto-fix and format SQL with copy-to-clipboard and apply-to-editor
21
+ - **Swagger UI** — Full OpenAPI 3.1 documentation at `/swagger`
22
+ - **17 SQL dialects** — ANSI, PostgreSQL, MySQL, T-SQL, SQLite, BigQuery, Snowflake, Redshift, DuckDB, Hive, Spark SQL, Trino, Databricks, Oracle, Teradata, ClickHouse, Athena
23
+
24
+ ## Architecture
25
+
26
+ ```
27
+ sql-analyzer-standalone/
28
+ ├── api/
29
+ │ ├── main.py ← FastAPI app (REST endpoints + static file serving)
30
+ │ └── static/ ← Built React bundle (generated by pnpm build)
31
+ ├── frontend/
32
+ │ ├── src/ ← React 19 + TypeScript source
33
+ │ ├── package.json
34
+ │ └── vite.config.ts ← Builds into ../api/static/
35
+ ├── requirements.txt
36
+ ├── Dockerfile
37
+ └── README.md
38
+ ```
39
+
40
+ ## Local Development
41
+
42
+ ```bash
43
+ # 1. Install Python deps
44
+ pip install -r requirements.txt
45
+
46
+ # 2. Build the React frontend
47
+ cd frontend && pnpm install && pnpm build && cd ..
48
+
49
+ # 3. Start the FastAPI server
50
+ cd api && uvicorn main:app --reload --port 7860
51
+ ```
52
+
53
+ Open http://localhost:7860
54
+
55
+ ## API Endpoints
56
+
57
+ | Method | Path | Description |
58
+ |--------|------|-------------|
59
+ | GET | `/api/health` | Health check + SQLFluff version |
60
+ | POST | `/api/lint` | Lint SQL with SQLFluff |
61
+ | POST | `/api/parse` | Parse SQL into AST |
62
+ | POST | `/api/format` | Format/fix SQL |
63
+ | POST | `/api/inject` | Detect SQL injection patterns |
64
+ | GET | `/openapi.json` | OpenAPI schema |
65
+ | GET | `/docs` | Swagger UI (FastAPI built-in) |
66
+ | GET | `/swagger` | Custom Swagger UI page |
67
+
68
+ ## Request Format
69
+
70
+ All POST endpoints accept:
71
+ ```json
72
+ {
73
+ "sql": "SELECT * FROM users WHERE id = 1",
74
+ "dialect": "ansi"
75
+ }
76
+ ```
77
+
78
+ Supported dialects: `ansi`, `postgres`, `mysql`, `tsql`, `sqlite`, `bigquery`, `snowflake`, `redshift`, `duckdb`, `hive`, `sparksql`, `trino`, `databricks`, `oracle`, `teradata`, `clickhouse`, `athena`
api/main.py ADDED
@@ -0,0 +1,544 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ SQL Analyzer — Pure FastAPI Backend
3
+ ====================================
4
+ Serves:
5
+ • REST API → /api/health, /api/lint, /api/parse, /api/format, /api/inject
6
+ • Swagger UI → /docs (auto-generated by FastAPI)
7
+ • React SPA → everything else (static files from ./static/)
8
+
9
+ Single-process deployment — no Node.js required.
10
+ Compatible with Hugging Face Spaces (Docker SDK) and any OCI-compatible host.
11
+ """
12
+
13
+ import re
14
+ import json
15
+ import traceback
16
+ import os
17
+ from pathlib import Path
18
+ from typing import Any, Optional
19
+
20
+ from fastapi import FastAPI, HTTPException, Request
21
+ from fastapi.middleware.cors import CORSMiddleware
22
+ from fastapi.responses import FileResponse, JSONResponse
23
+ from fastapi.staticfiles import StaticFiles
24
+ from pydantic import BaseModel
25
+ import sqlfluff
26
+ from sqlfluff.core import Linter
27
+ from sqlfluff.core.parser import BaseSegment
28
+
29
+ # ---------------------------------------------------------------------------
30
+ # App setup
31
+ # ---------------------------------------------------------------------------
32
+
33
+ app = FastAPI(
34
+ title="SQL Analyzer API",
35
+ description=(
36
+ "A powerful SQL analysis backend providing linting, AST parsing, "
37
+ "SQL formatting, and injection detection powered by SQLFluff."
38
+ ),
39
+ version="1.0.0",
40
+ docs_url="/docs",
41
+ redoc_url="/redoc",
42
+ openapi_url="/openapi.json",
43
+ )
44
+
45
+ app.add_middleware(
46
+ CORSMiddleware,
47
+ allow_origins=["*"],
48
+ allow_credentials=True,
49
+ allow_methods=["*"],
50
+ allow_headers=["*"],
51
+ )
52
+
53
+ # ---------------------------------------------------------------------------
54
+ # Request / Response models
55
+ # ---------------------------------------------------------------------------
56
+
57
+ class SqlRequest(BaseModel):
58
+ sql: str
59
+ dialect: str = "ansi"
60
+
61
+ class LintViolation(BaseModel):
62
+ line_no: int
63
+ line_pos: int
64
+ code: str
65
+ description: str
66
+ name: str
67
+ warning: bool
68
+ fixable: bool
69
+
70
+ class LintResponse(BaseModel):
71
+ dialect: str
72
+ violations: list[LintViolation]
73
+ passed: bool
74
+ stats: dict[str, Any]
75
+
76
+ class AstNode(BaseModel):
77
+ id: str
78
+ type: str
79
+ name: str
80
+ raw: Optional[str]
81
+ start_line: Optional[int]
82
+ start_pos: Optional[int]
83
+ end_line: Optional[int]
84
+ end_pos: Optional[int]
85
+ is_leaf: bool
86
+ children: list["AstNode"]
87
+
88
+ AstNode.model_rebuild()
89
+
90
+ class ParseResponse(BaseModel):
91
+ dialect: str
92
+ tree: AstNode
93
+ token_count: int
94
+ depth: int
95
+
96
+ class FormatResponse(BaseModel):
97
+ dialect: str
98
+ original: str
99
+ formatted: str
100
+ changed: bool
101
+ fixes_applied: int
102
+
103
+ class InjectionPattern(BaseModel):
104
+ pattern_id: str
105
+ risk_level: str # critical | high | medium | low
106
+ category: str
107
+ description: str
108
+ detail: str
109
+ offending_token: Optional[str]
110
+ line_no: Optional[int]
111
+ line_pos: Optional[int]
112
+ recommendation: str
113
+
114
+ class InjectionResponse(BaseModel):
115
+ dialect: str
116
+ safe: bool
117
+ risk_score: int # 0-100
118
+ patterns: list[InjectionPattern]
119
+ summary: str
120
+
121
+ class HealthResponse(BaseModel):
122
+ status: str
123
+ version: str
124
+ dialects: list[str]
125
+
126
+ # ---------------------------------------------------------------------------
127
+ # Helpers
128
+ # ---------------------------------------------------------------------------
129
+
130
+ SQLFLUFF_DIALECTS = [
131
+ "ansi", "athena", "bigquery", "clickhouse", "databricks", "db2",
132
+ "duckdb", "exasol", "greenplum", "hive", "mysql", "oracle",
133
+ "postgres", "redshift", "snowflake", "soql", "sparksql", "sqlite",
134
+ "teradata", "trino", "tsql",
135
+ ]
136
+
137
+ _node_counter = 0
138
+
139
+ def _reset_counter():
140
+ global _node_counter
141
+ _node_counter = 0
142
+
143
+ def _next_id() -> str:
144
+ global _node_counter
145
+ _node_counter += 1
146
+ return f"node_{_node_counter}"
147
+
148
+ def _segment_to_node(seg: BaseSegment, depth: int = 0) -> AstNode:
149
+ """Recursively convert a SQLFluff segment into a serialisable AstNode."""
150
+ is_raw_attr = getattr(seg, "is_raw", None)
151
+ if callable(is_raw_attr):
152
+ is_leaf: bool = is_raw_attr()
153
+ elif is_raw_attr is not None:
154
+ is_leaf = bool(is_raw_attr)
155
+ else:
156
+ is_leaf = not bool(seg.segments)
157
+
158
+ raw = seg.raw if is_leaf else None
159
+
160
+ start_line = start_pos = end_line = end_pos = None
161
+ try:
162
+ if hasattr(seg, "pos_marker") and seg.pos_marker:
163
+ pm = seg.pos_marker
164
+ start_line = pm.line_no
165
+ start_pos = pm.line_pos
166
+ except Exception:
167
+ pass
168
+
169
+ children = []
170
+ if not is_leaf and seg.segments:
171
+ for child in seg.segments:
172
+ children.append(_segment_to_node(child, depth + 1))
173
+
174
+ return AstNode(
175
+ id=_next_id(),
176
+ type=seg.get_type(),
177
+ name=type(seg).__name__,
178
+ raw=raw,
179
+ start_line=start_line,
180
+ start_pos=start_pos,
181
+ end_line=end_line,
182
+ end_pos=end_pos,
183
+ is_leaf=bool(is_leaf),
184
+ children=children,
185
+ )
186
+
187
+ def _tree_depth(node: AstNode) -> int:
188
+ if not node.children:
189
+ return 0
190
+ return 1 + max(_tree_depth(c) for c in node.children)
191
+
192
+ def _count_tokens(node: AstNode) -> int:
193
+ if node.is_leaf:
194
+ return 1
195
+ return sum(_count_tokens(c) for c in node.children)
196
+
197
+ # ---------------------------------------------------------------------------
198
+ # Injection detection patterns
199
+ # ---------------------------------------------------------------------------
200
+
201
+ INJECTION_PATTERNS = [
202
+ {
203
+ "id": "tautology_or_1_1",
204
+ "risk": "critical",
205
+ "category": "Tautology",
206
+ "description": "Always-true tautology detected (e.g. OR 1=1)",
207
+ "detail": "Tautologies force WHERE clauses to always evaluate to TRUE, bypassing all filters.",
208
+ "recommendation": "Use parameterised queries. Never interpolate user input into SQL strings.",
209
+ "regex": r"\bOR\s+['\"]?\d+['\"]?\s*=\s*['\"]?\d+['\"]?",
210
+ "flags": re.IGNORECASE,
211
+ },
212
+ {
213
+ "id": "tautology_or_true",
214
+ "risk": "critical",
215
+ "category": "Tautology",
216
+ "description": "Always-true boolean tautology (e.g. OR TRUE / OR 'a'='a')",
217
+ "detail": "Boolean tautologies bypass WHERE conditions entirely.",
218
+ "recommendation": "Use parameterised queries and validate all user-supplied data.",
219
+ "regex": r"\bOR\s+(?:TRUE|'[^']*'\s*=\s*'[^']*')",
220
+ "flags": re.IGNORECASE,
221
+ },
222
+ {
223
+ "id": "stacked_queries",
224
+ "risk": "critical",
225
+ "category": "Stacked Queries",
226
+ "description": "Stacked / batched query detected (semicolon followed by another statement)",
227
+ "detail": "Stacked queries allow attackers to append arbitrary SQL statements.",
228
+ "recommendation": "Disallow multiple statements in a single query. Use stored procedures or ORMs.",
229
+ "regex": r";\s*(?:SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|EXEC|EXECUTE|UNION)\b",
230
+ "flags": re.IGNORECASE,
231
+ },
232
+ {
233
+ "id": "comment_bypass_inline",
234
+ "risk": "high",
235
+ "category": "Comment Bypass",
236
+ "description": "Inline comment used to truncate or bypass query logic (--)",
237
+ "detail": "Inline comments (--) can strip the remainder of a query, bypassing authentication checks.",
238
+ "recommendation": "Strip or reject SQL comment sequences from user input.",
239
+ "regex": r"--[^\n]*",
240
+ "flags": 0,
241
+ },
242
+ {
243
+ "id": "comment_bypass_block",
244
+ "risk": "high",
245
+ "category": "Comment Bypass",
246
+ "description": "Block comment used to obfuscate or bypass query logic (/* ... */)",
247
+ "detail": "Block comments can hide injected code and bypass naive input filters.",
248
+ "recommendation": "Strip or reject SQL block comment sequences from user input.",
249
+ "regex": r"/\*.*?\*/",
250
+ "flags": re.DOTALL,
251
+ },
252
+ {
253
+ "id": "union_select",
254
+ "risk": "high",
255
+ "category": "UNION-based Injection",
256
+ "description": "UNION SELECT detected — potential data exfiltration vector",
257
+ "detail": "UNION SELECT allows attackers to append result sets from other tables, leaking sensitive data.",
258
+ "recommendation": "Use parameterised queries. Validate column counts and types.",
259
+ "regex": r"\bUNION\s+(?:ALL\s+)?SELECT\b",
260
+ "flags": re.IGNORECASE,
261
+ },
262
+ {
263
+ "id": "sleep_benchmark",
264
+ "risk": "high",
265
+ "category": "Time-based Blind Injection",
266
+ "description": "Time-delay function detected (SLEEP / BENCHMARK / WAITFOR)",
267
+ "detail": "Time-based blind injection uses delays to infer data without visible output.",
268
+ "recommendation": "Parameterise all queries and restrict execution of time-delay functions.",
269
+ "regex": r"\b(?:SLEEP|BENCHMARK|WAITFOR\s+DELAY|PG_SLEEP)\s*\(",
270
+ "flags": re.IGNORECASE,
271
+ },
272
+ {
273
+ "id": "exec_xp_cmdshell",
274
+ "risk": "critical",
275
+ "category": "Command Execution",
276
+ "description": "xp_cmdshell or EXEC detected — OS command execution risk",
277
+ "detail": "xp_cmdshell allows execution of arbitrary OS commands from SQL Server.",
278
+ "recommendation": "Disable xp_cmdshell. Never allow user input to reach EXEC statements.",
279
+ "regex": r"\b(?:xp_cmdshell|EXEC(?:UTE)?)\s*\(",
280
+ "flags": re.IGNORECASE,
281
+ },
282
+ {
283
+ "id": "drop_table",
284
+ "risk": "critical",
285
+ "category": "Destructive Statement",
286
+ "description": "DROP TABLE / DROP DATABASE detected",
287
+ "detail": "Injected DROP statements can destroy entire tables or databases.",
288
+ "recommendation": "Restrict DDL permissions. Use parameterised queries and least-privilege accounts.",
289
+ "regex": r"\bDROP\s+(?:TABLE|DATABASE|SCHEMA|INDEX)\b",
290
+ "flags": re.IGNORECASE,
291
+ },
292
+ {
293
+ "id": "hex_encoding",
294
+ "risk": "medium",
295
+ "category": "Obfuscation",
296
+ "description": "Hex-encoded string literal detected (0x...)",
297
+ "detail": "Hex encoding is commonly used to bypass string-based input filters.",
298
+ "recommendation": "Validate and sanitise all input. Use parameterised queries.",
299
+ "regex": r"\b0x[0-9a-fA-F]{4,}\b",
300
+ "flags": 0,
301
+ },
302
+ {
303
+ "id": "char_concat",
304
+ "risk": "medium",
305
+ "category": "Obfuscation",
306
+ "description": "CHAR() concatenation detected — common obfuscation technique",
307
+ "detail": "Attackers use CHAR() to build strings character-by-character to evade filters.",
308
+ "recommendation": "Use parameterised queries. Restrict use of CHAR() in user-facing contexts.",
309
+ "regex": r"\bCHAR\s*\(\s*\d+",
310
+ "flags": re.IGNORECASE,
311
+ },
312
+ {
313
+ "id": "information_schema",
314
+ "risk": "medium",
315
+ "category": "Schema Reconnaissance",
316
+ "description": "INFORMATION_SCHEMA query detected — schema enumeration attempt",
317
+ "detail": "Attackers query INFORMATION_SCHEMA to enumerate tables, columns, and credentials.",
318
+ "recommendation": "Restrict access to INFORMATION_SCHEMA. Use least-privilege DB accounts.",
319
+ "regex": r"\bINFORMATION_SCHEMA\b",
320
+ "flags": re.IGNORECASE,
321
+ },
322
+ {
323
+ "id": "load_file",
324
+ "risk": "critical",
325
+ "category": "File System Access",
326
+ "description": "LOAD_FILE() or INTO OUTFILE detected — file system access risk",
327
+ "detail": "These MySQL functions allow reading/writing arbitrary files on the server.",
328
+ "recommendation": "Disable FILE privilege. Never allow user input near file I/O functions.",
329
+ "regex": r"\b(?:LOAD_FILE|INTO\s+(?:OUT|DUMP)FILE)\b",
330
+ "flags": re.IGNORECASE,
331
+ },
332
+ ]
333
+
334
+ RISK_SCORE = {"critical": 35, "high": 20, "medium": 10, "low": 5}
335
+
336
+ def _detect_injection(sql: str) -> list[InjectionPattern]:
337
+ results: list[InjectionPattern] = []
338
+ for pat in INJECTION_PATTERNS:
339
+ for m in re.finditer(pat["regex"], sql, pat["flags"]):
340
+ line_no = sql[: m.start()].count("\n") + 1
341
+ line_pos = m.start() - sql[: m.start()].rfind("\n")
342
+ results.append(
343
+ InjectionPattern(
344
+ pattern_id=pat["id"],
345
+ risk_level=pat["risk"],
346
+ category=pat["category"],
347
+ description=pat["description"],
348
+ detail=pat["detail"],
349
+ offending_token=m.group(0)[:120],
350
+ line_no=line_no,
351
+ line_pos=line_pos,
352
+ recommendation=pat["recommendation"],
353
+ )
354
+ )
355
+ return results
356
+
357
+ # ---------------------------------------------------------------------------
358
+ # API Routes (all under /api/ prefix)
359
+ # ---------------------------------------------------------------------------
360
+
361
+ @app.get("/api/health", response_model=HealthResponse, tags=["System"])
362
+ def health():
363
+ """Return API health status and SQLFluff version."""
364
+ return HealthResponse(
365
+ status="ok",
366
+ version=sqlfluff.__version__,
367
+ dialects=SQLFLUFF_DIALECTS,
368
+ )
369
+
370
+ @app.post("/api/lint", response_model=LintResponse, tags=["Analysis"])
371
+ def lint_sql(req: SqlRequest):
372
+ """
373
+ Lint SQL using SQLFluff and return rule violations.
374
+
375
+ Returns a list of violations with line/column info, rule codes,
376
+ severity, and whether each violation is auto-fixable.
377
+ """
378
+ try:
379
+ dialect = req.dialect if req.dialect in SQLFLUFF_DIALECTS else "ansi"
380
+ linter = Linter(dialect=dialect)
381
+ result = linter.lint_string(req.sql)
382
+ violations = []
383
+ for v in result.violations:
384
+ violations.append(LintViolation(
385
+ line_no=v.line_no,
386
+ line_pos=v.line_pos,
387
+ code=v.rule_code(),
388
+ description=v.desc(),
389
+ name=v.rule_code(),
390
+ warning=getattr(v, "warning", False),
391
+ fixable=getattr(v, "fixable", False),
392
+ ))
393
+ stats = {
394
+ "total": len(violations),
395
+ "errors": sum(1 for v in violations if not v.warning),
396
+ "warnings": sum(1 for v in violations if v.warning),
397
+ "fixable": sum(1 for v in violations if v.fixable),
398
+ }
399
+ return LintResponse(
400
+ dialect=dialect,
401
+ violations=violations,
402
+ passed=len(violations) == 0,
403
+ stats=stats,
404
+ )
405
+ except Exception as e:
406
+ raise HTTPException(status_code=500, detail=f"Lint error: {str(e)}\n{traceback.format_exc()}")
407
+
408
+ @app.post("/api/parse", response_model=ParseResponse, tags=["Analysis"])
409
+ def parse_sql(req: SqlRequest):
410
+ """
411
+ Parse SQL into a full Abstract Syntax Tree (AST).
412
+
413
+ Returns a recursive tree of nodes with type, name, raw token value,
414
+ position info, and child nodes.
415
+ """
416
+ try:
417
+ dialect = req.dialect if req.dialect in SQLFLUFF_DIALECTS else "ansi"
418
+ linter = Linter(dialect=dialect)
419
+ parsed = linter.parse_string(req.sql)
420
+ _reset_counter()
421
+ tree = _segment_to_node(parsed.tree)
422
+ return ParseResponse(
423
+ dialect=dialect,
424
+ tree=tree,
425
+ token_count=_count_tokens(tree),
426
+ depth=_tree_depth(tree),
427
+ )
428
+ except Exception as e:
429
+ raise HTTPException(status_code=500, detail=f"Parse error: {str(e)}\n{traceback.format_exc()}")
430
+
431
+ @app.post("/api/format", response_model=FormatResponse, tags=["Analysis"])
432
+ def format_sql(req: SqlRequest):
433
+ """
434
+ Format and auto-fix SQL using SQLFluff.
435
+
436
+ Applies all auto-fixable rules and returns the cleaned SQL alongside
437
+ the original, with a count of fixes applied.
438
+ """
439
+ try:
440
+ dialect = req.dialect if req.dialect in SQLFLUFF_DIALECTS else "ansi"
441
+ # Exclude CV10 (quoted literals convention) which crashes with
442
+ # "templated_file property is required" when fix() is called without
443
+ # a full templated context (known SQLFluff 4.x bug).
444
+ linter = Linter(dialect=dialect, exclude_rules=["CV10"])
445
+ lint_before = linter.lint_string(req.sql)
446
+ before_count = len(lint_before.violations)
447
+ parsed = linter.parse_string(req.sql)
448
+ fixed_tree, _ = linter.fix(parsed.tree)
449
+ formatted = fixed_tree.raw.strip() if fixed_tree else req.sql
450
+ lint_after = linter.lint_string(formatted)
451
+ after_count = len(lint_after.violations)
452
+ fixes_applied = max(0, before_count - after_count)
453
+ return FormatResponse(
454
+ dialect=dialect,
455
+ original=req.sql,
456
+ formatted=formatted,
457
+ changed=formatted != req.sql,
458
+ fixes_applied=fixes_applied,
459
+ )
460
+ except Exception as e:
461
+ raise HTTPException(status_code=500, detail=f"Format error: {str(e)}\n{traceback.format_exc()}")
462
+
463
+ @app.post("/api/inject", response_model=InjectionResponse, tags=["Security"])
464
+ def detect_injection(req: SqlRequest):
465
+ """
466
+ Detect SQL injection patterns in the provided SQL string.
467
+
468
+ Checks for tautologies, stacked queries, comment-based bypasses,
469
+ UNION-based injection, time-based blind injection, command execution,
470
+ destructive statements, hex obfuscation, and schema reconnaissance.
471
+ """
472
+ try:
473
+ patterns = _detect_injection(req.sql)
474
+ seen: set[str] = set()
475
+ unique_patterns: list[InjectionPattern] = []
476
+ for p in patterns:
477
+ if p.pattern_id not in seen:
478
+ seen.add(p.pattern_id)
479
+ unique_patterns.append(p)
480
+
481
+ score = min(100, sum(RISK_SCORE.get(p.risk_level, 0) for p in unique_patterns))
482
+
483
+ if score == 0:
484
+ summary = "No injection patterns detected. The SQL appears safe."
485
+ elif score < 25:
486
+ summary = f"Low risk ({score}/100): Minor obfuscation or reconnaissance patterns found."
487
+ elif score < 50:
488
+ summary = f"Medium risk ({score}/100): Suspicious patterns detected. Review carefully."
489
+ elif score < 75:
490
+ summary = f"High risk ({score}/100): Multiple injection indicators found. Do not execute."
491
+ else:
492
+ summary = f"Critical risk ({score}/100): Severe injection patterns detected. This SQL is dangerous."
493
+
494
+ return InjectionResponse(
495
+ dialect=req.dialect,
496
+ safe=score == 0,
497
+ risk_score=score,
498
+ patterns=unique_patterns,
499
+ summary=summary,
500
+ )
501
+ except Exception as e:
502
+ raise HTTPException(status_code=500, detail=f"Injection detection error: {str(e)}")
503
+
504
+ # ---------------------------------------------------------------------------
505
+ # Static file serving — React SPA
506
+ # ---------------------------------------------------------------------------
507
+ # The React build output is placed in ./static/ (relative to this file).
508
+ # FastAPI serves it at the root, with a catch-all that returns index.html
509
+ # for any unknown path (client-side routing).
510
+
511
+ STATIC_DIR = Path(__file__).parent / "static"
512
+
513
+ if STATIC_DIR.exists():
514
+ # Mount assets (JS/CSS/fonts) under /assets so they don't clash with /api
515
+ assets_dir = STATIC_DIR / "assets"
516
+ if assets_dir.exists():
517
+ app.mount("/assets", StaticFiles(directory=str(assets_dir)), name="assets")
518
+
519
+ @app.get("/{full_path:path}", include_in_schema=False)
520
+ async def serve_spa(full_path: str, request: Request):
521
+ """Serve the React SPA for all non-API routes."""
522
+ # Try exact file match first (favicon.ico, robots.txt, etc.)
523
+ candidate = STATIC_DIR / full_path
524
+ if candidate.exists() and candidate.is_file():
525
+ return FileResponse(str(candidate))
526
+ # Fall back to index.html for client-side routing
527
+ return FileResponse(str(STATIC_DIR / "index.html"))
528
+ else:
529
+ @app.get("/", include_in_schema=False)
530
+ async def no_static():
531
+ return JSONResponse({
532
+ "message": "SQL Analyzer API is running. Build the React frontend and place it in api/static/.",
533
+ "docs": "/docs",
534
+ "endpoints": ["/api/health", "/api/lint", "/api/parse", "/api/format", "/api/inject"],
535
+ })
536
+
537
+ # ---------------------------------------------------------------------------
538
+ # Entry point
539
+ # ---------------------------------------------------------------------------
540
+
541
+ if __name__ == "__main__":
542
+ import uvicorn
543
+ port = int(os.environ.get("PORT", os.environ.get("PYTHON_API_PORT", "7860")))
544
+ uvicorn.run(app, host="0.0.0.0", port=port, log_level="info")
build.sh ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # build.sh — Build the React frontend and start the FastAPI server
3
+ # Usage: ./build.sh [--port 7860]
4
+
5
+ set -e
6
+
7
+ PORT=${2:-7860}
8
+
9
+ echo "=== SQL Analyzer Build Script ==="
10
+ echo ""
11
+
12
+ # 1. Install Python deps
13
+ echo "[1/3] Installing Python dependencies..."
14
+ pip install -r requirements.txt --quiet
15
+
16
+ # 2. Build the React frontend
17
+ echo "[2/3] Building React frontend..."
18
+ cd frontend
19
+ pnpm install --frozen-lockfile --silent
20
+ pnpm build --silent
21
+ cd ..
22
+
23
+ echo " → Built to api/static/"
24
+
25
+ # 3. Start the server
26
+ echo "[3/3] Starting FastAPI server on port $PORT..."
27
+ echo " → Open http://localhost:$PORT"
28
+ echo ""
29
+ cd api
30
+ exec uvicorn main:app --host 0.0.0.0 --port "$PORT"
frontend/index.html ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1" />
6
+ <title>SQL Analyzer — Linter, AST Explorer &amp; Injection Detector</title>
7
+ <meta name="description" content="Interactive SQL linter, AST explorer, formatter, and injection detector powered by SQLFluff." />
8
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
10
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
11
+ </head>
12
+ <body>
13
+ <div id="root"></div>
14
+ <script type="module" src="/src/main.tsx"></script>
15
+ </body>
16
+ </html>
frontend/package.json ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "sql-analyzer-frontend",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "vite",
7
+ "build": "tsc --noEmit && vite build",
8
+ "preview": "vite preview"
9
+ },
10
+ "dependencies": {
11
+ "@codemirror/autocomplete": "^6.20.1",
12
+ "@codemirror/commands": "^6.10.3",
13
+ "@codemirror/lang-sql": "^6.8.0",
14
+ "@codemirror/language": "^6.12.3",
15
+ "@codemirror/lint": "^6.9.5",
16
+ "@codemirror/state": "^6.5.2",
17
+ "@codemirror/theme-one-dark": "^6.1.2",
18
+ "@codemirror/view": "^6.36.3",
19
+ "@radix-ui/react-scroll-area": "^1.2.10",
20
+ "@radix-ui/react-select": "^2.2.6",
21
+ "@radix-ui/react-separator": "^1.1.7",
22
+ "@radix-ui/react-tabs": "^1.1.13",
23
+ "@radix-ui/react-tooltip": "^1.2.8",
24
+ "class-variance-authority": "^0.7.1",
25
+ "clsx": "^2.1.1",
26
+ "codemirror": "^6.0.1",
27
+ "framer-motion": "^12.0.0",
28
+ "lucide-react": "^0.453.0",
29
+ "react": "^19.0.0",
30
+ "react-dom": "^19.0.0",
31
+ "react-resizable-panels": "^3.0.6",
32
+ "sonner": "^2.0.7",
33
+ "swagger-ui-dist": "^5.18.2",
34
+ "tailwind-merge": "^3.0.0",
35
+ "wouter": "^3.3.5"
36
+ },
37
+ "devDependencies": {
38
+ "@tailwindcss/vite": "^4.1.3",
39
+ "@types/react": "^19.0.0",
40
+ "@types/react-dom": "^19.0.0",
41
+ "@vitejs/plugin-react": "^4.3.4",
42
+ "tailwindcss": "^4.1.3",
43
+ "typescript": "^5.7.3",
44
+ "vite": "^6.3.5"
45
+ }
46
+ }
frontend/pnpm-lock.yaml ADDED
@@ -0,0 +1,2631 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ lockfileVersion: '9.0'
2
+
3
+ settings:
4
+ autoInstallPeers: true
5
+ excludeLinksFromLockfile: false
6
+
7
+ importers:
8
+
9
+ .:
10
+ dependencies:
11
+ '@codemirror/autocomplete':
12
+ specifier: ^6.20.1
13
+ version: 6.20.1
14
+ '@codemirror/commands':
15
+ specifier: ^6.10.3
16
+ version: 6.10.3
17
+ '@codemirror/lang-sql':
18
+ specifier: ^6.8.0
19
+ version: 6.10.0
20
+ '@codemirror/language':
21
+ specifier: ^6.12.3
22
+ version: 6.12.3
23
+ '@codemirror/lint':
24
+ specifier: ^6.9.5
25
+ version: 6.9.5
26
+ '@codemirror/state':
27
+ specifier: ^6.5.2
28
+ version: 6.6.0
29
+ '@codemirror/theme-one-dark':
30
+ specifier: ^6.1.2
31
+ version: 6.1.3
32
+ '@codemirror/view':
33
+ specifier: ^6.36.3
34
+ version: 6.41.1
35
+ '@radix-ui/react-scroll-area':
36
+ specifier: ^1.2.10
37
+ version: 1.2.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
38
+ '@radix-ui/react-select':
39
+ specifier: ^2.2.6
40
+ version: 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
41
+ '@radix-ui/react-separator':
42
+ specifier: ^1.1.7
43
+ version: 1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
44
+ '@radix-ui/react-tabs':
45
+ specifier: ^1.1.13
46
+ version: 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
47
+ '@radix-ui/react-tooltip':
48
+ specifier: ^1.2.8
49
+ version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
50
+ class-variance-authority:
51
+ specifier: ^0.7.1
52
+ version: 0.7.1
53
+ clsx:
54
+ specifier: ^2.1.1
55
+ version: 2.1.1
56
+ codemirror:
57
+ specifier: ^6.0.1
58
+ version: 6.0.2
59
+ framer-motion:
60
+ specifier: ^12.0.0
61
+ version: 12.38.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
62
+ lucide-react:
63
+ specifier: ^0.453.0
64
+ version: 0.453.0(react@19.2.5)
65
+ react:
66
+ specifier: ^19.0.0
67
+ version: 19.2.5
68
+ react-dom:
69
+ specifier: ^19.0.0
70
+ version: 19.2.5(react@19.2.5)
71
+ react-resizable-panels:
72
+ specifier: ^3.0.6
73
+ version: 3.0.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
74
+ sonner:
75
+ specifier: ^2.0.7
76
+ version: 2.0.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
77
+ swagger-ui-dist:
78
+ specifier: ^5.18.2
79
+ version: 5.32.5
80
+ tailwind-merge:
81
+ specifier: ^3.0.0
82
+ version: 3.5.0
83
+ wouter:
84
+ specifier: ^3.3.5
85
+ version: 3.9.0(react@19.2.5)
86
+ devDependencies:
87
+ '@tailwindcss/vite':
88
+ specifier: ^4.1.3
89
+ version: 4.2.4(vite@6.4.2(jiti@2.6.1)(lightningcss@1.32.0))
90
+ '@types/react':
91
+ specifier: ^19.0.0
92
+ version: 19.2.14
93
+ '@types/react-dom':
94
+ specifier: ^19.0.0
95
+ version: 19.2.3(@types/react@19.2.14)
96
+ '@vitejs/plugin-react':
97
+ specifier: ^4.3.4
98
+ version: 4.7.0(vite@6.4.2(jiti@2.6.1)(lightningcss@1.32.0))
99
+ tailwindcss:
100
+ specifier: ^4.1.3
101
+ version: 4.2.4
102
+ typescript:
103
+ specifier: ^5.7.3
104
+ version: 5.9.3
105
+ vite:
106
+ specifier: ^6.3.5
107
+ version: 6.4.2(jiti@2.6.1)(lightningcss@1.32.0)
108
+
109
+ packages:
110
+
111
+ '@babel/code-frame@7.29.0':
112
+ resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
113
+ engines: {node: '>=6.9.0'}
114
+
115
+ '@babel/compat-data@7.29.0':
116
+ resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==}
117
+ engines: {node: '>=6.9.0'}
118
+
119
+ '@babel/core@7.29.0':
120
+ resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==}
121
+ engines: {node: '>=6.9.0'}
122
+
123
+ '@babel/generator@7.29.1':
124
+ resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==}
125
+ engines: {node: '>=6.9.0'}
126
+
127
+ '@babel/helper-compilation-targets@7.28.6':
128
+ resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==}
129
+ engines: {node: '>=6.9.0'}
130
+
131
+ '@babel/helper-globals@7.28.0':
132
+ resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==}
133
+ engines: {node: '>=6.9.0'}
134
+
135
+ '@babel/helper-module-imports@7.28.6':
136
+ resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==}
137
+ engines: {node: '>=6.9.0'}
138
+
139
+ '@babel/helper-module-transforms@7.28.6':
140
+ resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==}
141
+ engines: {node: '>=6.9.0'}
142
+ peerDependencies:
143
+ '@babel/core': ^7.0.0
144
+
145
+ '@babel/helper-plugin-utils@7.28.6':
146
+ resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==}
147
+ engines: {node: '>=6.9.0'}
148
+
149
+ '@babel/helper-string-parser@7.27.1':
150
+ resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
151
+ engines: {node: '>=6.9.0'}
152
+
153
+ '@babel/helper-validator-identifier@7.28.5':
154
+ resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
155
+ engines: {node: '>=6.9.0'}
156
+
157
+ '@babel/helper-validator-option@7.27.1':
158
+ resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
159
+ engines: {node: '>=6.9.0'}
160
+
161
+ '@babel/helpers@7.29.2':
162
+ resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==}
163
+ engines: {node: '>=6.9.0'}
164
+
165
+ '@babel/parser@7.29.2':
166
+ resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==}
167
+ engines: {node: '>=6.0.0'}
168
+ hasBin: true
169
+
170
+ '@babel/plugin-transform-react-jsx-self@7.27.1':
171
+ resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==}
172
+ engines: {node: '>=6.9.0'}
173
+ peerDependencies:
174
+ '@babel/core': ^7.0.0-0
175
+
176
+ '@babel/plugin-transform-react-jsx-source@7.27.1':
177
+ resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==}
178
+ engines: {node: '>=6.9.0'}
179
+ peerDependencies:
180
+ '@babel/core': ^7.0.0-0
181
+
182
+ '@babel/template@7.28.6':
183
+ resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==}
184
+ engines: {node: '>=6.9.0'}
185
+
186
+ '@babel/traverse@7.29.0':
187
+ resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==}
188
+ engines: {node: '>=6.9.0'}
189
+
190
+ '@babel/types@7.29.0':
191
+ resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
192
+ engines: {node: '>=6.9.0'}
193
+
194
+ '@codemirror/autocomplete@6.20.1':
195
+ resolution: {integrity: sha512-1cvg3Vz1dSSToCNlJfRA2WSI4ht3K+WplO0UMOgmUYPivCyy2oueZY6Lx7M9wThm7SDUBViRmuT+OG/i8+ON9A==}
196
+
197
+ '@codemirror/commands@6.10.3':
198
+ resolution: {integrity: sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==}
199
+
200
+ '@codemirror/lang-sql@6.10.0':
201
+ resolution: {integrity: sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w==}
202
+
203
+ '@codemirror/language@6.12.3':
204
+ resolution: {integrity: sha512-QwCZW6Tt1siP37Jet9Tb02Zs81TQt6qQrZR2H+eGMcFsL1zMrk2/b9CLC7/9ieP1fjIUMgviLWMmgiHoJrj+ZA==}
205
+
206
+ '@codemirror/lint@6.9.5':
207
+ resolution: {integrity: sha512-GElsbU9G7QT9xXhpUg1zWGmftA/7jamh+7+ydKRuT0ORpWS3wOSP0yT1FOlIZa7mIJjpVPipErsyvVqB9cfTFA==}
208
+
209
+ '@codemirror/search@6.7.0':
210
+ resolution: {integrity: sha512-ZvGm99wc/s2cITtMT15LFdn8aH/aS+V+DqyGq/N5ZlV5vWtH+nILvC2nw0zX7ByNoHHDZ2IxxdW38O0tc5nVHg==}
211
+
212
+ '@codemirror/state@6.6.0':
213
+ resolution: {integrity: sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==}
214
+
215
+ '@codemirror/theme-one-dark@6.1.3':
216
+ resolution: {integrity: sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==}
217
+
218
+ '@codemirror/view@6.41.1':
219
+ resolution: {integrity: sha512-ToDnWKbBnke+ZLrP6vgTTDScGi5H37YYuZGniQaBzxMVdtCxMrslsmtnOvbPZk4RX9bvkQqnWR/WS/35tJA0qg==}
220
+
221
+ '@esbuild/aix-ppc64@0.25.12':
222
+ resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==}
223
+ engines: {node: '>=18'}
224
+ cpu: [ppc64]
225
+ os: [aix]
226
+
227
+ '@esbuild/android-arm64@0.25.12':
228
+ resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==}
229
+ engines: {node: '>=18'}
230
+ cpu: [arm64]
231
+ os: [android]
232
+
233
+ '@esbuild/android-arm@0.25.12':
234
+ resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==}
235
+ engines: {node: '>=18'}
236
+ cpu: [arm]
237
+ os: [android]
238
+
239
+ '@esbuild/android-x64@0.25.12':
240
+ resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==}
241
+ engines: {node: '>=18'}
242
+ cpu: [x64]
243
+ os: [android]
244
+
245
+ '@esbuild/darwin-arm64@0.25.12':
246
+ resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==}
247
+ engines: {node: '>=18'}
248
+ cpu: [arm64]
249
+ os: [darwin]
250
+
251
+ '@esbuild/darwin-x64@0.25.12':
252
+ resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==}
253
+ engines: {node: '>=18'}
254
+ cpu: [x64]
255
+ os: [darwin]
256
+
257
+ '@esbuild/freebsd-arm64@0.25.12':
258
+ resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==}
259
+ engines: {node: '>=18'}
260
+ cpu: [arm64]
261
+ os: [freebsd]
262
+
263
+ '@esbuild/freebsd-x64@0.25.12':
264
+ resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==}
265
+ engines: {node: '>=18'}
266
+ cpu: [x64]
267
+ os: [freebsd]
268
+
269
+ '@esbuild/linux-arm64@0.25.12':
270
+ resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==}
271
+ engines: {node: '>=18'}
272
+ cpu: [arm64]
273
+ os: [linux]
274
+
275
+ '@esbuild/linux-arm@0.25.12':
276
+ resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==}
277
+ engines: {node: '>=18'}
278
+ cpu: [arm]
279
+ os: [linux]
280
+
281
+ '@esbuild/linux-ia32@0.25.12':
282
+ resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==}
283
+ engines: {node: '>=18'}
284
+ cpu: [ia32]
285
+ os: [linux]
286
+
287
+ '@esbuild/linux-loong64@0.25.12':
288
+ resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==}
289
+ engines: {node: '>=18'}
290
+ cpu: [loong64]
291
+ os: [linux]
292
+
293
+ '@esbuild/linux-mips64el@0.25.12':
294
+ resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==}
295
+ engines: {node: '>=18'}
296
+ cpu: [mips64el]
297
+ os: [linux]
298
+
299
+ '@esbuild/linux-ppc64@0.25.12':
300
+ resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==}
301
+ engines: {node: '>=18'}
302
+ cpu: [ppc64]
303
+ os: [linux]
304
+
305
+ '@esbuild/linux-riscv64@0.25.12':
306
+ resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==}
307
+ engines: {node: '>=18'}
308
+ cpu: [riscv64]
309
+ os: [linux]
310
+
311
+ '@esbuild/linux-s390x@0.25.12':
312
+ resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==}
313
+ engines: {node: '>=18'}
314
+ cpu: [s390x]
315
+ os: [linux]
316
+
317
+ '@esbuild/linux-x64@0.25.12':
318
+ resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==}
319
+ engines: {node: '>=18'}
320
+ cpu: [x64]
321
+ os: [linux]
322
+
323
+ '@esbuild/netbsd-arm64@0.25.12':
324
+ resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==}
325
+ engines: {node: '>=18'}
326
+ cpu: [arm64]
327
+ os: [netbsd]
328
+
329
+ '@esbuild/netbsd-x64@0.25.12':
330
+ resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==}
331
+ engines: {node: '>=18'}
332
+ cpu: [x64]
333
+ os: [netbsd]
334
+
335
+ '@esbuild/openbsd-arm64@0.25.12':
336
+ resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==}
337
+ engines: {node: '>=18'}
338
+ cpu: [arm64]
339
+ os: [openbsd]
340
+
341
+ '@esbuild/openbsd-x64@0.25.12':
342
+ resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==}
343
+ engines: {node: '>=18'}
344
+ cpu: [x64]
345
+ os: [openbsd]
346
+
347
+ '@esbuild/openharmony-arm64@0.25.12':
348
+ resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==}
349
+ engines: {node: '>=18'}
350
+ cpu: [arm64]
351
+ os: [openharmony]
352
+
353
+ '@esbuild/sunos-x64@0.25.12':
354
+ resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==}
355
+ engines: {node: '>=18'}
356
+ cpu: [x64]
357
+ os: [sunos]
358
+
359
+ '@esbuild/win32-arm64@0.25.12':
360
+ resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==}
361
+ engines: {node: '>=18'}
362
+ cpu: [arm64]
363
+ os: [win32]
364
+
365
+ '@esbuild/win32-ia32@0.25.12':
366
+ resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==}
367
+ engines: {node: '>=18'}
368
+ cpu: [ia32]
369
+ os: [win32]
370
+
371
+ '@esbuild/win32-x64@0.25.12':
372
+ resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==}
373
+ engines: {node: '>=18'}
374
+ cpu: [x64]
375
+ os: [win32]
376
+
377
+ '@floating-ui/core@1.7.5':
378
+ resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==}
379
+
380
+ '@floating-ui/dom@1.7.6':
381
+ resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==}
382
+
383
+ '@floating-ui/react-dom@2.1.8':
384
+ resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==}
385
+ peerDependencies:
386
+ react: '>=16.8.0'
387
+ react-dom: '>=16.8.0'
388
+
389
+ '@floating-ui/utils@0.2.11':
390
+ resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==}
391
+
392
+ '@jridgewell/gen-mapping@0.3.13':
393
+ resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
394
+
395
+ '@jridgewell/remapping@2.3.5':
396
+ resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
397
+
398
+ '@jridgewell/resolve-uri@3.1.2':
399
+ resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
400
+ engines: {node: '>=6.0.0'}
401
+
402
+ '@jridgewell/sourcemap-codec@1.5.5':
403
+ resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
404
+
405
+ '@jridgewell/trace-mapping@0.3.31':
406
+ resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
407
+
408
+ '@lezer/common@1.5.2':
409
+ resolution: {integrity: sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==}
410
+
411
+ '@lezer/highlight@1.2.3':
412
+ resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==}
413
+
414
+ '@lezer/lr@1.4.10':
415
+ resolution: {integrity: sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==}
416
+
417
+ '@marijn/find-cluster-break@1.0.2':
418
+ resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==}
419
+
420
+ '@radix-ui/number@1.1.1':
421
+ resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==}
422
+
423
+ '@radix-ui/primitive@1.1.3':
424
+ resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
425
+
426
+ '@radix-ui/react-arrow@1.1.7':
427
+ resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==}
428
+ peerDependencies:
429
+ '@types/react': '*'
430
+ '@types/react-dom': '*'
431
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
432
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
433
+ peerDependenciesMeta:
434
+ '@types/react':
435
+ optional: true
436
+ '@types/react-dom':
437
+ optional: true
438
+
439
+ '@radix-ui/react-collection@1.1.7':
440
+ resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==}
441
+ peerDependencies:
442
+ '@types/react': '*'
443
+ '@types/react-dom': '*'
444
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
445
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
446
+ peerDependenciesMeta:
447
+ '@types/react':
448
+ optional: true
449
+ '@types/react-dom':
450
+ optional: true
451
+
452
+ '@radix-ui/react-compose-refs@1.1.2':
453
+ resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==}
454
+ peerDependencies:
455
+ '@types/react': '*'
456
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
457
+ peerDependenciesMeta:
458
+ '@types/react':
459
+ optional: true
460
+
461
+ '@radix-ui/react-context@1.1.2':
462
+ resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==}
463
+ peerDependencies:
464
+ '@types/react': '*'
465
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
466
+ peerDependenciesMeta:
467
+ '@types/react':
468
+ optional: true
469
+
470
+ '@radix-ui/react-direction@1.1.1':
471
+ resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==}
472
+ peerDependencies:
473
+ '@types/react': '*'
474
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
475
+ peerDependenciesMeta:
476
+ '@types/react':
477
+ optional: true
478
+
479
+ '@radix-ui/react-dismissable-layer@1.1.11':
480
+ resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==}
481
+ peerDependencies:
482
+ '@types/react': '*'
483
+ '@types/react-dom': '*'
484
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
485
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
486
+ peerDependenciesMeta:
487
+ '@types/react':
488
+ optional: true
489
+ '@types/react-dom':
490
+ optional: true
491
+
492
+ '@radix-ui/react-focus-guards@1.1.3':
493
+ resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==}
494
+ peerDependencies:
495
+ '@types/react': '*'
496
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
497
+ peerDependenciesMeta:
498
+ '@types/react':
499
+ optional: true
500
+
501
+ '@radix-ui/react-focus-scope@1.1.7':
502
+ resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==}
503
+ peerDependencies:
504
+ '@types/react': '*'
505
+ '@types/react-dom': '*'
506
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
507
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
508
+ peerDependenciesMeta:
509
+ '@types/react':
510
+ optional: true
511
+ '@types/react-dom':
512
+ optional: true
513
+
514
+ '@radix-ui/react-id@1.1.1':
515
+ resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==}
516
+ peerDependencies:
517
+ '@types/react': '*'
518
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
519
+ peerDependenciesMeta:
520
+ '@types/react':
521
+ optional: true
522
+
523
+ '@radix-ui/react-popper@1.2.8':
524
+ resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==}
525
+ peerDependencies:
526
+ '@types/react': '*'
527
+ '@types/react-dom': '*'
528
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
529
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
530
+ peerDependenciesMeta:
531
+ '@types/react':
532
+ optional: true
533
+ '@types/react-dom':
534
+ optional: true
535
+
536
+ '@radix-ui/react-portal@1.1.9':
537
+ resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==}
538
+ peerDependencies:
539
+ '@types/react': '*'
540
+ '@types/react-dom': '*'
541
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
542
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
543
+ peerDependenciesMeta:
544
+ '@types/react':
545
+ optional: true
546
+ '@types/react-dom':
547
+ optional: true
548
+
549
+ '@radix-ui/react-presence@1.1.5':
550
+ resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==}
551
+ peerDependencies:
552
+ '@types/react': '*'
553
+ '@types/react-dom': '*'
554
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
555
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
556
+ peerDependenciesMeta:
557
+ '@types/react':
558
+ optional: true
559
+ '@types/react-dom':
560
+ optional: true
561
+
562
+ '@radix-ui/react-primitive@2.1.3':
563
+ resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==}
564
+ peerDependencies:
565
+ '@types/react': '*'
566
+ '@types/react-dom': '*'
567
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
568
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
569
+ peerDependenciesMeta:
570
+ '@types/react':
571
+ optional: true
572
+ '@types/react-dom':
573
+ optional: true
574
+
575
+ '@radix-ui/react-primitive@2.1.4':
576
+ resolution: {integrity: sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==}
577
+ peerDependencies:
578
+ '@types/react': '*'
579
+ '@types/react-dom': '*'
580
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
581
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
582
+ peerDependenciesMeta:
583
+ '@types/react':
584
+ optional: true
585
+ '@types/react-dom':
586
+ optional: true
587
+
588
+ '@radix-ui/react-roving-focus@1.1.11':
589
+ resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==}
590
+ peerDependencies:
591
+ '@types/react': '*'
592
+ '@types/react-dom': '*'
593
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
594
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
595
+ peerDependenciesMeta:
596
+ '@types/react':
597
+ optional: true
598
+ '@types/react-dom':
599
+ optional: true
600
+
601
+ '@radix-ui/react-scroll-area@1.2.10':
602
+ resolution: {integrity: sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==}
603
+ peerDependencies:
604
+ '@types/react': '*'
605
+ '@types/react-dom': '*'
606
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
607
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
608
+ peerDependenciesMeta:
609
+ '@types/react':
610
+ optional: true
611
+ '@types/react-dom':
612
+ optional: true
613
+
614
+ '@radix-ui/react-select@2.2.6':
615
+ resolution: {integrity: sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==}
616
+ peerDependencies:
617
+ '@types/react': '*'
618
+ '@types/react-dom': '*'
619
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
620
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
621
+ peerDependenciesMeta:
622
+ '@types/react':
623
+ optional: true
624
+ '@types/react-dom':
625
+ optional: true
626
+
627
+ '@radix-ui/react-separator@1.1.8':
628
+ resolution: {integrity: sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==}
629
+ peerDependencies:
630
+ '@types/react': '*'
631
+ '@types/react-dom': '*'
632
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
633
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
634
+ peerDependenciesMeta:
635
+ '@types/react':
636
+ optional: true
637
+ '@types/react-dom':
638
+ optional: true
639
+
640
+ '@radix-ui/react-slot@1.2.3':
641
+ resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==}
642
+ peerDependencies:
643
+ '@types/react': '*'
644
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
645
+ peerDependenciesMeta:
646
+ '@types/react':
647
+ optional: true
648
+
649
+ '@radix-ui/react-slot@1.2.4':
650
+ resolution: {integrity: sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==}
651
+ peerDependencies:
652
+ '@types/react': '*'
653
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
654
+ peerDependenciesMeta:
655
+ '@types/react':
656
+ optional: true
657
+
658
+ '@radix-ui/react-tabs@1.1.13':
659
+ resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==}
660
+ peerDependencies:
661
+ '@types/react': '*'
662
+ '@types/react-dom': '*'
663
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
664
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
665
+ peerDependenciesMeta:
666
+ '@types/react':
667
+ optional: true
668
+ '@types/react-dom':
669
+ optional: true
670
+
671
+ '@radix-ui/react-tooltip@1.2.8':
672
+ resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==}
673
+ peerDependencies:
674
+ '@types/react': '*'
675
+ '@types/react-dom': '*'
676
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
677
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
678
+ peerDependenciesMeta:
679
+ '@types/react':
680
+ optional: true
681
+ '@types/react-dom':
682
+ optional: true
683
+
684
+ '@radix-ui/react-use-callback-ref@1.1.1':
685
+ resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==}
686
+ peerDependencies:
687
+ '@types/react': '*'
688
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
689
+ peerDependenciesMeta:
690
+ '@types/react':
691
+ optional: true
692
+
693
+ '@radix-ui/react-use-controllable-state@1.2.2':
694
+ resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==}
695
+ peerDependencies:
696
+ '@types/react': '*'
697
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
698
+ peerDependenciesMeta:
699
+ '@types/react':
700
+ optional: true
701
+
702
+ '@radix-ui/react-use-effect-event@0.0.2':
703
+ resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==}
704
+ peerDependencies:
705
+ '@types/react': '*'
706
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
707
+ peerDependenciesMeta:
708
+ '@types/react':
709
+ optional: true
710
+
711
+ '@radix-ui/react-use-escape-keydown@1.1.1':
712
+ resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==}
713
+ peerDependencies:
714
+ '@types/react': '*'
715
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
716
+ peerDependenciesMeta:
717
+ '@types/react':
718
+ optional: true
719
+
720
+ '@radix-ui/react-use-layout-effect@1.1.1':
721
+ resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==}
722
+ peerDependencies:
723
+ '@types/react': '*'
724
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
725
+ peerDependenciesMeta:
726
+ '@types/react':
727
+ optional: true
728
+
729
+ '@radix-ui/react-use-previous@1.1.1':
730
+ resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==}
731
+ peerDependencies:
732
+ '@types/react': '*'
733
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
734
+ peerDependenciesMeta:
735
+ '@types/react':
736
+ optional: true
737
+
738
+ '@radix-ui/react-use-rect@1.1.1':
739
+ resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==}
740
+ peerDependencies:
741
+ '@types/react': '*'
742
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
743
+ peerDependenciesMeta:
744
+ '@types/react':
745
+ optional: true
746
+
747
+ '@radix-ui/react-use-size@1.1.1':
748
+ resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==}
749
+ peerDependencies:
750
+ '@types/react': '*'
751
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
752
+ peerDependenciesMeta:
753
+ '@types/react':
754
+ optional: true
755
+
756
+ '@radix-ui/react-visually-hidden@1.2.3':
757
+ resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==}
758
+ peerDependencies:
759
+ '@types/react': '*'
760
+ '@types/react-dom': '*'
761
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
762
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
763
+ peerDependenciesMeta:
764
+ '@types/react':
765
+ optional: true
766
+ '@types/react-dom':
767
+ optional: true
768
+
769
+ '@radix-ui/rect@1.1.1':
770
+ resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
771
+
772
+ '@rolldown/pluginutils@1.0.0-beta.27':
773
+ resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==}
774
+
775
+ '@rollup/rollup-android-arm-eabi@4.60.2':
776
+ resolution: {integrity: sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==}
777
+ cpu: [arm]
778
+ os: [android]
779
+
780
+ '@rollup/rollup-android-arm64@4.60.2':
781
+ resolution: {integrity: sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==}
782
+ cpu: [arm64]
783
+ os: [android]
784
+
785
+ '@rollup/rollup-darwin-arm64@4.60.2':
786
+ resolution: {integrity: sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==}
787
+ cpu: [arm64]
788
+ os: [darwin]
789
+
790
+ '@rollup/rollup-darwin-x64@4.60.2':
791
+ resolution: {integrity: sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==}
792
+ cpu: [x64]
793
+ os: [darwin]
794
+
795
+ '@rollup/rollup-freebsd-arm64@4.60.2':
796
+ resolution: {integrity: sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==}
797
+ cpu: [arm64]
798
+ os: [freebsd]
799
+
800
+ '@rollup/rollup-freebsd-x64@4.60.2':
801
+ resolution: {integrity: sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==}
802
+ cpu: [x64]
803
+ os: [freebsd]
804
+
805
+ '@rollup/rollup-linux-arm-gnueabihf@4.60.2':
806
+ resolution: {integrity: sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==}
807
+ cpu: [arm]
808
+ os: [linux]
809
+ libc: [glibc]
810
+
811
+ '@rollup/rollup-linux-arm-musleabihf@4.60.2':
812
+ resolution: {integrity: sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==}
813
+ cpu: [arm]
814
+ os: [linux]
815
+ libc: [musl]
816
+
817
+ '@rollup/rollup-linux-arm64-gnu@4.60.2':
818
+ resolution: {integrity: sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==}
819
+ cpu: [arm64]
820
+ os: [linux]
821
+ libc: [glibc]
822
+
823
+ '@rollup/rollup-linux-arm64-musl@4.60.2':
824
+ resolution: {integrity: sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==}
825
+ cpu: [arm64]
826
+ os: [linux]
827
+ libc: [musl]
828
+
829
+ '@rollup/rollup-linux-loong64-gnu@4.60.2':
830
+ resolution: {integrity: sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==}
831
+ cpu: [loong64]
832
+ os: [linux]
833
+ libc: [glibc]
834
+
835
+ '@rollup/rollup-linux-loong64-musl@4.60.2':
836
+ resolution: {integrity: sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==}
837
+ cpu: [loong64]
838
+ os: [linux]
839
+ libc: [musl]
840
+
841
+ '@rollup/rollup-linux-ppc64-gnu@4.60.2':
842
+ resolution: {integrity: sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==}
843
+ cpu: [ppc64]
844
+ os: [linux]
845
+ libc: [glibc]
846
+
847
+ '@rollup/rollup-linux-ppc64-musl@4.60.2':
848
+ resolution: {integrity: sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==}
849
+ cpu: [ppc64]
850
+ os: [linux]
851
+ libc: [musl]
852
+
853
+ '@rollup/rollup-linux-riscv64-gnu@4.60.2':
854
+ resolution: {integrity: sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==}
855
+ cpu: [riscv64]
856
+ os: [linux]
857
+ libc: [glibc]
858
+
859
+ '@rollup/rollup-linux-riscv64-musl@4.60.2':
860
+ resolution: {integrity: sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==}
861
+ cpu: [riscv64]
862
+ os: [linux]
863
+ libc: [musl]
864
+
865
+ '@rollup/rollup-linux-s390x-gnu@4.60.2':
866
+ resolution: {integrity: sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==}
867
+ cpu: [s390x]
868
+ os: [linux]
869
+ libc: [glibc]
870
+
871
+ '@rollup/rollup-linux-x64-gnu@4.60.2':
872
+ resolution: {integrity: sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==}
873
+ cpu: [x64]
874
+ os: [linux]
875
+ libc: [glibc]
876
+
877
+ '@rollup/rollup-linux-x64-musl@4.60.2':
878
+ resolution: {integrity: sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==}
879
+ cpu: [x64]
880
+ os: [linux]
881
+ libc: [musl]
882
+
883
+ '@rollup/rollup-openbsd-x64@4.60.2':
884
+ resolution: {integrity: sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==}
885
+ cpu: [x64]
886
+ os: [openbsd]
887
+
888
+ '@rollup/rollup-openharmony-arm64@4.60.2':
889
+ resolution: {integrity: sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==}
890
+ cpu: [arm64]
891
+ os: [openharmony]
892
+
893
+ '@rollup/rollup-win32-arm64-msvc@4.60.2':
894
+ resolution: {integrity: sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==}
895
+ cpu: [arm64]
896
+ os: [win32]
897
+
898
+ '@rollup/rollup-win32-ia32-msvc@4.60.2':
899
+ resolution: {integrity: sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==}
900
+ cpu: [ia32]
901
+ os: [win32]
902
+
903
+ '@rollup/rollup-win32-x64-gnu@4.60.2':
904
+ resolution: {integrity: sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==}
905
+ cpu: [x64]
906
+ os: [win32]
907
+
908
+ '@rollup/rollup-win32-x64-msvc@4.60.2':
909
+ resolution: {integrity: sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==}
910
+ cpu: [x64]
911
+ os: [win32]
912
+
913
+ '@scarf/scarf@1.4.0':
914
+ resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==}
915
+
916
+ '@tailwindcss/node@4.2.4':
917
+ resolution: {integrity: sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA==}
918
+
919
+ '@tailwindcss/oxide-android-arm64@4.2.4':
920
+ resolution: {integrity: sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g==}
921
+ engines: {node: '>= 20'}
922
+ cpu: [arm64]
923
+ os: [android]
924
+
925
+ '@tailwindcss/oxide-darwin-arm64@4.2.4':
926
+ resolution: {integrity: sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg==}
927
+ engines: {node: '>= 20'}
928
+ cpu: [arm64]
929
+ os: [darwin]
930
+
931
+ '@tailwindcss/oxide-darwin-x64@4.2.4':
932
+ resolution: {integrity: sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg==}
933
+ engines: {node: '>= 20'}
934
+ cpu: [x64]
935
+ os: [darwin]
936
+
937
+ '@tailwindcss/oxide-freebsd-x64@4.2.4':
938
+ resolution: {integrity: sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw==}
939
+ engines: {node: '>= 20'}
940
+ cpu: [x64]
941
+ os: [freebsd]
942
+
943
+ '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.4':
944
+ resolution: {integrity: sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA==}
945
+ engines: {node: '>= 20'}
946
+ cpu: [arm]
947
+ os: [linux]
948
+
949
+ '@tailwindcss/oxide-linux-arm64-gnu@4.2.4':
950
+ resolution: {integrity: sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw==}
951
+ engines: {node: '>= 20'}
952
+ cpu: [arm64]
953
+ os: [linux]
954
+ libc: [glibc]
955
+
956
+ '@tailwindcss/oxide-linux-arm64-musl@4.2.4':
957
+ resolution: {integrity: sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g==}
958
+ engines: {node: '>= 20'}
959
+ cpu: [arm64]
960
+ os: [linux]
961
+ libc: [musl]
962
+
963
+ '@tailwindcss/oxide-linux-x64-gnu@4.2.4':
964
+ resolution: {integrity: sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA==}
965
+ engines: {node: '>= 20'}
966
+ cpu: [x64]
967
+ os: [linux]
968
+ libc: [glibc]
969
+
970
+ '@tailwindcss/oxide-linux-x64-musl@4.2.4':
971
+ resolution: {integrity: sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA==}
972
+ engines: {node: '>= 20'}
973
+ cpu: [x64]
974
+ os: [linux]
975
+ libc: [musl]
976
+
977
+ '@tailwindcss/oxide-wasm32-wasi@4.2.4':
978
+ resolution: {integrity: sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw==}
979
+ engines: {node: '>=14.0.0'}
980
+ cpu: [wasm32]
981
+ bundledDependencies:
982
+ - '@napi-rs/wasm-runtime'
983
+ - '@emnapi/core'
984
+ - '@emnapi/runtime'
985
+ - '@tybys/wasm-util'
986
+ - '@emnapi/wasi-threads'
987
+ - tslib
988
+
989
+ '@tailwindcss/oxide-win32-arm64-msvc@4.2.4':
990
+ resolution: {integrity: sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ==}
991
+ engines: {node: '>= 20'}
992
+ cpu: [arm64]
993
+ os: [win32]
994
+
995
+ '@tailwindcss/oxide-win32-x64-msvc@4.2.4':
996
+ resolution: {integrity: sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw==}
997
+ engines: {node: '>= 20'}
998
+ cpu: [x64]
999
+ os: [win32]
1000
+
1001
+ '@tailwindcss/oxide@4.2.4':
1002
+ resolution: {integrity: sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q==}
1003
+ engines: {node: '>= 20'}
1004
+
1005
+ '@tailwindcss/vite@4.2.4':
1006
+ resolution: {integrity: sha512-pCvohwOCspk3ZFn6eJzrrX3g4n2JY73H6MmYC87XfGPyTty4YsCjYTMArRZm/zOI8dIt3+EcrLHAFPe5A4bgtw==}
1007
+ peerDependencies:
1008
+ vite: ^5.2.0 || ^6 || ^7 || ^8
1009
+
1010
+ '@types/babel__core@7.20.5':
1011
+ resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
1012
+
1013
+ '@types/babel__generator@7.27.0':
1014
+ resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==}
1015
+
1016
+ '@types/babel__template@7.4.4':
1017
+ resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==}
1018
+
1019
+ '@types/babel__traverse@7.28.0':
1020
+ resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
1021
+
1022
+ '@types/estree@1.0.8':
1023
+ resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
1024
+
1025
+ '@types/react-dom@19.2.3':
1026
+ resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
1027
+ peerDependencies:
1028
+ '@types/react': ^19.2.0
1029
+
1030
+ '@types/react@19.2.14':
1031
+ resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==}
1032
+
1033
+ '@vitejs/plugin-react@4.7.0':
1034
+ resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==}
1035
+ engines: {node: ^14.18.0 || >=16.0.0}
1036
+ peerDependencies:
1037
+ vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
1038
+
1039
+ aria-hidden@1.2.6:
1040
+ resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
1041
+ engines: {node: '>=10'}
1042
+
1043
+ baseline-browser-mapping@2.10.23:
1044
+ resolution: {integrity: sha512-xwVXGqevyKPsiuQdLj+dZMVjidjJV508TBqexND5HrF89cGdCYCJFB3qhcxRHSeMctdCfbR1jrxBajhDy7o29g==}
1045
+ engines: {node: '>=6.0.0'}
1046
+ hasBin: true
1047
+
1048
+ browserslist@4.28.2:
1049
+ resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==}
1050
+ engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
1051
+ hasBin: true
1052
+
1053
+ caniuse-lite@1.0.30001791:
1054
+ resolution: {integrity: sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==}
1055
+
1056
+ class-variance-authority@0.7.1:
1057
+ resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
1058
+
1059
+ clsx@2.1.1:
1060
+ resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
1061
+ engines: {node: '>=6'}
1062
+
1063
+ codemirror@6.0.2:
1064
+ resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==}
1065
+
1066
+ convert-source-map@2.0.0:
1067
+ resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
1068
+
1069
+ crelt@1.0.6:
1070
+ resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
1071
+
1072
+ csstype@3.2.3:
1073
+ resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
1074
+
1075
+ debug@4.4.3:
1076
+ resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
1077
+ engines: {node: '>=6.0'}
1078
+ peerDependencies:
1079
+ supports-color: '*'
1080
+ peerDependenciesMeta:
1081
+ supports-color:
1082
+ optional: true
1083
+
1084
+ detect-libc@2.1.2:
1085
+ resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
1086
+ engines: {node: '>=8'}
1087
+
1088
+ detect-node-es@1.1.0:
1089
+ resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
1090
+
1091
+ electron-to-chromium@1.5.344:
1092
+ resolution: {integrity: sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==}
1093
+
1094
+ enhanced-resolve@5.21.0:
1095
+ resolution: {integrity: sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==}
1096
+ engines: {node: '>=10.13.0'}
1097
+
1098
+ esbuild@0.25.12:
1099
+ resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==}
1100
+ engines: {node: '>=18'}
1101
+ hasBin: true
1102
+
1103
+ escalade@3.2.0:
1104
+ resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
1105
+ engines: {node: '>=6'}
1106
+
1107
+ fdir@6.5.0:
1108
+ resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
1109
+ engines: {node: '>=12.0.0'}
1110
+ peerDependencies:
1111
+ picomatch: ^3 || ^4
1112
+ peerDependenciesMeta:
1113
+ picomatch:
1114
+ optional: true
1115
+
1116
+ framer-motion@12.38.0:
1117
+ resolution: {integrity: sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==}
1118
+ peerDependencies:
1119
+ '@emotion/is-prop-valid': '*'
1120
+ react: ^18.0.0 || ^19.0.0
1121
+ react-dom: ^18.0.0 || ^19.0.0
1122
+ peerDependenciesMeta:
1123
+ '@emotion/is-prop-valid':
1124
+ optional: true
1125
+ react:
1126
+ optional: true
1127
+ react-dom:
1128
+ optional: true
1129
+
1130
+ fsevents@2.3.3:
1131
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
1132
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
1133
+ os: [darwin]
1134
+
1135
+ gensync@1.0.0-beta.2:
1136
+ resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
1137
+ engines: {node: '>=6.9.0'}
1138
+
1139
+ get-nonce@1.0.1:
1140
+ resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
1141
+ engines: {node: '>=6'}
1142
+
1143
+ graceful-fs@4.2.11:
1144
+ resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
1145
+
1146
+ jiti@2.6.1:
1147
+ resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
1148
+ hasBin: true
1149
+
1150
+ js-tokens@4.0.0:
1151
+ resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
1152
+
1153
+ jsesc@3.1.0:
1154
+ resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
1155
+ engines: {node: '>=6'}
1156
+ hasBin: true
1157
+
1158
+ json5@2.2.3:
1159
+ resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
1160
+ engines: {node: '>=6'}
1161
+ hasBin: true
1162
+
1163
+ lightningcss-android-arm64@1.32.0:
1164
+ resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
1165
+ engines: {node: '>= 12.0.0'}
1166
+ cpu: [arm64]
1167
+ os: [android]
1168
+
1169
+ lightningcss-darwin-arm64@1.32.0:
1170
+ resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==}
1171
+ engines: {node: '>= 12.0.0'}
1172
+ cpu: [arm64]
1173
+ os: [darwin]
1174
+
1175
+ lightningcss-darwin-x64@1.32.0:
1176
+ resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==}
1177
+ engines: {node: '>= 12.0.0'}
1178
+ cpu: [x64]
1179
+ os: [darwin]
1180
+
1181
+ lightningcss-freebsd-x64@1.32.0:
1182
+ resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==}
1183
+ engines: {node: '>= 12.0.0'}
1184
+ cpu: [x64]
1185
+ os: [freebsd]
1186
+
1187
+ lightningcss-linux-arm-gnueabihf@1.32.0:
1188
+ resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==}
1189
+ engines: {node: '>= 12.0.0'}
1190
+ cpu: [arm]
1191
+ os: [linux]
1192
+
1193
+ lightningcss-linux-arm64-gnu@1.32.0:
1194
+ resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==}
1195
+ engines: {node: '>= 12.0.0'}
1196
+ cpu: [arm64]
1197
+ os: [linux]
1198
+ libc: [glibc]
1199
+
1200
+ lightningcss-linux-arm64-musl@1.32.0:
1201
+ resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
1202
+ engines: {node: '>= 12.0.0'}
1203
+ cpu: [arm64]
1204
+ os: [linux]
1205
+ libc: [musl]
1206
+
1207
+ lightningcss-linux-x64-gnu@1.32.0:
1208
+ resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
1209
+ engines: {node: '>= 12.0.0'}
1210
+ cpu: [x64]
1211
+ os: [linux]
1212
+ libc: [glibc]
1213
+
1214
+ lightningcss-linux-x64-musl@1.32.0:
1215
+ resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
1216
+ engines: {node: '>= 12.0.0'}
1217
+ cpu: [x64]
1218
+ os: [linux]
1219
+ libc: [musl]
1220
+
1221
+ lightningcss-win32-arm64-msvc@1.32.0:
1222
+ resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
1223
+ engines: {node: '>= 12.0.0'}
1224
+ cpu: [arm64]
1225
+ os: [win32]
1226
+
1227
+ lightningcss-win32-x64-msvc@1.32.0:
1228
+ resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==}
1229
+ engines: {node: '>= 12.0.0'}
1230
+ cpu: [x64]
1231
+ os: [win32]
1232
+
1233
+ lightningcss@1.32.0:
1234
+ resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
1235
+ engines: {node: '>= 12.0.0'}
1236
+
1237
+ lru-cache@5.1.1:
1238
+ resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
1239
+
1240
+ lucide-react@0.453.0:
1241
+ resolution: {integrity: sha512-kL+RGZCcJi9BvJtzg2kshO192Ddy9hv3ij+cPrVPWSRzgCWCVazoQJxOjAwgK53NomL07HB7GPHW120FimjNhQ==}
1242
+ peerDependencies:
1243
+ react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc
1244
+
1245
+ magic-string@0.30.21:
1246
+ resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
1247
+
1248
+ mitt@3.0.1:
1249
+ resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
1250
+
1251
+ motion-dom@12.38.0:
1252
+ resolution: {integrity: sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==}
1253
+
1254
+ motion-utils@12.36.0:
1255
+ resolution: {integrity: sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==}
1256
+
1257
+ ms@2.1.3:
1258
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
1259
+
1260
+ nanoid@3.3.11:
1261
+ resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
1262
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
1263
+ hasBin: true
1264
+
1265
+ node-releases@2.0.38:
1266
+ resolution: {integrity: sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==}
1267
+
1268
+ picocolors@1.1.1:
1269
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
1270
+
1271
+ picomatch@4.0.4:
1272
+ resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
1273
+ engines: {node: '>=12'}
1274
+
1275
+ postcss@8.5.12:
1276
+ resolution: {integrity: sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==}
1277
+ engines: {node: ^10 || ^12 || >=14}
1278
+
1279
+ react-dom@19.2.5:
1280
+ resolution: {integrity: sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==}
1281
+ peerDependencies:
1282
+ react: ^19.2.5
1283
+
1284
+ react-refresh@0.17.0:
1285
+ resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
1286
+ engines: {node: '>=0.10.0'}
1287
+
1288
+ react-remove-scroll-bar@2.3.8:
1289
+ resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==}
1290
+ engines: {node: '>=10'}
1291
+ peerDependencies:
1292
+ '@types/react': '*'
1293
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
1294
+ peerDependenciesMeta:
1295
+ '@types/react':
1296
+ optional: true
1297
+
1298
+ react-remove-scroll@2.7.2:
1299
+ resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==}
1300
+ engines: {node: '>=10'}
1301
+ peerDependencies:
1302
+ '@types/react': '*'
1303
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
1304
+ peerDependenciesMeta:
1305
+ '@types/react':
1306
+ optional: true
1307
+
1308
+ react-resizable-panels@3.0.6:
1309
+ resolution: {integrity: sha512-b3qKHQ3MLqOgSS+FRYKapNkJZf5EQzuf6+RLiq1/IlTHw99YrZ2NJZLk4hQIzTnnIkRg2LUqyVinu6YWWpUYew==}
1310
+ peerDependencies:
1311
+ react: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
1312
+ react-dom: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
1313
+
1314
+ react-style-singleton@2.2.3:
1315
+ resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==}
1316
+ engines: {node: '>=10'}
1317
+ peerDependencies:
1318
+ '@types/react': '*'
1319
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
1320
+ peerDependenciesMeta:
1321
+ '@types/react':
1322
+ optional: true
1323
+
1324
+ react@19.2.5:
1325
+ resolution: {integrity: sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==}
1326
+ engines: {node: '>=0.10.0'}
1327
+
1328
+ regexparam@3.0.0:
1329
+ resolution: {integrity: sha512-RSYAtP31mvYLkAHrOlh25pCNQ5hWnT106VukGaaFfuJrZFkGRX5GhUAdPqpSDXxOhA2c4akmRuplv1mRqnBn6Q==}
1330
+ engines: {node: '>=8'}
1331
+
1332
+ rollup@4.60.2:
1333
+ resolution: {integrity: sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==}
1334
+ engines: {node: '>=18.0.0', npm: '>=8.0.0'}
1335
+ hasBin: true
1336
+
1337
+ scheduler@0.27.0:
1338
+ resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
1339
+
1340
+ semver@6.3.1:
1341
+ resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
1342
+ hasBin: true
1343
+
1344
+ sonner@2.0.7:
1345
+ resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==}
1346
+ peerDependencies:
1347
+ react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
1348
+ react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
1349
+
1350
+ source-map-js@1.2.1:
1351
+ resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
1352
+ engines: {node: '>=0.10.0'}
1353
+
1354
+ style-mod@4.1.3:
1355
+ resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==}
1356
+
1357
+ swagger-ui-dist@5.32.5:
1358
+ resolution: {integrity: sha512-7/FQfWe9A4qoyYFdAwy0chD0uDYidDp/ZT9VQ9LZlgD4AnnHJk8/+ytAA1HkJYOPySmK6helPDdJQMlcumt7HA==}
1359
+
1360
+ tailwind-merge@3.5.0:
1361
+ resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==}
1362
+
1363
+ tailwindcss@4.2.4:
1364
+ resolution: {integrity: sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA==}
1365
+
1366
+ tapable@2.3.3:
1367
+ resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==}
1368
+ engines: {node: '>=6'}
1369
+
1370
+ tinyglobby@0.2.16:
1371
+ resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==}
1372
+ engines: {node: '>=12.0.0'}
1373
+
1374
+ tslib@2.8.1:
1375
+ resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
1376
+
1377
+ typescript@5.9.3:
1378
+ resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
1379
+ engines: {node: '>=14.17'}
1380
+ hasBin: true
1381
+
1382
+ update-browserslist-db@1.2.3:
1383
+ resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
1384
+ hasBin: true
1385
+ peerDependencies:
1386
+ browserslist: '>= 4.21.0'
1387
+
1388
+ use-callback-ref@1.3.3:
1389
+ resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==}
1390
+ engines: {node: '>=10'}
1391
+ peerDependencies:
1392
+ '@types/react': '*'
1393
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
1394
+ peerDependenciesMeta:
1395
+ '@types/react':
1396
+ optional: true
1397
+
1398
+ use-sidecar@1.1.3:
1399
+ resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==}
1400
+ engines: {node: '>=10'}
1401
+ peerDependencies:
1402
+ '@types/react': '*'
1403
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
1404
+ peerDependenciesMeta:
1405
+ '@types/react':
1406
+ optional: true
1407
+
1408
+ use-sync-external-store@1.6.0:
1409
+ resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==}
1410
+ peerDependencies:
1411
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
1412
+
1413
+ vite@6.4.2:
1414
+ resolution: {integrity: sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==}
1415
+ engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
1416
+ hasBin: true
1417
+ peerDependencies:
1418
+ '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
1419
+ jiti: '>=1.21.0'
1420
+ less: '*'
1421
+ lightningcss: ^1.21.0
1422
+ sass: '*'
1423
+ sass-embedded: '*'
1424
+ stylus: '*'
1425
+ sugarss: '*'
1426
+ terser: ^5.16.0
1427
+ tsx: ^4.8.1
1428
+ yaml: ^2.4.2
1429
+ peerDependenciesMeta:
1430
+ '@types/node':
1431
+ optional: true
1432
+ jiti:
1433
+ optional: true
1434
+ less:
1435
+ optional: true
1436
+ lightningcss:
1437
+ optional: true
1438
+ sass:
1439
+ optional: true
1440
+ sass-embedded:
1441
+ optional: true
1442
+ stylus:
1443
+ optional: true
1444
+ sugarss:
1445
+ optional: true
1446
+ terser:
1447
+ optional: true
1448
+ tsx:
1449
+ optional: true
1450
+ yaml:
1451
+ optional: true
1452
+
1453
+ w3c-keyname@2.2.8:
1454
+ resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
1455
+
1456
+ wouter@3.9.0:
1457
+ resolution: {integrity: sha512-sF/od/PIgqEQBQcrN7a2x3MX6MQE6nW0ygCfy9hQuUkuB28wEZuu/6M5GyqkrrEu9M6jxdkgE12yDFsQMKos4Q==}
1458
+ peerDependencies:
1459
+ react: '>=16.8.0'
1460
+
1461
+ yallist@3.1.1:
1462
+ resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
1463
+
1464
+ snapshots:
1465
+
1466
+ '@babel/code-frame@7.29.0':
1467
+ dependencies:
1468
+ '@babel/helper-validator-identifier': 7.28.5
1469
+ js-tokens: 4.0.0
1470
+ picocolors: 1.1.1
1471
+
1472
+ '@babel/compat-data@7.29.0': {}
1473
+
1474
+ '@babel/core@7.29.0':
1475
+ dependencies:
1476
+ '@babel/code-frame': 7.29.0
1477
+ '@babel/generator': 7.29.1
1478
+ '@babel/helper-compilation-targets': 7.28.6
1479
+ '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0)
1480
+ '@babel/helpers': 7.29.2
1481
+ '@babel/parser': 7.29.2
1482
+ '@babel/template': 7.28.6
1483
+ '@babel/traverse': 7.29.0
1484
+ '@babel/types': 7.29.0
1485
+ '@jridgewell/remapping': 2.3.5
1486
+ convert-source-map: 2.0.0
1487
+ debug: 4.4.3
1488
+ gensync: 1.0.0-beta.2
1489
+ json5: 2.2.3
1490
+ semver: 6.3.1
1491
+ transitivePeerDependencies:
1492
+ - supports-color
1493
+
1494
+ '@babel/generator@7.29.1':
1495
+ dependencies:
1496
+ '@babel/parser': 7.29.2
1497
+ '@babel/types': 7.29.0
1498
+ '@jridgewell/gen-mapping': 0.3.13
1499
+ '@jridgewell/trace-mapping': 0.3.31
1500
+ jsesc: 3.1.0
1501
+
1502
+ '@babel/helper-compilation-targets@7.28.6':
1503
+ dependencies:
1504
+ '@babel/compat-data': 7.29.0
1505
+ '@babel/helper-validator-option': 7.27.1
1506
+ browserslist: 4.28.2
1507
+ lru-cache: 5.1.1
1508
+ semver: 6.3.1
1509
+
1510
+ '@babel/helper-globals@7.28.0': {}
1511
+
1512
+ '@babel/helper-module-imports@7.28.6':
1513
+ dependencies:
1514
+ '@babel/traverse': 7.29.0
1515
+ '@babel/types': 7.29.0
1516
+ transitivePeerDependencies:
1517
+ - supports-color
1518
+
1519
+ '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)':
1520
+ dependencies:
1521
+ '@babel/core': 7.29.0
1522
+ '@babel/helper-module-imports': 7.28.6
1523
+ '@babel/helper-validator-identifier': 7.28.5
1524
+ '@babel/traverse': 7.29.0
1525
+ transitivePeerDependencies:
1526
+ - supports-color
1527
+
1528
+ '@babel/helper-plugin-utils@7.28.6': {}
1529
+
1530
+ '@babel/helper-string-parser@7.27.1': {}
1531
+
1532
+ '@babel/helper-validator-identifier@7.28.5': {}
1533
+
1534
+ '@babel/helper-validator-option@7.27.1': {}
1535
+
1536
+ '@babel/helpers@7.29.2':
1537
+ dependencies:
1538
+ '@babel/template': 7.28.6
1539
+ '@babel/types': 7.29.0
1540
+
1541
+ '@babel/parser@7.29.2':
1542
+ dependencies:
1543
+ '@babel/types': 7.29.0
1544
+
1545
+ '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)':
1546
+ dependencies:
1547
+ '@babel/core': 7.29.0
1548
+ '@babel/helper-plugin-utils': 7.28.6
1549
+
1550
+ '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)':
1551
+ dependencies:
1552
+ '@babel/core': 7.29.0
1553
+ '@babel/helper-plugin-utils': 7.28.6
1554
+
1555
+ '@babel/template@7.28.6':
1556
+ dependencies:
1557
+ '@babel/code-frame': 7.29.0
1558
+ '@babel/parser': 7.29.2
1559
+ '@babel/types': 7.29.0
1560
+
1561
+ '@babel/traverse@7.29.0':
1562
+ dependencies:
1563
+ '@babel/code-frame': 7.29.0
1564
+ '@babel/generator': 7.29.1
1565
+ '@babel/helper-globals': 7.28.0
1566
+ '@babel/parser': 7.29.2
1567
+ '@babel/template': 7.28.6
1568
+ '@babel/types': 7.29.0
1569
+ debug: 4.4.3
1570
+ transitivePeerDependencies:
1571
+ - supports-color
1572
+
1573
+ '@babel/types@7.29.0':
1574
+ dependencies:
1575
+ '@babel/helper-string-parser': 7.27.1
1576
+ '@babel/helper-validator-identifier': 7.28.5
1577
+
1578
+ '@codemirror/autocomplete@6.20.1':
1579
+ dependencies:
1580
+ '@codemirror/language': 6.12.3
1581
+ '@codemirror/state': 6.6.0
1582
+ '@codemirror/view': 6.41.1
1583
+ '@lezer/common': 1.5.2
1584
+
1585
+ '@codemirror/commands@6.10.3':
1586
+ dependencies:
1587
+ '@codemirror/language': 6.12.3
1588
+ '@codemirror/state': 6.6.0
1589
+ '@codemirror/view': 6.41.1
1590
+ '@lezer/common': 1.5.2
1591
+
1592
+ '@codemirror/lang-sql@6.10.0':
1593
+ dependencies:
1594
+ '@codemirror/autocomplete': 6.20.1
1595
+ '@codemirror/language': 6.12.3
1596
+ '@codemirror/state': 6.6.0
1597
+ '@lezer/common': 1.5.2
1598
+ '@lezer/highlight': 1.2.3
1599
+ '@lezer/lr': 1.4.10
1600
+
1601
+ '@codemirror/language@6.12.3':
1602
+ dependencies:
1603
+ '@codemirror/state': 6.6.0
1604
+ '@codemirror/view': 6.41.1
1605
+ '@lezer/common': 1.5.2
1606
+ '@lezer/highlight': 1.2.3
1607
+ '@lezer/lr': 1.4.10
1608
+ style-mod: 4.1.3
1609
+
1610
+ '@codemirror/lint@6.9.5':
1611
+ dependencies:
1612
+ '@codemirror/state': 6.6.0
1613
+ '@codemirror/view': 6.41.1
1614
+ crelt: 1.0.6
1615
+
1616
+ '@codemirror/search@6.7.0':
1617
+ dependencies:
1618
+ '@codemirror/state': 6.6.0
1619
+ '@codemirror/view': 6.41.1
1620
+ crelt: 1.0.6
1621
+
1622
+ '@codemirror/state@6.6.0':
1623
+ dependencies:
1624
+ '@marijn/find-cluster-break': 1.0.2
1625
+
1626
+ '@codemirror/theme-one-dark@6.1.3':
1627
+ dependencies:
1628
+ '@codemirror/language': 6.12.3
1629
+ '@codemirror/state': 6.6.0
1630
+ '@codemirror/view': 6.41.1
1631
+ '@lezer/highlight': 1.2.3
1632
+
1633
+ '@codemirror/view@6.41.1':
1634
+ dependencies:
1635
+ '@codemirror/state': 6.6.0
1636
+ crelt: 1.0.6
1637
+ style-mod: 4.1.3
1638
+ w3c-keyname: 2.2.8
1639
+
1640
+ '@esbuild/aix-ppc64@0.25.12':
1641
+ optional: true
1642
+
1643
+ '@esbuild/android-arm64@0.25.12':
1644
+ optional: true
1645
+
1646
+ '@esbuild/android-arm@0.25.12':
1647
+ optional: true
1648
+
1649
+ '@esbuild/android-x64@0.25.12':
1650
+ optional: true
1651
+
1652
+ '@esbuild/darwin-arm64@0.25.12':
1653
+ optional: true
1654
+
1655
+ '@esbuild/darwin-x64@0.25.12':
1656
+ optional: true
1657
+
1658
+ '@esbuild/freebsd-arm64@0.25.12':
1659
+ optional: true
1660
+
1661
+ '@esbuild/freebsd-x64@0.25.12':
1662
+ optional: true
1663
+
1664
+ '@esbuild/linux-arm64@0.25.12':
1665
+ optional: true
1666
+
1667
+ '@esbuild/linux-arm@0.25.12':
1668
+ optional: true
1669
+
1670
+ '@esbuild/linux-ia32@0.25.12':
1671
+ optional: true
1672
+
1673
+ '@esbuild/linux-loong64@0.25.12':
1674
+ optional: true
1675
+
1676
+ '@esbuild/linux-mips64el@0.25.12':
1677
+ optional: true
1678
+
1679
+ '@esbuild/linux-ppc64@0.25.12':
1680
+ optional: true
1681
+
1682
+ '@esbuild/linux-riscv64@0.25.12':
1683
+ optional: true
1684
+
1685
+ '@esbuild/linux-s390x@0.25.12':
1686
+ optional: true
1687
+
1688
+ '@esbuild/linux-x64@0.25.12':
1689
+ optional: true
1690
+
1691
+ '@esbuild/netbsd-arm64@0.25.12':
1692
+ optional: true
1693
+
1694
+ '@esbuild/netbsd-x64@0.25.12':
1695
+ optional: true
1696
+
1697
+ '@esbuild/openbsd-arm64@0.25.12':
1698
+ optional: true
1699
+
1700
+ '@esbuild/openbsd-x64@0.25.12':
1701
+ optional: true
1702
+
1703
+ '@esbuild/openharmony-arm64@0.25.12':
1704
+ optional: true
1705
+
1706
+ '@esbuild/sunos-x64@0.25.12':
1707
+ optional: true
1708
+
1709
+ '@esbuild/win32-arm64@0.25.12':
1710
+ optional: true
1711
+
1712
+ '@esbuild/win32-ia32@0.25.12':
1713
+ optional: true
1714
+
1715
+ '@esbuild/win32-x64@0.25.12':
1716
+ optional: true
1717
+
1718
+ '@floating-ui/core@1.7.5':
1719
+ dependencies:
1720
+ '@floating-ui/utils': 0.2.11
1721
+
1722
+ '@floating-ui/dom@1.7.6':
1723
+ dependencies:
1724
+ '@floating-ui/core': 1.7.5
1725
+ '@floating-ui/utils': 0.2.11
1726
+
1727
+ '@floating-ui/react-dom@2.1.8(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
1728
+ dependencies:
1729
+ '@floating-ui/dom': 1.7.6
1730
+ react: 19.2.5
1731
+ react-dom: 19.2.5(react@19.2.5)
1732
+
1733
+ '@floating-ui/utils@0.2.11': {}
1734
+
1735
+ '@jridgewell/gen-mapping@0.3.13':
1736
+ dependencies:
1737
+ '@jridgewell/sourcemap-codec': 1.5.5
1738
+ '@jridgewell/trace-mapping': 0.3.31
1739
+
1740
+ '@jridgewell/remapping@2.3.5':
1741
+ dependencies:
1742
+ '@jridgewell/gen-mapping': 0.3.13
1743
+ '@jridgewell/trace-mapping': 0.3.31
1744
+
1745
+ '@jridgewell/resolve-uri@3.1.2': {}
1746
+
1747
+ '@jridgewell/sourcemap-codec@1.5.5': {}
1748
+
1749
+ '@jridgewell/trace-mapping@0.3.31':
1750
+ dependencies:
1751
+ '@jridgewell/resolve-uri': 3.1.2
1752
+ '@jridgewell/sourcemap-codec': 1.5.5
1753
+
1754
+ '@lezer/common@1.5.2': {}
1755
+
1756
+ '@lezer/highlight@1.2.3':
1757
+ dependencies:
1758
+ '@lezer/common': 1.5.2
1759
+
1760
+ '@lezer/lr@1.4.10':
1761
+ dependencies:
1762
+ '@lezer/common': 1.5.2
1763
+
1764
+ '@marijn/find-cluster-break@1.0.2': {}
1765
+
1766
+ '@radix-ui/number@1.1.1': {}
1767
+
1768
+ '@radix-ui/primitive@1.1.3': {}
1769
+
1770
+ '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
1771
+ dependencies:
1772
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
1773
+ react: 19.2.5
1774
+ react-dom: 19.2.5(react@19.2.5)
1775
+ optionalDependencies:
1776
+ '@types/react': 19.2.14
1777
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
1778
+
1779
+ '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
1780
+ dependencies:
1781
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5)
1782
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5)
1783
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
1784
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5)
1785
+ react: 19.2.5
1786
+ react-dom: 19.2.5(react@19.2.5)
1787
+ optionalDependencies:
1788
+ '@types/react': 19.2.14
1789
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
1790
+
1791
+ '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.14)(react@19.2.5)':
1792
+ dependencies:
1793
+ react: 19.2.5
1794
+ optionalDependencies:
1795
+ '@types/react': 19.2.14
1796
+
1797
+ '@radix-ui/react-context@1.1.2(@types/react@19.2.14)(react@19.2.5)':
1798
+ dependencies:
1799
+ react: 19.2.5
1800
+ optionalDependencies:
1801
+ '@types/react': 19.2.14
1802
+
1803
+ '@radix-ui/react-direction@1.1.1(@types/react@19.2.14)(react@19.2.5)':
1804
+ dependencies:
1805
+ react: 19.2.5
1806
+ optionalDependencies:
1807
+ '@types/react': 19.2.14
1808
+
1809
+ '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
1810
+ dependencies:
1811
+ '@radix-ui/primitive': 1.1.3
1812
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5)
1813
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
1814
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5)
1815
+ '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.14)(react@19.2.5)
1816
+ react: 19.2.5
1817
+ react-dom: 19.2.5(react@19.2.5)
1818
+ optionalDependencies:
1819
+ '@types/react': 19.2.14
1820
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
1821
+
1822
+ '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.14)(react@19.2.5)':
1823
+ dependencies:
1824
+ react: 19.2.5
1825
+ optionalDependencies:
1826
+ '@types/react': 19.2.14
1827
+
1828
+ '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
1829
+ dependencies:
1830
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5)
1831
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
1832
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5)
1833
+ react: 19.2.5
1834
+ react-dom: 19.2.5(react@19.2.5)
1835
+ optionalDependencies:
1836
+ '@types/react': 19.2.14
1837
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
1838
+
1839
+ '@radix-ui/react-id@1.1.1(@types/react@19.2.14)(react@19.2.5)':
1840
+ dependencies:
1841
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5)
1842
+ react: 19.2.5
1843
+ optionalDependencies:
1844
+ '@types/react': 19.2.14
1845
+
1846
+ '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
1847
+ dependencies:
1848
+ '@floating-ui/react-dom': 2.1.8(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
1849
+ '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
1850
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5)
1851
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5)
1852
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
1853
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5)
1854
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5)
1855
+ '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.14)(react@19.2.5)
1856
+ '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.5)
1857
+ '@radix-ui/rect': 1.1.1
1858
+ react: 19.2.5
1859
+ react-dom: 19.2.5(react@19.2.5)
1860
+ optionalDependencies:
1861
+ '@types/react': 19.2.14
1862
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
1863
+
1864
+ '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
1865
+ dependencies:
1866
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
1867
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5)
1868
+ react: 19.2.5
1869
+ react-dom: 19.2.5(react@19.2.5)
1870
+ optionalDependencies:
1871
+ '@types/react': 19.2.14
1872
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
1873
+
1874
+ '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
1875
+ dependencies:
1876
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5)
1877
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5)
1878
+ react: 19.2.5
1879
+ react-dom: 19.2.5(react@19.2.5)
1880
+ optionalDependencies:
1881
+ '@types/react': 19.2.14
1882
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
1883
+
1884
+ '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
1885
+ dependencies:
1886
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5)
1887
+ react: 19.2.5
1888
+ react-dom: 19.2.5(react@19.2.5)
1889
+ optionalDependencies:
1890
+ '@types/react': 19.2.14
1891
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
1892
+
1893
+ '@radix-ui/react-primitive@2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
1894
+ dependencies:
1895
+ '@radix-ui/react-slot': 1.2.4(@types/react@19.2.14)(react@19.2.5)
1896
+ react: 19.2.5
1897
+ react-dom: 19.2.5(react@19.2.5)
1898
+ optionalDependencies:
1899
+ '@types/react': 19.2.14
1900
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
1901
+
1902
+ '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
1903
+ dependencies:
1904
+ '@radix-ui/primitive': 1.1.3
1905
+ '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
1906
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5)
1907
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5)
1908
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5)
1909
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5)
1910
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
1911
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5)
1912
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5)
1913
+ react: 19.2.5
1914
+ react-dom: 19.2.5(react@19.2.5)
1915
+ optionalDependencies:
1916
+ '@types/react': 19.2.14
1917
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
1918
+
1919
+ '@radix-ui/react-scroll-area@1.2.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
1920
+ dependencies:
1921
+ '@radix-ui/number': 1.1.1
1922
+ '@radix-ui/primitive': 1.1.3
1923
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5)
1924
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5)
1925
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5)
1926
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
1927
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
1928
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5)
1929
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5)
1930
+ react: 19.2.5
1931
+ react-dom: 19.2.5(react@19.2.5)
1932
+ optionalDependencies:
1933
+ '@types/react': 19.2.14
1934
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
1935
+
1936
+ '@radix-ui/react-select@2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
1937
+ dependencies:
1938
+ '@radix-ui/number': 1.1.1
1939
+ '@radix-ui/primitive': 1.1.3
1940
+ '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
1941
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5)
1942
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5)
1943
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5)
1944
+ '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
1945
+ '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.5)
1946
+ '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
1947
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5)
1948
+ '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
1949
+ '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
1950
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
1951
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5)
1952
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5)
1953
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5)
1954
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5)
1955
+ '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.5)
1956
+ '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
1957
+ aria-hidden: 1.2.6
1958
+ react: 19.2.5
1959
+ react-dom: 19.2.5(react@19.2.5)
1960
+ react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.5)
1961
+ optionalDependencies:
1962
+ '@types/react': 19.2.14
1963
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
1964
+
1965
+ '@radix-ui/react-separator@1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
1966
+ dependencies:
1967
+ '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
1968
+ react: 19.2.5
1969
+ react-dom: 19.2.5(react@19.2.5)
1970
+ optionalDependencies:
1971
+ '@types/react': 19.2.14
1972
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
1973
+
1974
+ '@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@19.2.5)':
1975
+ dependencies:
1976
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5)
1977
+ react: 19.2.5
1978
+ optionalDependencies:
1979
+ '@types/react': 19.2.14
1980
+
1981
+ '@radix-ui/react-slot@1.2.4(@types/react@19.2.14)(react@19.2.5)':
1982
+ dependencies:
1983
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5)
1984
+ react: 19.2.5
1985
+ optionalDependencies:
1986
+ '@types/react': 19.2.14
1987
+
1988
+ '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
1989
+ dependencies:
1990
+ '@radix-ui/primitive': 1.1.3
1991
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5)
1992
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5)
1993
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5)
1994
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
1995
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
1996
+ '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
1997
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5)
1998
+ react: 19.2.5
1999
+ react-dom: 19.2.5(react@19.2.5)
2000
+ optionalDependencies:
2001
+ '@types/react': 19.2.14
2002
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
2003
+
2004
+ '@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
2005
+ dependencies:
2006
+ '@radix-ui/primitive': 1.1.3
2007
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5)
2008
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5)
2009
+ '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
2010
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5)
2011
+ '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
2012
+ '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
2013
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
2014
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
2015
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5)
2016
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5)
2017
+ '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
2018
+ react: 19.2.5
2019
+ react-dom: 19.2.5(react@19.2.5)
2020
+ optionalDependencies:
2021
+ '@types/react': 19.2.14
2022
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
2023
+
2024
+ '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.14)(react@19.2.5)':
2025
+ dependencies:
2026
+ react: 19.2.5
2027
+ optionalDependencies:
2028
+ '@types/react': 19.2.14
2029
+
2030
+ '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.14)(react@19.2.5)':
2031
+ dependencies:
2032
+ '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.5)
2033
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5)
2034
+ react: 19.2.5
2035
+ optionalDependencies:
2036
+ '@types/react': 19.2.14
2037
+
2038
+ '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.14)(react@19.2.5)':
2039
+ dependencies:
2040
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5)
2041
+ react: 19.2.5
2042
+ optionalDependencies:
2043
+ '@types/react': 19.2.14
2044
+
2045
+ '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.14)(react@19.2.5)':
2046
+ dependencies:
2047
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5)
2048
+ react: 19.2.5
2049
+ optionalDependencies:
2050
+ '@types/react': 19.2.14
2051
+
2052
+ '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.14)(react@19.2.5)':
2053
+ dependencies:
2054
+ react: 19.2.5
2055
+ optionalDependencies:
2056
+ '@types/react': 19.2.14
2057
+
2058
+ '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.14)(react@19.2.5)':
2059
+ dependencies:
2060
+ react: 19.2.5
2061
+ optionalDependencies:
2062
+ '@types/react': 19.2.14
2063
+
2064
+ '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.14)(react@19.2.5)':
2065
+ dependencies:
2066
+ '@radix-ui/rect': 1.1.1
2067
+ react: 19.2.5
2068
+ optionalDependencies:
2069
+ '@types/react': 19.2.14
2070
+
2071
+ '@radix-ui/react-use-size@1.1.1(@types/react@19.2.14)(react@19.2.5)':
2072
+ dependencies:
2073
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5)
2074
+ react: 19.2.5
2075
+ optionalDependencies:
2076
+ '@types/react': 19.2.14
2077
+
2078
+ '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
2079
+ dependencies:
2080
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
2081
+ react: 19.2.5
2082
+ react-dom: 19.2.5(react@19.2.5)
2083
+ optionalDependencies:
2084
+ '@types/react': 19.2.14
2085
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
2086
+
2087
+ '@radix-ui/rect@1.1.1': {}
2088
+
2089
+ '@rolldown/pluginutils@1.0.0-beta.27': {}
2090
+
2091
+ '@rollup/rollup-android-arm-eabi@4.60.2':
2092
+ optional: true
2093
+
2094
+ '@rollup/rollup-android-arm64@4.60.2':
2095
+ optional: true
2096
+
2097
+ '@rollup/rollup-darwin-arm64@4.60.2':
2098
+ optional: true
2099
+
2100
+ '@rollup/rollup-darwin-x64@4.60.2':
2101
+ optional: true
2102
+
2103
+ '@rollup/rollup-freebsd-arm64@4.60.2':
2104
+ optional: true
2105
+
2106
+ '@rollup/rollup-freebsd-x64@4.60.2':
2107
+ optional: true
2108
+
2109
+ '@rollup/rollup-linux-arm-gnueabihf@4.60.2':
2110
+ optional: true
2111
+
2112
+ '@rollup/rollup-linux-arm-musleabihf@4.60.2':
2113
+ optional: true
2114
+
2115
+ '@rollup/rollup-linux-arm64-gnu@4.60.2':
2116
+ optional: true
2117
+
2118
+ '@rollup/rollup-linux-arm64-musl@4.60.2':
2119
+ optional: true
2120
+
2121
+ '@rollup/rollup-linux-loong64-gnu@4.60.2':
2122
+ optional: true
2123
+
2124
+ '@rollup/rollup-linux-loong64-musl@4.60.2':
2125
+ optional: true
2126
+
2127
+ '@rollup/rollup-linux-ppc64-gnu@4.60.2':
2128
+ optional: true
2129
+
2130
+ '@rollup/rollup-linux-ppc64-musl@4.60.2':
2131
+ optional: true
2132
+
2133
+ '@rollup/rollup-linux-riscv64-gnu@4.60.2':
2134
+ optional: true
2135
+
2136
+ '@rollup/rollup-linux-riscv64-musl@4.60.2':
2137
+ optional: true
2138
+
2139
+ '@rollup/rollup-linux-s390x-gnu@4.60.2':
2140
+ optional: true
2141
+
2142
+ '@rollup/rollup-linux-x64-gnu@4.60.2':
2143
+ optional: true
2144
+
2145
+ '@rollup/rollup-linux-x64-musl@4.60.2':
2146
+ optional: true
2147
+
2148
+ '@rollup/rollup-openbsd-x64@4.60.2':
2149
+ optional: true
2150
+
2151
+ '@rollup/rollup-openharmony-arm64@4.60.2':
2152
+ optional: true
2153
+
2154
+ '@rollup/rollup-win32-arm64-msvc@4.60.2':
2155
+ optional: true
2156
+
2157
+ '@rollup/rollup-win32-ia32-msvc@4.60.2':
2158
+ optional: true
2159
+
2160
+ '@rollup/rollup-win32-x64-gnu@4.60.2':
2161
+ optional: true
2162
+
2163
+ '@rollup/rollup-win32-x64-msvc@4.60.2':
2164
+ optional: true
2165
+
2166
+ '@scarf/scarf@1.4.0': {}
2167
+
2168
+ '@tailwindcss/node@4.2.4':
2169
+ dependencies:
2170
+ '@jridgewell/remapping': 2.3.5
2171
+ enhanced-resolve: 5.21.0
2172
+ jiti: 2.6.1
2173
+ lightningcss: 1.32.0
2174
+ magic-string: 0.30.21
2175
+ source-map-js: 1.2.1
2176
+ tailwindcss: 4.2.4
2177
+
2178
+ '@tailwindcss/oxide-android-arm64@4.2.4':
2179
+ optional: true
2180
+
2181
+ '@tailwindcss/oxide-darwin-arm64@4.2.4':
2182
+ optional: true
2183
+
2184
+ '@tailwindcss/oxide-darwin-x64@4.2.4':
2185
+ optional: true
2186
+
2187
+ '@tailwindcss/oxide-freebsd-x64@4.2.4':
2188
+ optional: true
2189
+
2190
+ '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.4':
2191
+ optional: true
2192
+
2193
+ '@tailwindcss/oxide-linux-arm64-gnu@4.2.4':
2194
+ optional: true
2195
+
2196
+ '@tailwindcss/oxide-linux-arm64-musl@4.2.4':
2197
+ optional: true
2198
+
2199
+ '@tailwindcss/oxide-linux-x64-gnu@4.2.4':
2200
+ optional: true
2201
+
2202
+ '@tailwindcss/oxide-linux-x64-musl@4.2.4':
2203
+ optional: true
2204
+
2205
+ '@tailwindcss/oxide-wasm32-wasi@4.2.4':
2206
+ optional: true
2207
+
2208
+ '@tailwindcss/oxide-win32-arm64-msvc@4.2.4':
2209
+ optional: true
2210
+
2211
+ '@tailwindcss/oxide-win32-x64-msvc@4.2.4':
2212
+ optional: true
2213
+
2214
+ '@tailwindcss/oxide@4.2.4':
2215
+ optionalDependencies:
2216
+ '@tailwindcss/oxide-android-arm64': 4.2.4
2217
+ '@tailwindcss/oxide-darwin-arm64': 4.2.4
2218
+ '@tailwindcss/oxide-darwin-x64': 4.2.4
2219
+ '@tailwindcss/oxide-freebsd-x64': 4.2.4
2220
+ '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.4
2221
+ '@tailwindcss/oxide-linux-arm64-gnu': 4.2.4
2222
+ '@tailwindcss/oxide-linux-arm64-musl': 4.2.4
2223
+ '@tailwindcss/oxide-linux-x64-gnu': 4.2.4
2224
+ '@tailwindcss/oxide-linux-x64-musl': 4.2.4
2225
+ '@tailwindcss/oxide-wasm32-wasi': 4.2.4
2226
+ '@tailwindcss/oxide-win32-arm64-msvc': 4.2.4
2227
+ '@tailwindcss/oxide-win32-x64-msvc': 4.2.4
2228
+
2229
+ '@tailwindcss/vite@4.2.4(vite@6.4.2(jiti@2.6.1)(lightningcss@1.32.0))':
2230
+ dependencies:
2231
+ '@tailwindcss/node': 4.2.4
2232
+ '@tailwindcss/oxide': 4.2.4
2233
+ tailwindcss: 4.2.4
2234
+ vite: 6.4.2(jiti@2.6.1)(lightningcss@1.32.0)
2235
+
2236
+ '@types/babel__core@7.20.5':
2237
+ dependencies:
2238
+ '@babel/parser': 7.29.2
2239
+ '@babel/types': 7.29.0
2240
+ '@types/babel__generator': 7.27.0
2241
+ '@types/babel__template': 7.4.4
2242
+ '@types/babel__traverse': 7.28.0
2243
+
2244
+ '@types/babel__generator@7.27.0':
2245
+ dependencies:
2246
+ '@babel/types': 7.29.0
2247
+
2248
+ '@types/babel__template@7.4.4':
2249
+ dependencies:
2250
+ '@babel/parser': 7.29.2
2251
+ '@babel/types': 7.29.0
2252
+
2253
+ '@types/babel__traverse@7.28.0':
2254
+ dependencies:
2255
+ '@babel/types': 7.29.0
2256
+
2257
+ '@types/estree@1.0.8': {}
2258
+
2259
+ '@types/react-dom@19.2.3(@types/react@19.2.14)':
2260
+ dependencies:
2261
+ '@types/react': 19.2.14
2262
+
2263
+ '@types/react@19.2.14':
2264
+ dependencies:
2265
+ csstype: 3.2.3
2266
+
2267
+ '@vitejs/plugin-react@4.7.0(vite@6.4.2(jiti@2.6.1)(lightningcss@1.32.0))':
2268
+ dependencies:
2269
+ '@babel/core': 7.29.0
2270
+ '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0)
2271
+ '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0)
2272
+ '@rolldown/pluginutils': 1.0.0-beta.27
2273
+ '@types/babel__core': 7.20.5
2274
+ react-refresh: 0.17.0
2275
+ vite: 6.4.2(jiti@2.6.1)(lightningcss@1.32.0)
2276
+ transitivePeerDependencies:
2277
+ - supports-color
2278
+
2279
+ aria-hidden@1.2.6:
2280
+ dependencies:
2281
+ tslib: 2.8.1
2282
+
2283
+ baseline-browser-mapping@2.10.23: {}
2284
+
2285
+ browserslist@4.28.2:
2286
+ dependencies:
2287
+ baseline-browser-mapping: 2.10.23
2288
+ caniuse-lite: 1.0.30001791
2289
+ electron-to-chromium: 1.5.344
2290
+ node-releases: 2.0.38
2291
+ update-browserslist-db: 1.2.3(browserslist@4.28.2)
2292
+
2293
+ caniuse-lite@1.0.30001791: {}
2294
+
2295
+ class-variance-authority@0.7.1:
2296
+ dependencies:
2297
+ clsx: 2.1.1
2298
+
2299
+ clsx@2.1.1: {}
2300
+
2301
+ codemirror@6.0.2:
2302
+ dependencies:
2303
+ '@codemirror/autocomplete': 6.20.1
2304
+ '@codemirror/commands': 6.10.3
2305
+ '@codemirror/language': 6.12.3
2306
+ '@codemirror/lint': 6.9.5
2307
+ '@codemirror/search': 6.7.0
2308
+ '@codemirror/state': 6.6.0
2309
+ '@codemirror/view': 6.41.1
2310
+
2311
+ convert-source-map@2.0.0: {}
2312
+
2313
+ crelt@1.0.6: {}
2314
+
2315
+ csstype@3.2.3: {}
2316
+
2317
+ debug@4.4.3:
2318
+ dependencies:
2319
+ ms: 2.1.3
2320
+
2321
+ detect-libc@2.1.2: {}
2322
+
2323
+ detect-node-es@1.1.0: {}
2324
+
2325
+ electron-to-chromium@1.5.344: {}
2326
+
2327
+ enhanced-resolve@5.21.0:
2328
+ dependencies:
2329
+ graceful-fs: 4.2.11
2330
+ tapable: 2.3.3
2331
+
2332
+ esbuild@0.25.12:
2333
+ optionalDependencies:
2334
+ '@esbuild/aix-ppc64': 0.25.12
2335
+ '@esbuild/android-arm': 0.25.12
2336
+ '@esbuild/android-arm64': 0.25.12
2337
+ '@esbuild/android-x64': 0.25.12
2338
+ '@esbuild/darwin-arm64': 0.25.12
2339
+ '@esbuild/darwin-x64': 0.25.12
2340
+ '@esbuild/freebsd-arm64': 0.25.12
2341
+ '@esbuild/freebsd-x64': 0.25.12
2342
+ '@esbuild/linux-arm': 0.25.12
2343
+ '@esbuild/linux-arm64': 0.25.12
2344
+ '@esbuild/linux-ia32': 0.25.12
2345
+ '@esbuild/linux-loong64': 0.25.12
2346
+ '@esbuild/linux-mips64el': 0.25.12
2347
+ '@esbuild/linux-ppc64': 0.25.12
2348
+ '@esbuild/linux-riscv64': 0.25.12
2349
+ '@esbuild/linux-s390x': 0.25.12
2350
+ '@esbuild/linux-x64': 0.25.12
2351
+ '@esbuild/netbsd-arm64': 0.25.12
2352
+ '@esbuild/netbsd-x64': 0.25.12
2353
+ '@esbuild/openbsd-arm64': 0.25.12
2354
+ '@esbuild/openbsd-x64': 0.25.12
2355
+ '@esbuild/openharmony-arm64': 0.25.12
2356
+ '@esbuild/sunos-x64': 0.25.12
2357
+ '@esbuild/win32-arm64': 0.25.12
2358
+ '@esbuild/win32-ia32': 0.25.12
2359
+ '@esbuild/win32-x64': 0.25.12
2360
+
2361
+ escalade@3.2.0: {}
2362
+
2363
+ fdir@6.5.0(picomatch@4.0.4):
2364
+ optionalDependencies:
2365
+ picomatch: 4.0.4
2366
+
2367
+ framer-motion@12.38.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
2368
+ dependencies:
2369
+ motion-dom: 12.38.0
2370
+ motion-utils: 12.36.0
2371
+ tslib: 2.8.1
2372
+ optionalDependencies:
2373
+ react: 19.2.5
2374
+ react-dom: 19.2.5(react@19.2.5)
2375
+
2376
+ fsevents@2.3.3:
2377
+ optional: true
2378
+
2379
+ gensync@1.0.0-beta.2: {}
2380
+
2381
+ get-nonce@1.0.1: {}
2382
+
2383
+ graceful-fs@4.2.11: {}
2384
+
2385
+ jiti@2.6.1: {}
2386
+
2387
+ js-tokens@4.0.0: {}
2388
+
2389
+ jsesc@3.1.0: {}
2390
+
2391
+ json5@2.2.3: {}
2392
+
2393
+ lightningcss-android-arm64@1.32.0:
2394
+ optional: true
2395
+
2396
+ lightningcss-darwin-arm64@1.32.0:
2397
+ optional: true
2398
+
2399
+ lightningcss-darwin-x64@1.32.0:
2400
+ optional: true
2401
+
2402
+ lightningcss-freebsd-x64@1.32.0:
2403
+ optional: true
2404
+
2405
+ lightningcss-linux-arm-gnueabihf@1.32.0:
2406
+ optional: true
2407
+
2408
+ lightningcss-linux-arm64-gnu@1.32.0:
2409
+ optional: true
2410
+
2411
+ lightningcss-linux-arm64-musl@1.32.0:
2412
+ optional: true
2413
+
2414
+ lightningcss-linux-x64-gnu@1.32.0:
2415
+ optional: true
2416
+
2417
+ lightningcss-linux-x64-musl@1.32.0:
2418
+ optional: true
2419
+
2420
+ lightningcss-win32-arm64-msvc@1.32.0:
2421
+ optional: true
2422
+
2423
+ lightningcss-win32-x64-msvc@1.32.0:
2424
+ optional: true
2425
+
2426
+ lightningcss@1.32.0:
2427
+ dependencies:
2428
+ detect-libc: 2.1.2
2429
+ optionalDependencies:
2430
+ lightningcss-android-arm64: 1.32.0
2431
+ lightningcss-darwin-arm64: 1.32.0
2432
+ lightningcss-darwin-x64: 1.32.0
2433
+ lightningcss-freebsd-x64: 1.32.0
2434
+ lightningcss-linux-arm-gnueabihf: 1.32.0
2435
+ lightningcss-linux-arm64-gnu: 1.32.0
2436
+ lightningcss-linux-arm64-musl: 1.32.0
2437
+ lightningcss-linux-x64-gnu: 1.32.0
2438
+ lightningcss-linux-x64-musl: 1.32.0
2439
+ lightningcss-win32-arm64-msvc: 1.32.0
2440
+ lightningcss-win32-x64-msvc: 1.32.0
2441
+
2442
+ lru-cache@5.1.1:
2443
+ dependencies:
2444
+ yallist: 3.1.1
2445
+
2446
+ lucide-react@0.453.0(react@19.2.5):
2447
+ dependencies:
2448
+ react: 19.2.5
2449
+
2450
+ magic-string@0.30.21:
2451
+ dependencies:
2452
+ '@jridgewell/sourcemap-codec': 1.5.5
2453
+
2454
+ mitt@3.0.1: {}
2455
+
2456
+ motion-dom@12.38.0:
2457
+ dependencies:
2458
+ motion-utils: 12.36.0
2459
+
2460
+ motion-utils@12.36.0: {}
2461
+
2462
+ ms@2.1.3: {}
2463
+
2464
+ nanoid@3.3.11: {}
2465
+
2466
+ node-releases@2.0.38: {}
2467
+
2468
+ picocolors@1.1.1: {}
2469
+
2470
+ picomatch@4.0.4: {}
2471
+
2472
+ postcss@8.5.12:
2473
+ dependencies:
2474
+ nanoid: 3.3.11
2475
+ picocolors: 1.1.1
2476
+ source-map-js: 1.2.1
2477
+
2478
+ react-dom@19.2.5(react@19.2.5):
2479
+ dependencies:
2480
+ react: 19.2.5
2481
+ scheduler: 0.27.0
2482
+
2483
+ react-refresh@0.17.0: {}
2484
+
2485
+ react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.5):
2486
+ dependencies:
2487
+ react: 19.2.5
2488
+ react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.5)
2489
+ tslib: 2.8.1
2490
+ optionalDependencies:
2491
+ '@types/react': 19.2.14
2492
+
2493
+ react-remove-scroll@2.7.2(@types/react@19.2.14)(react@19.2.5):
2494
+ dependencies:
2495
+ react: 19.2.5
2496
+ react-remove-scroll-bar: 2.3.8(@types/react@19.2.14)(react@19.2.5)
2497
+ react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.5)
2498
+ tslib: 2.8.1
2499
+ use-callback-ref: 1.3.3(@types/react@19.2.14)(react@19.2.5)
2500
+ use-sidecar: 1.1.3(@types/react@19.2.14)(react@19.2.5)
2501
+ optionalDependencies:
2502
+ '@types/react': 19.2.14
2503
+
2504
+ react-resizable-panels@3.0.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
2505
+ dependencies:
2506
+ react: 19.2.5
2507
+ react-dom: 19.2.5(react@19.2.5)
2508
+
2509
+ react-style-singleton@2.2.3(@types/react@19.2.14)(react@19.2.5):
2510
+ dependencies:
2511
+ get-nonce: 1.0.1
2512
+ react: 19.2.5
2513
+ tslib: 2.8.1
2514
+ optionalDependencies:
2515
+ '@types/react': 19.2.14
2516
+
2517
+ react@19.2.5: {}
2518
+
2519
+ regexparam@3.0.0: {}
2520
+
2521
+ rollup@4.60.2:
2522
+ dependencies:
2523
+ '@types/estree': 1.0.8
2524
+ optionalDependencies:
2525
+ '@rollup/rollup-android-arm-eabi': 4.60.2
2526
+ '@rollup/rollup-android-arm64': 4.60.2
2527
+ '@rollup/rollup-darwin-arm64': 4.60.2
2528
+ '@rollup/rollup-darwin-x64': 4.60.2
2529
+ '@rollup/rollup-freebsd-arm64': 4.60.2
2530
+ '@rollup/rollup-freebsd-x64': 4.60.2
2531
+ '@rollup/rollup-linux-arm-gnueabihf': 4.60.2
2532
+ '@rollup/rollup-linux-arm-musleabihf': 4.60.2
2533
+ '@rollup/rollup-linux-arm64-gnu': 4.60.2
2534
+ '@rollup/rollup-linux-arm64-musl': 4.60.2
2535
+ '@rollup/rollup-linux-loong64-gnu': 4.60.2
2536
+ '@rollup/rollup-linux-loong64-musl': 4.60.2
2537
+ '@rollup/rollup-linux-ppc64-gnu': 4.60.2
2538
+ '@rollup/rollup-linux-ppc64-musl': 4.60.2
2539
+ '@rollup/rollup-linux-riscv64-gnu': 4.60.2
2540
+ '@rollup/rollup-linux-riscv64-musl': 4.60.2
2541
+ '@rollup/rollup-linux-s390x-gnu': 4.60.2
2542
+ '@rollup/rollup-linux-x64-gnu': 4.60.2
2543
+ '@rollup/rollup-linux-x64-musl': 4.60.2
2544
+ '@rollup/rollup-openbsd-x64': 4.60.2
2545
+ '@rollup/rollup-openharmony-arm64': 4.60.2
2546
+ '@rollup/rollup-win32-arm64-msvc': 4.60.2
2547
+ '@rollup/rollup-win32-ia32-msvc': 4.60.2
2548
+ '@rollup/rollup-win32-x64-gnu': 4.60.2
2549
+ '@rollup/rollup-win32-x64-msvc': 4.60.2
2550
+ fsevents: 2.3.3
2551
+
2552
+ scheduler@0.27.0: {}
2553
+
2554
+ semver@6.3.1: {}
2555
+
2556
+ sonner@2.0.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
2557
+ dependencies:
2558
+ react: 19.2.5
2559
+ react-dom: 19.2.5(react@19.2.5)
2560
+
2561
+ source-map-js@1.2.1: {}
2562
+
2563
+ style-mod@4.1.3: {}
2564
+
2565
+ swagger-ui-dist@5.32.5:
2566
+ dependencies:
2567
+ '@scarf/scarf': 1.4.0
2568
+
2569
+ tailwind-merge@3.5.0: {}
2570
+
2571
+ tailwindcss@4.2.4: {}
2572
+
2573
+ tapable@2.3.3: {}
2574
+
2575
+ tinyglobby@0.2.16:
2576
+ dependencies:
2577
+ fdir: 6.5.0(picomatch@4.0.4)
2578
+ picomatch: 4.0.4
2579
+
2580
+ tslib@2.8.1: {}
2581
+
2582
+ typescript@5.9.3: {}
2583
+
2584
+ update-browserslist-db@1.2.3(browserslist@4.28.2):
2585
+ dependencies:
2586
+ browserslist: 4.28.2
2587
+ escalade: 3.2.0
2588
+ picocolors: 1.1.1
2589
+
2590
+ use-callback-ref@1.3.3(@types/react@19.2.14)(react@19.2.5):
2591
+ dependencies:
2592
+ react: 19.2.5
2593
+ tslib: 2.8.1
2594
+ optionalDependencies:
2595
+ '@types/react': 19.2.14
2596
+
2597
+ use-sidecar@1.1.3(@types/react@19.2.14)(react@19.2.5):
2598
+ dependencies:
2599
+ detect-node-es: 1.1.0
2600
+ react: 19.2.5
2601
+ tslib: 2.8.1
2602
+ optionalDependencies:
2603
+ '@types/react': 19.2.14
2604
+
2605
+ use-sync-external-store@1.6.0(react@19.2.5):
2606
+ dependencies:
2607
+ react: 19.2.5
2608
+
2609
+ vite@6.4.2(jiti@2.6.1)(lightningcss@1.32.0):
2610
+ dependencies:
2611
+ esbuild: 0.25.12
2612
+ fdir: 6.5.0(picomatch@4.0.4)
2613
+ picomatch: 4.0.4
2614
+ postcss: 8.5.12
2615
+ rollup: 4.60.2
2616
+ tinyglobby: 0.2.16
2617
+ optionalDependencies:
2618
+ fsevents: 2.3.3
2619
+ jiti: 2.6.1
2620
+ lightningcss: 1.32.0
2621
+
2622
+ w3c-keyname@2.2.8: {}
2623
+
2624
+ wouter@3.9.0(react@19.2.5):
2625
+ dependencies:
2626
+ mitt: 3.0.1
2627
+ react: 19.2.5
2628
+ regexparam: 3.0.0
2629
+ use-sync-external-store: 1.6.0(react@19.2.5)
2630
+
2631
+ yallist@3.1.1: {}
frontend/src/App.tsx ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Route, Switch } from "wouter";
2
+ import { Toaster } from "sonner";
3
+ import Analyzer from "./pages/Analyzer";
4
+ import SwaggerPage from "./pages/SwaggerPage";
5
+ import NotFound from "./pages/NotFound";
6
+
7
+ export default function App() {
8
+ return (
9
+ <>
10
+ <Toaster
11
+ theme="dark"
12
+ position="bottom-right"
13
+ toastOptions={{
14
+ style: {
15
+ background: "var(--bg-elevated)",
16
+ border: "1px solid var(--border)",
17
+ color: "var(--text-primary)",
18
+ fontFamily: "var(--font-sans)",
19
+ },
20
+ }}
21
+ />
22
+ <Switch>
23
+ <Route path="/" component={Analyzer} />
24
+ <Route path="/swagger" component={SwaggerPage} />
25
+ <Route component={NotFound} />
26
+ </Switch>
27
+ </>
28
+ );
29
+ }
frontend/src/components/AstTreeView.tsx ADDED
@@ -0,0 +1,303 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect, useMemo, useCallback } from "react";
2
+ import { ChevronRight, ChevronDown, Search, X } from "lucide-react";
3
+ import type { AstNode } from "@/lib/api";
4
+
5
+ // ── Node type → CSS class mapping ─────────────────────────────────────
6
+
7
+ function getNodeClass(type: string): string {
8
+ const t = type.toLowerCase();
9
+ if (t.includes("keyword") || t.includes("statement") || t.includes("clause")) return "ast-keyword";
10
+ if (t.includes("literal") || t.includes("quoted") || t.includes("string")) return "ast-literal";
11
+ if (t.includes("numeric") || t.includes("integer") || t.includes("float")) return "ast-numeric";
12
+ if (t.includes("comment")) return "ast-comment";
13
+ if (t.includes("function") || t.includes("func")) return "ast-function";
14
+ if (t.includes("operator") || t.includes("comparison") || t.includes("arithmetic")) return "ast-operator";
15
+ if (t.includes("type") || t.includes("datatype")) return "ast-type";
16
+ if (t.includes("whitespace") || t.includes("newline") || t.includes("indent")) return "ast-whitespace";
17
+ return "ast-default";
18
+ }
19
+
20
+ function getNodeBadgeStyle(type: string): React.CSSProperties {
21
+ const t = type.toLowerCase();
22
+ if (t.includes("keyword") || t.includes("statement") || t.includes("clause"))
23
+ return { background: "oklch(0.72 0.18 270 / 0.15)", color: "oklch(0.72 0.18 270)", border: "1px solid oklch(0.72 0.18 270 / 0.3)" };
24
+ if (t.includes("literal") || t.includes("quoted") || t.includes("string"))
25
+ return { background: "oklch(0.75 0.16 145 / 0.15)", color: "oklch(0.75 0.16 145)", border: "1px solid oklch(0.75 0.16 145 / 0.3)" };
26
+ if (t.includes("numeric") || t.includes("integer") || t.includes("float"))
27
+ return { background: "oklch(0.78 0.18 55 / 0.15)", color: "oklch(0.78 0.18 55)", border: "1px solid oklch(0.78 0.18 55 / 0.3)" };
28
+ if (t.includes("comment"))
29
+ return { background: "oklch(0.50 0.010 264 / 0.15)", color: "oklch(0.55 0.010 264)", border: "1px solid oklch(0.50 0.010 264 / 0.3)" };
30
+ if (t.includes("function") || t.includes("func"))
31
+ return { background: "oklch(0.78 0.16 210 / 0.15)", color: "oklch(0.78 0.16 210)", border: "1px solid oklch(0.78 0.16 210 / 0.3)" };
32
+ if (t.includes("operator") || t.includes("comparison"))
33
+ return { background: "oklch(0.82 0.010 264 / 0.10)", color: "oklch(0.82 0.010 264)", border: "1px solid oklch(0.82 0.010 264 / 0.3)" };
34
+ if (t.includes("whitespace") || t.includes("newline"))
35
+ return { background: "oklch(0.20 0.008 264 / 0.5)", color: "oklch(0.38 0.008 264)", border: "1px solid oklch(0.25 0.008 264 / 0.3)" };
36
+ return { background: "oklch(0.20 0.010 264 / 0.5)", color: "oklch(0.72 0.010 264)", border: "1px solid oklch(0.28 0.010 264 / 0.3)" };
37
+ }
38
+
39
+ // ── Search helpers ─────────────────────────────────────────────────────
40
+
41
+ function collectMatchIds(node: AstNode, query: string, matchIds: Set<string>, ancestorIds: Set<string>, path: string[]): boolean {
42
+ const q = query.toLowerCase();
43
+ const matches =
44
+ node.type.toLowerCase().includes(q) ||
45
+ node.name.toLowerCase().includes(q) ||
46
+ (node.raw?.toLowerCase().includes(q) ?? false);
47
+
48
+ let childMatched = false;
49
+ for (const child of node.children) {
50
+ if (collectMatchIds(child, query, matchIds, ancestorIds, [...path, node.id])) {
51
+ childMatched = true;
52
+ }
53
+ }
54
+
55
+ if (matches) {
56
+ matchIds.add(node.id);
57
+ path.forEach((id) => ancestorIds.add(id));
58
+ }
59
+
60
+ return matches || childMatched;
61
+ }
62
+
63
+ function highlightText(text: string, query: string): React.ReactNode {
64
+ if (!query) return text;
65
+ const idx = text.toLowerCase().indexOf(query.toLowerCase());
66
+ if (idx === -1) return text;
67
+ return (
68
+ <>
69
+ {text.slice(0, idx)}
70
+ <mark style={{ background: "oklch(0.68 0.16 210 / 0.35)", color: "inherit", borderRadius: "2px" }}>
71
+ {text.slice(idx, idx + query.length)}
72
+ </mark>
73
+ {text.slice(idx + query.length)}
74
+ </>
75
+ );
76
+ }
77
+
78
+ // ── Single tree node ───────────────────────────────────────────────────
79
+
80
+ interface NodeProps {
81
+ node: AstNode;
82
+ depth: number;
83
+ query: string;
84
+ matchIds: Set<string>;
85
+ ancestorIds: Set<string>;
86
+ expandedIds: Set<string>;
87
+ onToggle: (id: string) => void;
88
+ onJumpToLine?: (line: number) => void;
89
+ }
90
+
91
+ function TreeNode({ node, depth, query, matchIds, ancestorIds, expandedIds, onToggle, onJumpToLine }: NodeProps) {
92
+ const isMatch = matchIds.has(node.id);
93
+ const isAncestor = ancestorIds.has(node.id);
94
+ const isExpanded = expandedIds.has(node.id);
95
+ const hasChildren = node.children.length > 0;
96
+
97
+ // Skip pure whitespace nodes when no search
98
+ const isWhitespace = node.type.toLowerCase().includes("whitespace") || node.type.toLowerCase().includes("newline");
99
+ if (isWhitespace && !query && !node.raw?.trim()) return null;
100
+
101
+ const indent = depth * 16;
102
+ const nodeClass = getNodeClass(node.type);
103
+ const badgeStyle = getNodeBadgeStyle(node.type);
104
+
105
+ return (
106
+ <div>
107
+ <div
108
+ className={`flex items-center gap-1.5 py-0.5 px-2 rounded cursor-pointer group transition-colors duration-100 ${
109
+ isMatch
110
+ ? "bg-[oklch(0.68_0.16_210_/_0.12)] border border-[oklch(0.68_0.16_210_/_0.3)]"
111
+ : "hover:bg-[oklch(0.18_0.010_264)]"
112
+ }`}
113
+ style={{ paddingLeft: `${indent + 8}px` }}
114
+ onClick={() => {
115
+ if (hasChildren) onToggle(node.id);
116
+ if (node.start_line && onJumpToLine) onJumpToLine(node.start_line);
117
+ }}
118
+ >
119
+ {/* Expand/collapse chevron */}
120
+ <span className="w-4 h-4 flex items-center justify-center flex-shrink-0 text-[oklch(0.45_0.010_264)]">
121
+ {hasChildren ? (
122
+ isExpanded ? <ChevronDown size={12} /> : <ChevronRight size={12} />
123
+ ) : (
124
+ <span className="w-1 h-1 rounded-full bg-[oklch(0.30_0.010_264)]" />
125
+ )}
126
+ </span>
127
+
128
+ {/* Node type badge */}
129
+ <span
130
+ className="text-[10px] font-mono font-medium px-1.5 py-0.5 rounded flex-shrink-0"
131
+ style={badgeStyle}
132
+ >
133
+ {highlightText(node.type, query)}
134
+ </span>
135
+
136
+ {/* Raw value for leaf nodes */}
137
+ {node.is_leaf && node.raw && node.raw.trim() && (
138
+ <span className={`text-xs font-mono truncate max-w-[200px] ${nodeClass}`}>
139
+ {highlightText(JSON.stringify(node.raw), query)}
140
+ </span>
141
+ )}
142
+
143
+ {/* Position info */}
144
+ {node.start_line && (
145
+ <span className="text-[10px] text-[oklch(0.38_0.010_264)] ml-auto flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
146
+ L{node.start_line}:{node.start_pos}
147
+ </span>
148
+ )}
149
+ </div>
150
+
151
+ {/* Children */}
152
+ {hasChildren && isExpanded && (
153
+ <div>
154
+ {node.children.map((child) => (
155
+ <TreeNode
156
+ key={child.id}
157
+ node={child}
158
+ depth={depth + 1}
159
+ query={query}
160
+ matchIds={matchIds}
161
+ ancestorIds={ancestorIds}
162
+ expandedIds={expandedIds}
163
+ onToggle={onToggle}
164
+ onJumpToLine={onJumpToLine}
165
+ />
166
+ ))}
167
+ </div>
168
+ )}
169
+ </div>
170
+ );
171
+ }
172
+
173
+ // ── Main component ─────────────────────────────────────────────────────
174
+
175
+ interface AstTreeViewProps {
176
+ tree: AstNode | null;
177
+ tokenCount?: number;
178
+ depth?: number;
179
+ onJumpToLine?: (line: number) => void;
180
+ }
181
+
182
+ export function AstTreeView({ tree, tokenCount, depth, onJumpToLine }: AstTreeViewProps) {
183
+ const [query, setQuery] = useState("");
184
+ const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
185
+
186
+ // Auto-expand root on first load
187
+ useEffect(() => {
188
+ if (tree) {
189
+ setExpandedIds(new Set([tree.id, ...tree.children.map((c) => c.id)]));
190
+ }
191
+ }, [tree]);
192
+
193
+ const { matchIds, ancestorIds } = useMemo(() => {
194
+ if (!tree || !query.trim()) return { matchIds: new Set<string>(), ancestorIds: new Set<string>() };
195
+ const matchIds = new Set<string>();
196
+ const ancestorIds = new Set<string>();
197
+ collectMatchIds(tree, query.trim(), matchIds, ancestorIds, []);
198
+ return { matchIds, ancestorIds };
199
+ }, [tree, query]);
200
+
201
+ // Auto-expand ancestors when searching
202
+ useEffect(() => {
203
+ if (query.trim() && ancestorIds.size > 0) {
204
+ setExpandedIds((prev) => {
205
+ const next = new Set(prev);
206
+ ancestorIds.forEach((id) => next.add(id));
207
+ matchIds.forEach((id) => next.add(id));
208
+ return next;
209
+ });
210
+ }
211
+ }, [ancestorIds, matchIds, query]);
212
+
213
+ const handleToggle = useCallback((id: string) => {
214
+ setExpandedIds((prev) => {
215
+ const next = new Set(prev);
216
+ if (next.has(id)) next.delete(id);
217
+ else next.add(id);
218
+ return next;
219
+ });
220
+ }, []);
221
+
222
+ const expandAll = useCallback(() => {
223
+ if (!tree) return;
224
+ const ids = new Set<string>();
225
+ const collect = (n: AstNode) => { ids.add(n.id); n.children.forEach(collect); };
226
+ collect(tree);
227
+ setExpandedIds(ids);
228
+ }, [tree]);
229
+
230
+ const collapseAll = useCallback(() => {
231
+ if (!tree) return;
232
+ setExpandedIds(new Set([tree.id]));
233
+ }, [tree]);
234
+
235
+ if (!tree) {
236
+ return (
237
+ <div className="flex flex-col items-center justify-center h-full gap-3 text-[oklch(0.45_0.010_264)]">
238
+ <div className="text-4xl opacity-30">⟨/⟩</div>
239
+ <p className="text-sm">Run analysis to view the AST</p>
240
+ </div>
241
+ );
242
+ }
243
+
244
+ return (
245
+ <div className="flex flex-col h-full">
246
+ {/* Toolbar */}
247
+ <div className="flex items-center gap-2 px-3 py-2 border-b border-[oklch(0.22_0.010_264)] flex-shrink-0">
248
+ <div className="relative flex-1">
249
+ <Search size={12} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-[oklch(0.45_0.010_264)]" />
250
+ <input
251
+ value={query}
252
+ onChange={(e) => setQuery(e.target.value)}
253
+ placeholder="Filter by node type…"
254
+ className="h-7 pl-7 pr-7 text-xs w-full rounded"
255
+ style={{ background: "var(--bg-elevated)", border: "1px solid var(--border)", color: "var(--text-primary)", fontFamily: "var(--font-mono)", outline: "none", paddingLeft: "28px", paddingRight: "28px" }}
256
+ />
257
+ {query && (
258
+ <button
259
+ onClick={() => setQuery("")}
260
+ className="absolute right-2 top-1/2 -translate-y-1/2 text-[oklch(0.45_0.010_264)] hover:text-foreground"
261
+ >
262
+ <X size={12} />
263
+ </button>
264
+ )}
265
+ </div>
266
+ <button onClick={expandAll} className="h-7 px-2 text-xs rounded transition-colors" style={{ color: "var(--text-secondary)", background: "transparent" }}>Expand</button>
267
+ <button onClick={collapseAll} className="h-7 px-2 text-xs rounded transition-colors" style={{ color: "var(--text-secondary)", background: "transparent" }}>Collapse</button>
268
+ </div>
269
+
270
+ {/* Stats */}
271
+ <div className="flex items-center gap-3 px-3 py-1.5 border-b border-[oklch(0.22_0.010_264)] flex-shrink-0">
272
+ <span className="text-[10px] text-[oklch(0.45_0.010_264)]">
273
+ <span className="text-[oklch(0.68_0.16_210)]">{tokenCount}</span> tokens
274
+ </span>
275
+ <span className="text-[10px] text-[oklch(0.45_0.010_264)]">
276
+ depth <span className="text-[oklch(0.68_0.16_210)]">{depth}</span>
277
+ </span>
278
+ {query && matchIds.size > 0 && (
279
+ <span className="text-[10px] text-[oklch(0.75_0.16_145)]">
280
+ {matchIds.size} match{matchIds.size !== 1 ? "es" : ""}
281
+ </span>
282
+ )}
283
+ {query && matchIds.size === 0 && (
284
+ <span className="text-[10px] text-[oklch(0.60_0.20_25)]">No matches</span>
285
+ )}
286
+ </div>
287
+
288
+ {/* Tree */}
289
+ <div className="flex-1 overflow-auto py-1 text-xs">
290
+ <TreeNode
291
+ node={tree}
292
+ depth={0}
293
+ query={query}
294
+ matchIds={matchIds}
295
+ ancestorIds={ancestorIds}
296
+ expandedIds={expandedIds}
297
+ onToggle={handleToggle}
298
+ onJumpToLine={onJumpToLine}
299
+ />
300
+ </div>
301
+ </div>
302
+ );
303
+ }
frontend/src/components/FormatterPanel.tsx ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from "react";
2
+ import { Copy, Check, ArrowLeftRight, Wand2 } from "lucide-react";
3
+ import { toast } from "sonner";
4
+ import type { FormatResult } from "@/lib/api";
5
+
6
+ interface FormatterPanelProps {
7
+ result: FormatResult | null;
8
+ onApplyToEditor?: (sql: string) => void;
9
+ }
10
+
11
+ export function FormatterPanel({ result, onApplyToEditor }: FormatterPanelProps) {
12
+ const [copied, setCopied] = useState(false);
13
+
14
+ const handleCopy = async () => {
15
+ if (!result) return;
16
+ await navigator.clipboard.writeText(result.formatted);
17
+ setCopied(true);
18
+ toast.success("Copied to clipboard");
19
+ setTimeout(() => setCopied(false), 2000);
20
+ };
21
+
22
+ const handleApply = () => {
23
+ if (!result) return;
24
+ onApplyToEditor?.(result.formatted);
25
+ toast.success("Applied to editor");
26
+ };
27
+
28
+ if (!result) {
29
+ return (
30
+ <div className="flex flex-col items-center justify-center h-full gap-3 text-[oklch(0.45_0.010_264)]">
31
+ <Wand2 size={32} className="opacity-30" />
32
+ <p className="text-sm">Run analysis to see formatted SQL</p>
33
+ </div>
34
+ );
35
+ }
36
+
37
+ return (
38
+ <div className="flex flex-col h-full">
39
+ {/* Toolbar */}
40
+ <div className="flex items-center gap-2 px-3 py-2 border-b border-[oklch(0.22_0.010_264)] flex-shrink-0">
41
+ <div className="flex items-center gap-2 flex-1">
42
+ {result.changed ? (
43
+ <div className="flex items-center gap-1.5 text-xs" style={{ color: "oklch(0.72 0.17 160)" }}>
44
+ <Wand2 size={13} />
45
+ <span className="font-medium">{result.fixes_applied} fix{result.fixes_applied !== 1 ? "es" : ""} applied</span>
46
+ </div>
47
+ ) : (
48
+ <div className="flex items-center gap-1.5 text-xs text-[oklch(0.55_0.010_264)]">
49
+ <Check size={13} />
50
+ <span>No formatting changes needed</span>
51
+ </div>
52
+ )}
53
+ <span className="text-[10px] text-[oklch(0.38_0.010_264)] font-mono ml-2">
54
+ dialect: {result.dialect}
55
+ </span>
56
+ </div>
57
+
58
+ <button
59
+ onClick={handleCopy}
60
+ className="flex items-center gap-1.5 h-7 px-2 text-xs rounded transition-colors"
61
+ style={{ color: "var(--text-secondary)", background: "transparent" }}
62
+ >
63
+ {copied ? <Check size={12} /> : <Copy size={12} />}
64
+ {copied ? "Copied" : "Copy"}
65
+ </button>
66
+
67
+ <button
68
+ onClick={handleApply}
69
+ className="flex items-center gap-1.5 h-7 px-2 text-xs font-semibold rounded"
70
+ style={{ background: "var(--accent-blue)", color: "var(--bg-base)" }}
71
+ >
72
+ <ArrowLeftRight size={12} />
73
+ Apply to Editor
74
+ </button>
75
+ </div>
76
+
77
+ {/* Diff indicator */}
78
+ {result.changed && (
79
+ <div className="flex items-center gap-2 px-3 py-1.5 border-b border-[oklch(0.22_0.010_264)] flex-shrink-0 bg-[oklch(0.72_0.17_160_/_0.05)]">
80
+ <ArrowLeftRight size={11} style={{ color: "oklch(0.72 0.17 160)" }} />
81
+ <span className="text-[10px] text-[oklch(0.60_0.010_264)]">
82
+ Formatted SQL differs from original
83
+ </span>
84
+ </div>
85
+ )}
86
+
87
+ {/* Formatted SQL output */}
88
+ <div className="flex-1 overflow-auto">
89
+ <pre
90
+ className="p-4 text-xs font-mono leading-relaxed text-[oklch(0.85_0.010_264)] whitespace-pre-wrap break-words"
91
+ style={{ fontFamily: "'JetBrains Mono', monospace" }}
92
+ >
93
+ {result.formatted}
94
+ </pre>
95
+ </div>
96
+ </div>
97
+ );
98
+ }
frontend/src/components/InjectionPanel.tsx ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Shield, ShieldAlert, ShieldCheck, ShieldX, Info } from "lucide-react";
2
+ import type { InjectionResult, InjectionPattern } from "@/lib/api";
3
+
4
+ interface InjectionPanelProps {
5
+ result: InjectionResult | null;
6
+ onJumpToLine?: (line: number) => void;
7
+ }
8
+
9
+ const RISK_CONFIG = {
10
+ critical: {
11
+ label: "CRITICAL",
12
+ color: "oklch(0.60 0.20 25)",
13
+ bg: "oklch(0.60 0.20 25 / 0.12)",
14
+ border: "oklch(0.60 0.20 25 / 0.35)",
15
+ icon: ShieldX,
16
+ },
17
+ high: {
18
+ label: "HIGH",
19
+ color: "oklch(0.72 0.18 55)",
20
+ bg: "oklch(0.72 0.18 55 / 0.12)",
21
+ border: "oklch(0.72 0.18 55 / 0.35)",
22
+ icon: ShieldAlert,
23
+ },
24
+ medium: {
25
+ label: "MEDIUM",
26
+ color: "oklch(0.78 0.18 90)",
27
+ bg: "oklch(0.78 0.18 90 / 0.12)",
28
+ border: "oklch(0.78 0.18 90 / 0.35)",
29
+ icon: ShieldAlert,
30
+ },
31
+ low: {
32
+ label: "LOW",
33
+ color: "oklch(0.68 0.16 210)",
34
+ bg: "oklch(0.68 0.16 210 / 0.12)",
35
+ border: "oklch(0.68 0.16 210 / 0.35)",
36
+ icon: Shield,
37
+ },
38
+ };
39
+
40
+ function RiskMeter({ score }: { score: number }) {
41
+ const color =
42
+ score >= 75 ? "oklch(0.60 0.20 25)" :
43
+ score >= 50 ? "oklch(0.72 0.18 55)" :
44
+ score >= 25 ? "oklch(0.78 0.18 90)" :
45
+ "oklch(0.72 0.17 160)";
46
+
47
+ return (
48
+ <div className="flex items-center gap-3">
49
+ <div className="flex-1 h-1.5 rounded-full bg-[oklch(0.20_0.010_264)] overflow-hidden">
50
+ <div
51
+ className="h-full rounded-full transition-all duration-500"
52
+ style={{ width: `${score}%`, background: color }}
53
+ />
54
+ </div>
55
+ <span className="text-sm font-mono font-semibold w-12 text-right" style={{ color }}>
56
+ {score}/100
57
+ </span>
58
+ </div>
59
+ );
60
+ }
61
+
62
+ function PatternCard({ pattern, onJumpToLine }: { pattern: InjectionPattern; onJumpToLine?: (line: number) => void }) {
63
+ const cfg = RISK_CONFIG[pattern.risk_level] ?? RISK_CONFIG.low;
64
+ const Icon = cfg.icon;
65
+
66
+ return (
67
+ <div
68
+ className="mx-3 mb-2 rounded-lg border overflow-hidden"
69
+ style={{ borderColor: cfg.border, background: cfg.bg }}
70
+ >
71
+ {/* Header */}
72
+ <div className="flex items-center gap-2 px-3 py-2 border-b" style={{ borderColor: cfg.border }}>
73
+ <Icon size={14} style={{ color: cfg.color }} className="flex-shrink-0" />
74
+ <span className="text-xs font-semibold flex-1" style={{ color: cfg.color }}>
75
+ {pattern.description}
76
+ </span>
77
+ <span
78
+ className="text-[10px] font-bold px-1.5 py-0.5 rounded"
79
+ style={{ background: cfg.bg, color: cfg.color, border: `1px solid ${cfg.border}` }}
80
+ >
81
+ {cfg.label}
82
+ </span>
83
+ </div>
84
+
85
+ {/* Body */}
86
+ <div className="px-3 py-2 space-y-2">
87
+ {/* Category */}
88
+ <div className="flex items-center gap-2">
89
+ <span className="text-[10px] text-[oklch(0.45_0.010_264)]">Category:</span>
90
+ <span className="text-[10px] font-mono font-medium" style={{ color: cfg.color }}>
91
+ {pattern.category}
92
+ </span>
93
+ {pattern.line_no && (
94
+ <button
95
+ className="text-[10px] font-mono ml-auto text-[oklch(0.45_0.010_264)] hover:text-[oklch(0.68_0.16_210)] transition-colors"
96
+ onClick={() => pattern.line_no && onJumpToLine?.(pattern.line_no)}
97
+ >
98
+ L{pattern.line_no}:{pattern.line_pos}
99
+ </button>
100
+ )}
101
+ </div>
102
+
103
+ {/* Detail */}
104
+ <p className="text-xs text-[oklch(0.72_0.010_264)] leading-relaxed">{pattern.detail}</p>
105
+
106
+ {/* Offending token */}
107
+ {pattern.offending_token && (
108
+ <div className="rounded px-2 py-1.5" style={{ background: "oklch(0.10 0.008 264)" }}>
109
+ <p className="text-[10px] text-[oklch(0.45_0.010_264)] mb-1">Offending token:</p>
110
+ <code className="text-xs font-mono break-all" style={{ color: cfg.color }}>
111
+ {pattern.offending_token}
112
+ </code>
113
+ </div>
114
+ )}
115
+
116
+ {/* Recommendation */}
117
+ <div className="flex items-start gap-1.5 pt-1">
118
+ <Info size={10} className="flex-shrink-0 mt-0.5 text-[oklch(0.45_0.010_264)]" />
119
+ <p className="text-[10px] text-[oklch(0.55_0.010_264)] leading-relaxed">{pattern.recommendation}</p>
120
+ </div>
121
+ </div>
122
+ </div>
123
+ );
124
+ }
125
+
126
+ export function InjectionPanel({ result, onJumpToLine }: InjectionPanelProps) {
127
+ if (!result) {
128
+ return (
129
+ <div className="flex flex-col items-center justify-center h-full gap-3 text-[oklch(0.45_0.010_264)]">
130
+ <Shield size={32} className="opacity-30" />
131
+ <p className="text-sm">Run analysis to check for injection risks</p>
132
+ </div>
133
+ );
134
+ }
135
+
136
+ const { safe, risk_score, patterns, summary } = result;
137
+
138
+ const summaryColor =
139
+ risk_score >= 75 ? "oklch(0.60 0.20 25)" :
140
+ risk_score >= 50 ? "oklch(0.72 0.18 55)" :
141
+ risk_score >= 25 ? "oklch(0.78 0.18 90)" :
142
+ "oklch(0.72 0.17 160)";
143
+
144
+ const SummaryIcon = safe ? ShieldCheck : risk_score >= 75 ? ShieldX : ShieldAlert;
145
+
146
+ return (
147
+ <div className="flex flex-col h-full">
148
+ {/* Summary header */}
149
+ <div className="px-3 py-3 border-b border-[oklch(0.22_0.010_264)] flex-shrink-0 space-y-2">
150
+ <div className="flex items-center gap-2">
151
+ <SummaryIcon size={16} style={{ color: summaryColor }} />
152
+ <span className="text-xs font-semibold" style={{ color: summaryColor }}>
153
+ {safe ? "No injection patterns detected" : `${patterns.length} pattern${patterns.length !== 1 ? "s" : ""} detected`}
154
+ </span>
155
+ </div>
156
+ <RiskMeter score={risk_score} />
157
+ <p className="text-xs text-[oklch(0.60_0.010_264)] leading-relaxed">{summary}</p>
158
+ </div>
159
+
160
+ {/* Pattern cards */}
161
+ {safe ? (
162
+ <div className="flex flex-col items-center justify-center flex-1 gap-3">
163
+ <ShieldCheck size={40} style={{ color: "oklch(0.72 0.17 160)" }} className="opacity-60" />
164
+ <p className="text-sm font-medium" style={{ color: "oklch(0.72 0.17 160)" }}>SQL appears safe</p>
165
+ <p className="text-xs text-[oklch(0.45_0.010_264)]">No known injection patterns were found</p>
166
+ </div>
167
+ ) : (
168
+ <div className="flex-1 overflow-auto pt-2">
169
+ {patterns.map((p) => (
170
+ <PatternCard key={p.pattern_id} pattern={p} onJumpToLine={onJumpToLine} />
171
+ ))}
172
+ </div>
173
+ )}
174
+ </div>
175
+ );
176
+ }
frontend/src/components/LintPanel.tsx ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { AlertCircle, AlertTriangle, CheckCircle2, Wrench } from "lucide-react";
2
+ import type { LintResult, LintViolation } from "@/lib/api";
3
+
4
+ interface LintPanelProps {
5
+ result: LintResult | null;
6
+ onJumpToLine?: (line: number) => void;
7
+ }
8
+
9
+ function SeverityBadge({ warning }: { warning: boolean }) {
10
+ if (warning) {
11
+ return (
12
+ <span className="inline-flex items-center gap-1 text-[10px] font-medium px-1.5 py-0.5 rounded"
13
+ style={{ background: "oklch(0.78 0.18 55 / 0.15)", color: "oklch(0.78 0.18 55)", border: "1px solid oklch(0.78 0.18 55 / 0.3)" }}>
14
+ <AlertTriangle size={9} />
15
+ WARN
16
+ </span>
17
+ );
18
+ }
19
+ return (
20
+ <span className="inline-flex items-center gap-1 text-[10px] font-medium px-1.5 py-0.5 rounded"
21
+ style={{ background: "oklch(0.60 0.20 25 / 0.15)", color: "oklch(0.65 0.20 25)", border: "1px solid oklch(0.60 0.20 25 / 0.3)" }}>
22
+ <AlertCircle size={9} />
23
+ ERROR
24
+ </span>
25
+ );
26
+ }
27
+
28
+ function RuleCodeBadge({ code }: { code: string }) {
29
+ return (
30
+ <span className="text-[10px] font-mono font-medium px-1.5 py-0.5 rounded"
31
+ style={{ background: "oklch(0.68 0.16 210 / 0.12)", color: "oklch(0.68 0.16 210)", border: "1px solid oklch(0.68 0.16 210 / 0.25)" }}>
32
+ {code}
33
+ </span>
34
+ );
35
+ }
36
+
37
+ function ViolationRow({ v, onJumpToLine }: { v: LintViolation; onJumpToLine?: (line: number) => void }) {
38
+ return (
39
+ <div
40
+ className="group flex flex-col gap-1.5 px-3 py-2.5 border-b border-[oklch(0.18_0.010_264)] hover:bg-[oklch(0.16_0.010_264)] cursor-pointer transition-colors"
41
+ onClick={() => onJumpToLine?.(v.line_no)}
42
+ >
43
+ <div className="flex items-center gap-2 flex-wrap">
44
+ <SeverityBadge warning={v.warning} />
45
+ <RuleCodeBadge code={v.code} />
46
+ <span className="text-[10px] font-mono text-[oklch(0.45_0.010_264)] ml-auto group-hover:text-[oklch(0.55_0.010_264)] transition-colors">
47
+ L{v.line_no}:{v.line_pos}
48
+ </span>
49
+ {v.fixable && (
50
+ <span className="inline-flex items-center gap-0.5 text-[10px]"
51
+ style={{ color: "oklch(0.72 0.17 160)" }}>
52
+ <Wrench size={9} />
53
+ fixable
54
+ </span>
55
+ )}
56
+ </div>
57
+ <p className="text-xs text-[oklch(0.78_0.010_264)] leading-relaxed">{v.description}</p>
58
+ </div>
59
+ );
60
+ }
61
+
62
+ export function LintPanel({ result, onJumpToLine }: LintPanelProps) {
63
+ if (!result) {
64
+ return (
65
+ <div className="flex flex-col items-center justify-center h-full gap-3 text-[oklch(0.45_0.010_264)]">
66
+ <AlertCircle size={32} className="opacity-30" />
67
+ <p className="text-sm">Run analysis to see lint results</p>
68
+ </div>
69
+ );
70
+ }
71
+
72
+ const { violations, passed, stats } = result;
73
+
74
+ return (
75
+ <div className="flex flex-col h-full">
76
+ {/* Summary bar */}
77
+ <div className="flex items-center gap-4 px-3 py-2 border-b border-[oklch(0.22_0.010_264)] flex-shrink-0">
78
+ {passed ? (
79
+ <div className="flex items-center gap-1.5 text-xs" style={{ color: "oklch(0.72 0.17 160)" }}>
80
+ <CheckCircle2 size={14} />
81
+ <span className="font-medium">All checks passed</span>
82
+ </div>
83
+ ) : (
84
+ <div className="flex items-center gap-1.5 text-xs" style={{ color: "oklch(0.65 0.20 25)" }}>
85
+ <AlertCircle size={14} />
86
+ <span className="font-medium">{stats.total} violation{stats.total !== 1 ? "s" : ""}</span>
87
+ </div>
88
+ )}
89
+ <div className="flex items-center gap-3 ml-auto text-[10px] text-[oklch(0.45_0.010_264)]">
90
+ {stats.errors > 0 && (
91
+ <span style={{ color: "oklch(0.65 0.20 25)" }}>{stats.errors} error{stats.errors !== 1 ? "s" : ""}</span>
92
+ )}
93
+ {stats.warnings > 0 && (
94
+ <span style={{ color: "oklch(0.78 0.18 55)" }}>{stats.warnings} warning{stats.warnings !== 1 ? "s" : ""}</span>
95
+ )}
96
+ {stats.fixable > 0 && (
97
+ <span style={{ color: "oklch(0.72 0.17 160)" }}>{stats.fixable} fixable</span>
98
+ )}
99
+ </div>
100
+ </div>
101
+
102
+ {/* Violations list */}
103
+ {passed ? (
104
+ <div className="flex flex-col items-center justify-center h-full gap-3">
105
+ <CheckCircle2 size={40} style={{ color: "oklch(0.72 0.17 160)" }} className="opacity-60" />
106
+ <p className="text-sm font-medium" style={{ color: "oklch(0.72 0.17 160)" }}>No violations found</p>
107
+ <p className="text-xs text-[oklch(0.45_0.010_264)]">Your SQL is clean for dialect: {result.dialect}</p>
108
+ </div>
109
+ ) : (
110
+ <div className="flex-1 overflow-auto">
111
+ {violations.map((v, i) => (
112
+ <ViolationRow key={`${v.code}-${v.line_no}-${v.line_pos}-${i}`} v={v} onJumpToLine={onJumpToLine} />
113
+ ))}
114
+ </div>
115
+ )}
116
+ </div>
117
+ );
118
+ }
frontend/src/components/SqlEditor.tsx ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useRef, useCallback } from "react";
2
+ import { EditorView, keymap, lineNumbers, highlightActiveLineGutter, highlightActiveLine, drawSelection, dropCursor } from "@codemirror/view";
3
+ import { EditorState, Compartment } from "@codemirror/state";
4
+ import { defaultKeymap, history, historyKeymap, indentWithTab } from "@codemirror/commands";
5
+ import { sql, MySQL, PostgreSQL, StandardSQL, MSSQL, SQLite } from "@codemirror/lang-sql";
6
+ import { oneDark } from "@codemirror/theme-one-dark";
7
+ import { bracketMatching, indentOnInput, syntaxHighlighting, defaultHighlightStyle, foldGutter, foldKeymap } from "@codemirror/language";
8
+ import { autocompletion, completionKeymap, closeBrackets, closeBracketsKeymap } from "@codemirror/autocomplete";
9
+ import { lintKeymap } from "@codemirror/lint";
10
+
11
+ interface SqlEditorProps {
12
+ value: string;
13
+ onChange: (value: string) => void;
14
+ onAnalyze: () => void;
15
+ dialect: string;
16
+ }
17
+
18
+ const dialectMap: Record<string, unknown> = {
19
+ mysql: MySQL,
20
+ postgres: PostgreSQL,
21
+ tsql: MSSQL,
22
+ sqlite: SQLite,
23
+ ansi: StandardSQL,
24
+ };
25
+
26
+ const dialectCompartment = new Compartment();
27
+
28
+ function getDialectExtension(dialect: string) {
29
+ const schema = dialectMap[dialect] ?? StandardSQL;
30
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
31
+ return sql({ dialect: schema as any });
32
+ }
33
+
34
+ export function SqlEditor({ value, onChange, onAnalyze, dialect }: SqlEditorProps) {
35
+ const containerRef = useRef<HTMLDivElement>(null);
36
+ const viewRef = useRef<EditorView | null>(null);
37
+
38
+ const onAnalyzeRef = useRef(onAnalyze);
39
+ onAnalyzeRef.current = onAnalyze;
40
+
41
+ // Create editor once
42
+ useEffect(() => {
43
+ if (!containerRef.current) return;
44
+
45
+ const analyzeKeymap = keymap.of([
46
+ {
47
+ key: "Ctrl-Enter",
48
+ mac: "Cmd-Enter",
49
+ run: () => {
50
+ onAnalyzeRef.current();
51
+ return true;
52
+ },
53
+ },
54
+ ]);
55
+
56
+ const state = EditorState.create({
57
+ doc: value,
58
+ extensions: [
59
+ lineNumbers(),
60
+ highlightActiveLineGutter(),
61
+ highlightActiveLine(),
62
+ history(),
63
+ drawSelection(),
64
+ dropCursor(),
65
+ indentOnInput(),
66
+ bracketMatching(),
67
+ closeBrackets(),
68
+ autocompletion(),
69
+ foldGutter(),
70
+ syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
71
+ dialectCompartment.of(getDialectExtension(dialect)),
72
+ oneDark,
73
+ keymap.of([
74
+ ...closeBracketsKeymap,
75
+ ...defaultKeymap,
76
+ ...historyKeymap,
77
+ ...foldKeymap,
78
+ ...completionKeymap,
79
+ ...lintKeymap,
80
+ indentWithTab,
81
+ ]),
82
+ analyzeKeymap,
83
+ EditorView.updateListener.of((update) => {
84
+ if (update.docChanged) {
85
+ onChange(update.state.doc.toString());
86
+ }
87
+ }),
88
+ EditorView.theme({
89
+ "&": {
90
+ height: "100%",
91
+ backgroundColor: "oklch(0.10 0.008 264)",
92
+ },
93
+ ".cm-content": {
94
+ padding: "12px 0",
95
+ caretColor: "oklch(0.68 0.16 210)",
96
+ },
97
+ ".cm-line": {
98
+ padding: "0 16px 0 8px",
99
+ },
100
+ ".cm-cursor": {
101
+ borderLeftColor: "oklch(0.68 0.16 210)",
102
+ borderLeftWidth: "2px",
103
+ },
104
+ }),
105
+ ],
106
+ });
107
+
108
+ const view = new EditorView({ state, parent: containerRef.current });
109
+ viewRef.current = view;
110
+
111
+ return () => {
112
+ view.destroy();
113
+ viewRef.current = null;
114
+ };
115
+ // eslint-disable-next-line react-hooks/exhaustive-deps
116
+ }, []);
117
+
118
+ // Sync dialect changes
119
+ useEffect(() => {
120
+ const view = viewRef.current;
121
+ if (!view) return;
122
+ view.dispatch({
123
+ effects: dialectCompartment.reconfigure(getDialectExtension(dialect)),
124
+ });
125
+ }, [dialect]);
126
+
127
+ // Sync external value changes (e.g. "apply formatted")
128
+ const lastValueRef = useRef(value);
129
+ useEffect(() => {
130
+ const view = viewRef.current;
131
+ if (!view) return;
132
+ const current = view.state.doc.toString();
133
+ if (current !== value && value !== lastValueRef.current) {
134
+ view.dispatch({
135
+ changes: { from: 0, to: current.length, insert: value },
136
+ });
137
+ }
138
+ lastValueRef.current = value;
139
+ }, [value]);
140
+
141
+ // Jump to line
142
+ const jumpToLine = useCallback((lineNo: number) => {
143
+ const view = viewRef.current;
144
+ if (!view) return;
145
+ const line = view.state.doc.line(Math.max(1, Math.min(lineNo, view.state.doc.lines)));
146
+ view.dispatch({
147
+ selection: { anchor: line.from },
148
+ scrollIntoView: true,
149
+ });
150
+ view.focus();
151
+ }, []);
152
+
153
+ // Expose jumpToLine via a custom event
154
+ useEffect(() => {
155
+ const el = containerRef.current;
156
+ if (!el) return;
157
+ const handler = (e: Event) => {
158
+ const lineNo = (e as CustomEvent<number>).detail;
159
+ jumpToLine(lineNo);
160
+ };
161
+ el.addEventListener("jump-to-line", handler);
162
+ return () => el.removeEventListener("jump-to-line", handler);
163
+ }, [jumpToLine]);
164
+
165
+ return (
166
+ <div
167
+ ref={containerRef}
168
+ className="h-full w-full overflow-hidden"
169
+ style={{ fontFamily: "'JetBrains Mono', monospace" }}
170
+ data-editor-container
171
+ />
172
+ );
173
+ }
frontend/src/index.css ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import "tailwindcss";
2
+
3
+ :root {
4
+ --bg-base: #0d1117;
5
+ --bg-surface: #161b22;
6
+ --bg-elevated: #1c2128;
7
+ --bg-overlay: #21262d;
8
+ --border: #30363d;
9
+ --border-muted: #21262d;
10
+
11
+ --text-primary: #e6edf3;
12
+ --text-secondary: #8b949e;
13
+ --text-muted: #484f58;
14
+
15
+ --accent-blue: #58a6ff;
16
+ --accent-cyan: #39d353;
17
+ --accent-purple: #bc8cff;
18
+ --accent-orange: #f78166;
19
+ --accent-yellow: #e3b341;
20
+ --accent-red: #ff7b72;
21
+ --accent-green: #3fb950;
22
+
23
+ --font-sans: "Inter", system-ui, sans-serif;
24
+ --font-mono: "JetBrains Mono", "Fira Code", "Cascadia Code", monospace;
25
+ }
26
+
27
+ *, *::before, *::after { box-sizing: border-box; }
28
+
29
+ html, body, #root {
30
+ height: 100%;
31
+ margin: 0;
32
+ padding: 0;
33
+ background: var(--bg-base);
34
+ color: var(--text-primary);
35
+ font-family: var(--font-sans);
36
+ font-size: 14px;
37
+ line-height: 1.5;
38
+ -webkit-font-smoothing: antialiased;
39
+ }
40
+
41
+ /* Scrollbars */
42
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
43
+ ::-webkit-scrollbar-track { background: transparent; }
44
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
45
+ ::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
46
+
47
+ /* CodeMirror dark overrides */
48
+ .cm-editor {
49
+ height: 100%;
50
+ font-family: var(--font-mono) !important;
51
+ font-size: 13px !important;
52
+ background: var(--bg-base) !important;
53
+ }
54
+ .cm-editor.cm-focused { outline: none !important; }
55
+ .cm-scroller { overflow: auto !important; }
56
+ .cm-gutters {
57
+ background: var(--bg-surface) !important;
58
+ border-right: 1px solid var(--border) !important;
59
+ color: var(--text-muted) !important;
60
+ }
61
+ .cm-activeLineGutter { background: var(--bg-elevated) !important; }
62
+ .cm-activeLine { background: rgba(88, 166, 255, 0.04) !important; }
63
+ .cm-selectionBackground { background: rgba(88, 166, 255, 0.15) !important; }
64
+ .cm-cursor { border-left-color: var(--accent-blue) !important; }
65
+ .cm-content { caret-color: var(--accent-blue) !important; }
66
+
67
+ /* AST tree */
68
+ .ast-tree { font-family: var(--font-mono); font-size: 12px; }
69
+ .ast-node-toggle { cursor: pointer; user-select: none; }
70
+ .ast-node-toggle:hover { opacity: 0.8; }
71
+
72
+ /* Resizable panel handle */
73
+ [data-panel-resize-handle-id] {
74
+ background: var(--border);
75
+ transition: background 0.15s;
76
+ }
77
+ [data-panel-resize-handle-id]:hover,
78
+ [data-panel-resize-handle-id][data-resize-handle-active] {
79
+ background: var(--accent-blue);
80
+ }
81
+
82
+ /* Swagger UI embed */
83
+ .swagger-ui-frame {
84
+ width: 100%;
85
+ height: 100%;
86
+ border: none;
87
+ background: #fff;
88
+ }
frontend/src/lib/api.ts ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * SQL Analyzer REST API client
3
+ * Plain fetch() — no tRPC, no Node.js dependency.
4
+ * Base URL is determined by the VITE_API_BASE env var (defaults to "").
5
+ * In production the React bundle is served by FastAPI itself, so "" works.
6
+ * In dev, Vite proxies /api/* to http://localhost:7860.
7
+ */
8
+
9
+ const BASE = (import.meta.env.VITE_API_BASE as string | undefined) ?? "";
10
+
11
+ async function post<T>(path: string, body: unknown): Promise<T> {
12
+ const res = await fetch(`${BASE}${path}`, {
13
+ method: "POST",
14
+ headers: { "Content-Type": "application/json" },
15
+ body: JSON.stringify(body),
16
+ });
17
+ if (!res.ok) {
18
+ const text = await res.text().catch(() => "");
19
+ throw new Error(`API error ${res.status}: ${text.slice(0, 400)}`);
20
+ }
21
+ return res.json() as Promise<T>;
22
+ }
23
+
24
+ async function get<T>(path: string): Promise<T> {
25
+ const res = await fetch(`${BASE}${path}`);
26
+ if (!res.ok) {
27
+ const text = await res.text().catch(() => "");
28
+ throw new Error(`API error ${res.status}: ${text.slice(0, 400)}`);
29
+ }
30
+ return res.json() as Promise<T>;
31
+ }
32
+
33
+ // ── Types ──────────────────────────────────────────────────────────────────
34
+
35
+ export interface LintViolation {
36
+ line_no: number;
37
+ line_pos: number;
38
+ code: string;
39
+ description: string;
40
+ name: string;
41
+ warning: boolean;
42
+ fixable: boolean;
43
+ }
44
+
45
+ export interface LintResult {
46
+ dialect: string;
47
+ violations: LintViolation[];
48
+ passed: boolean;
49
+ stats: { total: number; errors: number; warnings: number; fixable: number };
50
+ }
51
+
52
+ export interface AstNode {
53
+ id: string;
54
+ type: string;
55
+ name: string;
56
+ raw: string | null;
57
+ start_line: number | null;
58
+ start_pos: number | null;
59
+ end_line: number | null;
60
+ end_pos: number | null;
61
+ is_leaf: boolean;
62
+ children: AstNode[];
63
+ }
64
+
65
+ export interface ParseResult {
66
+ dialect: string;
67
+ tree: AstNode;
68
+ token_count: number;
69
+ depth: number;
70
+ }
71
+
72
+ export interface FormatResult {
73
+ dialect: string;
74
+ original: string;
75
+ formatted: string;
76
+ changed: boolean;
77
+ fixes_applied: number;
78
+ }
79
+
80
+ export interface InjectionPattern {
81
+ pattern_id: string;
82
+ risk_level: "critical" | "high" | "medium" | "low";
83
+ category: string;
84
+ description: string;
85
+ detail: string;
86
+ offending_token: string | null;
87
+ line_no: number | null;
88
+ line_pos: number | null;
89
+ recommendation: string;
90
+ }
91
+
92
+ export interface InjectionResult {
93
+ dialect: string;
94
+ safe: boolean;
95
+ risk_score: number;
96
+ patterns: InjectionPattern[];
97
+ summary: string;
98
+ }
99
+
100
+ export interface HealthResult {
101
+ status: string;
102
+ version: string;
103
+ dialects: string[];
104
+ }
105
+
106
+ // ── API calls ──────────────────────────────────────────────────────────────
107
+
108
+ export const api = {
109
+ health: () => get<HealthResult>("/api/health"),
110
+ lint: (sql: string, dialect: string) => post<LintResult>("/api/lint", { sql, dialect }),
111
+ parse: (sql: string, dialect: string) => post<ParseResult>("/api/parse", { sql, dialect }),
112
+ format: (sql: string, dialect: string) => post<FormatResult>("/api/format", { sql, dialect }),
113
+ inject: (sql: string, dialect: string) => post<InjectionResult>("/api/inject", { sql, dialect }),
114
+ };
frontend/src/main.tsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StrictMode } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import "./index.css";
4
+ import App from "./App";
5
+
6
+ createRoot(document.getElementById("root")!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>
10
+ );
frontend/src/pages/Analyzer.tsx ADDED
@@ -0,0 +1,366 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useRef, useCallback, useEffect } from "react";
2
+ import { PanelGroup, Panel, PanelResizeHandle } from "react-resizable-panels";
3
+ import { Play, Loader2, BookOpen, Zap, Database } from "lucide-react";
4
+ import { toast } from "sonner";
5
+ import { Link } from "wouter";
6
+ import { api, type LintResult, type ParseResult, type FormatResult, type InjectionResult, type HealthResult } from "@/lib/api";
7
+ import { SqlEditor } from "@/components/SqlEditor";
8
+ import { AstTreeView } from "@/components/AstTreeView";
9
+ import { LintPanel } from "@/components/LintPanel";
10
+ import { InjectionPanel } from "@/components/InjectionPanel";
11
+ import { FormatterPanel } from "@/components/FormatterPanel";
12
+
13
+ // ── Constants ──────────────────────────────────────────────────────────────
14
+
15
+ type AnalysisTab = "ast" | "lint" | "injection" | "formatted";
16
+
17
+ const TABS: { id: AnalysisTab; label: string }[] = [
18
+ { id: "ast", label: "AST Tree" },
19
+ { id: "lint", label: "Lint" },
20
+ { id: "injection", label: "Injection" },
21
+ { id: "formatted", label: "Formatted" },
22
+ ];
23
+
24
+ const DIALECTS = [
25
+ { value: "ansi", label: "ANSI SQL" },
26
+ { value: "postgres", label: "PostgreSQL" },
27
+ { value: "mysql", label: "MySQL" },
28
+ { value: "tsql", label: "T-SQL" },
29
+ { value: "sqlite", label: "SQLite" },
30
+ { value: "bigquery", label: "BigQuery" },
31
+ { value: "snowflake", label: "Snowflake" },
32
+ { value: "redshift", label: "Redshift" },
33
+ { value: "duckdb", label: "DuckDB" },
34
+ { value: "hive", label: "Hive" },
35
+ { value: "sparksql", label: "Spark SQL" },
36
+ { value: "trino", label: "Trino" },
37
+ { value: "databricks", label: "Databricks" },
38
+ { value: "oracle", label: "Oracle" },
39
+ { value: "teradata", label: "Teradata" },
40
+ { value: "clickhouse", label: "ClickHouse" },
41
+ { value: "athena", label: "Athena" },
42
+ ];
43
+
44
+ const DEFAULT_SQL = `-- SQL Analyzer Demo
45
+ -- Press Ctrl+Enter (or Cmd+Enter) to analyze
46
+
47
+ SELECT
48
+ u.id,
49
+ u.name,
50
+ u.email,
51
+ o.total,
52
+ o.created_at
53
+ FROM users u
54
+ INNER JOIN orders o ON u.id = o.user_id
55
+ WHERE u.active = 1
56
+ AND o.created_at >= '2024-01-01'
57
+ ORDER BY o.created_at DESC
58
+ LIMIT 50;`;
59
+
60
+ const INJECTION_SQL = `-- SQL Injection Demo
61
+ -- This query contains several injection patterns
62
+
63
+ SELECT * FROM users
64
+ WHERE username = 'admin' OR 1=1 --
65
+ AND password = 'anything';
66
+
67
+ -- Stacked query attempt
68
+ SELECT * FROM products WHERE id = 1;
69
+ DROP TABLE users;
70
+
71
+ -- UNION-based exfiltration
72
+ SELECT id, name FROM products
73
+ UNION SELECT username, password FROM users;`;
74
+
75
+ // ── Status indicator ───────────────────────────────────────────────────────
76
+
77
+ function StatusDot({ health }: { health: HealthResult | null }) {
78
+ const isOk = health?.status === "ok";
79
+ return (
80
+ <div className="flex items-center gap-1.5">
81
+ <div
82
+ className={`w-1.5 h-1.5 rounded-full ${isOk ? "animate-pulse" : ""}`}
83
+ style={{ background: isOk ? "var(--accent-green)" : "var(--accent-yellow)" }}
84
+ />
85
+ <span className="text-[10px]" style={{ color: "var(--text-muted)", fontFamily: "var(--font-mono)" }}>
86
+ {isOk ? `SQLFluff ${health!.version}` : "starting…"}
87
+ </span>
88
+ </div>
89
+ );
90
+ }
91
+
92
+ // ── Main page ──────────────────────────────────────────────────────────────
93
+
94
+ export default function Analyzer() {
95
+ const [sql, setSql] = useState(DEFAULT_SQL);
96
+ const [dialect, setDialect] = useState("ansi");
97
+ const [activeTab, setActiveTab] = useState<AnalysisTab>("ast");
98
+ const [isAnalyzing, setIsAnalyzing] = useState(false);
99
+
100
+ const [health, setHealth] = useState<HealthResult | null>(null);
101
+ const [lintResult, setLintResult] = useState<LintResult | null>(null);
102
+ const [parseResult, setParseResult] = useState<ParseResult | null>(null);
103
+ const [formatResult, setFormatResult] = useState<FormatResult | null>(null);
104
+ const [injectResult, setInjectResult] = useState<InjectionResult | null>(null);
105
+ const [analysisTime, setAnalysisTime] = useState<number | null>(null);
106
+
107
+ const editorContainerRef = useRef<HTMLDivElement>(null);
108
+
109
+ // Poll health endpoint every 6 s until ready
110
+ useEffect(() => {
111
+ let cancelled = false;
112
+ async function poll() {
113
+ try {
114
+ const h = await api.health();
115
+ if (!cancelled) setHealth(h);
116
+ } catch {
117
+ // still starting
118
+ }
119
+ }
120
+ poll();
121
+ const id = setInterval(poll, 6000);
122
+ return () => { cancelled = true; clearInterval(id); };
123
+ }, []);
124
+
125
+ // ── Analyze ────────────────────────────────────────────────────────────
126
+
127
+ const handleAnalyze = useCallback(async () => {
128
+ if (!sql.trim()) { toast.error("Please enter some SQL to analyze"); return; }
129
+ if (health?.status !== "ok") { toast.error("API is not ready yet — please wait a moment"); return; }
130
+
131
+ setIsAnalyzing(true);
132
+ const start = Date.now();
133
+
134
+ try {
135
+ const [lint, parse, format, inject] = await Promise.allSettled([
136
+ api.lint(sql, dialect),
137
+ api.parse(sql, dialect),
138
+ api.format(sql, dialect),
139
+ api.inject(sql, dialect),
140
+ ]);
141
+
142
+ if (lint.status === "fulfilled") setLintResult(lint.value);
143
+ if (parse.status === "fulfilled") setParseResult(parse.value);
144
+ if (format.status === "fulfilled") setFormatResult(format.value);
145
+ if (inject.status === "fulfilled") setInjectResult(inject.value);
146
+
147
+ setAnalysisTime(Date.now() - start);
148
+
149
+ const lv = lint.status === "fulfilled" ? lint.value : null;
150
+ const iv = inject.status === "fulfilled" ? inject.value : null;
151
+ if (lv && iv) {
152
+ if (!lv.passed || !iv.safe) {
153
+ toast.warning(
154
+ `Analysis complete — ${lv.stats.total} lint issue${lv.stats.total !== 1 ? "s" : ""}${!iv.safe ? `, risk score ${iv.risk_score}/100` : ""}`,
155
+ { duration: 4000 }
156
+ );
157
+ } else {
158
+ toast.success("Analysis complete — no issues found", { duration: 3000 });
159
+ }
160
+ }
161
+ } catch (err) {
162
+ toast.error((err instanceof Error ? err.message : "Analysis failed").slice(0, 120));
163
+ } finally {
164
+ setIsAnalyzing(false);
165
+ }
166
+ }, [sql, dialect, health]);
167
+
168
+ // ── Jump to line ───────────────────────────────────────────────────────
169
+
170
+ const jumpToLine = useCallback((lineNo: number) => {
171
+ const container = editorContainerRef.current;
172
+ if (!container) return;
173
+ const editorEl = container.querySelector("[data-editor-container]");
174
+ if (!editorEl) return;
175
+ editorEl.dispatchEvent(new CustomEvent("jump-to-line", { detail: lineNo }));
176
+ }, []);
177
+
178
+ const lintBadge = lintResult && !lintResult.passed ? lintResult.stats.total : null;
179
+ const injectBadge = injectResult && !injectResult.safe ? injectResult.patterns.length : null;
180
+
181
+ // ── Render ─────────────────────────────────────────────────────────────
182
+
183
+ return (
184
+ <div className="flex flex-col h-screen" style={{ background: "var(--bg-base)", color: "var(--text-primary)" }}>
185
+ {/* Header */}
186
+ <header
187
+ className="flex items-center gap-3 px-4 py-2.5 flex-shrink-0"
188
+ style={{ borderBottom: "1px solid var(--border)", background: "var(--bg-surface)" }}
189
+ >
190
+ {/* Logo */}
191
+ <div className="flex items-center gap-2 mr-2">
192
+ <div
193
+ className="w-7 h-7 rounded flex items-center justify-center"
194
+ style={{ background: "rgba(88,166,255,0.12)", border: "1px solid rgba(88,166,255,0.25)" }}
195
+ >
196
+ <Database size={14} style={{ color: "var(--accent-blue)" }} />
197
+ </div>
198
+ <span className="text-sm font-semibold tracking-tight">SQL Analyzer</span>
199
+ </div>
200
+
201
+ {/* Dialect selector */}
202
+ <select
203
+ value={dialect}
204
+ onChange={(e) => setDialect(e.target.value)}
205
+ className="h-7 text-xs rounded px-2 pr-6 appearance-none cursor-pointer"
206
+ style={{
207
+ background: "var(--bg-elevated)",
208
+ border: "1px solid var(--border)",
209
+ color: "var(--text-primary)",
210
+ fontFamily: "var(--font-mono)",
211
+ outline: "none",
212
+ }}
213
+ >
214
+ {DIALECTS.map((d) => (
215
+ <option key={d.value} value={d.value}>{d.label}</option>
216
+ ))}
217
+ </select>
218
+
219
+ {/* Example loader */}
220
+ <select
221
+ defaultValue=""
222
+ onChange={(e) => {
223
+ if (e.target.value === "demo") setSql(DEFAULT_SQL);
224
+ else if (e.target.value === "injection") setSql(INJECTION_SQL);
225
+ e.target.value = "";
226
+ }}
227
+ className="h-7 text-xs rounded px-2 pr-6 appearance-none cursor-pointer"
228
+ style={{
229
+ background: "var(--bg-elevated)",
230
+ border: "1px solid var(--border)",
231
+ color: "var(--text-secondary)",
232
+ outline: "none",
233
+ }}
234
+ >
235
+ <option value="" disabled>Examples…</option>
236
+ <option value="demo">Demo Query</option>
237
+ <option value="injection">Injection Demo</option>
238
+ </select>
239
+
240
+ <div className="flex-1" />
241
+
242
+ <StatusDot health={health} />
243
+
244
+ {analysisTime !== null && (
245
+ <span className="text-[10px]" style={{ color: "var(--text-muted)", fontFamily: "var(--font-mono)" }}>
246
+ {analysisTime}ms
247
+ </span>
248
+ )}
249
+
250
+ {/* API Docs link */}
251
+ <Link href="/swagger">
252
+ <button
253
+ className="flex items-center gap-1.5 h-7 px-2 text-xs rounded transition-colors"
254
+ style={{ color: "var(--text-secondary)" }}
255
+ onMouseEnter={(e) => (e.currentTarget.style.color = "var(--text-primary)")}
256
+ onMouseLeave={(e) => (e.currentTarget.style.color = "var(--text-secondary)")}
257
+ >
258
+ <BookOpen size={11} />
259
+ API Docs
260
+ </button>
261
+ </Link>
262
+
263
+ {/* Analyze button */}
264
+ <button
265
+ onClick={handleAnalyze}
266
+ disabled={isAnalyzing || health?.status !== "ok"}
267
+ className="flex items-center gap-1.5 h-7 px-3 text-xs font-semibold rounded transition-opacity disabled:opacity-40"
268
+ style={{ background: "var(--accent-blue)", color: "var(--bg-base)" }}
269
+ >
270
+ {isAnalyzing ? <Loader2 size={12} className="animate-spin" /> : <Play size={11} />}
271
+ {isAnalyzing ? "Analyzing…" : "Analyze"}
272
+ <span className="text-[9px] opacity-60 ml-0.5">⌘↵</span>
273
+ </button>
274
+ </header>
275
+
276
+ {/* Split panels */}
277
+ <div className="flex-1 overflow-hidden">
278
+ <PanelGroup direction="horizontal" className="h-full">
279
+ {/* Left: SQL Editor */}
280
+ <Panel defaultSize={50} minSize={20}>
281
+ <div className="flex flex-col h-full">
282
+ <div
283
+ className="flex items-center gap-2 px-3 py-1.5 flex-shrink-0"
284
+ style={{ borderBottom: "1px solid var(--border-muted)" }}
285
+ >
286
+ <span className="text-[10px]" style={{ color: "var(--text-muted)", fontFamily: "var(--font-mono)" }}>
287
+ SQL Editor
288
+ </span>
289
+ <span className="ml-auto text-[10px]" style={{ color: "var(--text-muted)", fontFamily: "var(--font-mono)" }}>
290
+ Ctrl+Enter to analyze
291
+ </span>
292
+ </div>
293
+ <div ref={editorContainerRef} className="flex-1 overflow-hidden">
294
+ <SqlEditor value={sql} onChange={setSql} onAnalyze={handleAnalyze} dialect={dialect} />
295
+ </div>
296
+ </div>
297
+ </Panel>
298
+
299
+ {/* Resize handle */}
300
+ <PanelResizeHandle
301
+ className="w-1 cursor-col-resize transition-colors"
302
+ style={{ background: "var(--border)" }}
303
+ />
304
+
305
+ {/* Right: Results */}
306
+ <Panel defaultSize={50} minSize={20}>
307
+ <div className="flex flex-col h-full">
308
+ {/* Tab bar */}
309
+ <div
310
+ className="flex items-center flex-shrink-0 px-1"
311
+ style={{ borderBottom: "1px solid var(--border)" }}
312
+ >
313
+ {TABS.map((tab) => {
314
+ const badge = tab.id === "lint" ? lintBadge : tab.id === "injection" ? injectBadge : null;
315
+ const isActive = activeTab === tab.id;
316
+ return (
317
+ <button
318
+ key={tab.id}
319
+ onClick={() => setActiveTab(tab.id)}
320
+ className="relative flex items-center gap-1.5 px-3 py-2 text-xs font-medium transition-colors border-b-2"
321
+ style={{
322
+ color: isActive ? "var(--accent-blue)" : "var(--text-secondary)",
323
+ borderBottomColor: isActive ? "var(--accent-blue)" : "transparent",
324
+ }}
325
+ >
326
+ {tab.label}
327
+ {badge !== null && (
328
+ <span
329
+ className="text-[9px] font-bold px-1 py-0.5 rounded-full min-w-[16px] text-center"
330
+ style={{ background: "rgba(255,123,114,0.2)", color: "var(--accent-red)" }}
331
+ >
332
+ {badge}
333
+ </span>
334
+ )}
335
+ </button>
336
+ );
337
+ })}
338
+ </div>
339
+
340
+ {/* Tab content */}
341
+ <div className="flex-1 overflow-hidden">
342
+ {activeTab === "ast" && (
343
+ <AstTreeView
344
+ tree={parseResult?.tree ?? null}
345
+ tokenCount={parseResult?.token_count}
346
+ depth={parseResult?.depth}
347
+ onJumpToLine={jumpToLine}
348
+ />
349
+ )}
350
+ {activeTab === "lint" && (
351
+ <LintPanel result={lintResult} onJumpToLine={jumpToLine} />
352
+ )}
353
+ {activeTab === "injection" && (
354
+ <InjectionPanel result={injectResult} onJumpToLine={jumpToLine} />
355
+ )}
356
+ {activeTab === "formatted" && (
357
+ <FormatterPanel result={formatResult} onApplyToEditor={setSql} />
358
+ )}
359
+ </div>
360
+ </div>
361
+ </Panel>
362
+ </PanelGroup>
363
+ </div>
364
+ </div>
365
+ );
366
+ }
frontend/src/pages/NotFound.tsx ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Link } from "wouter";
2
+
3
+ export default function NotFound() {
4
+ return (
5
+ <div className="flex flex-col items-center justify-center h-screen gap-4" style={{ background: "var(--bg-base)", color: "var(--text-primary)" }}>
6
+ <span style={{ fontSize: 64, opacity: 0.2 }}>/</span>
7
+ <h1 className="text-2xl font-semibold">404 — Page not found</h1>
8
+ <Link href="/" className="text-sm" style={{ color: "var(--accent-blue)" }}>
9
+ ← Back to Analyzer
10
+ </Link>
11
+ </div>
12
+ );
13
+ }
frontend/src/pages/SwaggerPage.tsx ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useRef, useState } from "react";
2
+ import { Link } from "wouter";
3
+ import { ArrowLeft, Loader2 } from "lucide-react";
4
+ // @ts-expect-error no types
5
+ import SwaggerUIBundle from "swagger-ui-dist/swagger-ui-bundle.js";
6
+ import "swagger-ui-dist/swagger-ui.css";
7
+
8
+ export default function SwaggerPage() {
9
+ const containerRef = useRef<HTMLDivElement>(null);
10
+ const [loading, setLoading] = useState(true);
11
+ const [error, setError] = useState<string | null>(null);
12
+
13
+ useEffect(() => {
14
+ if (!containerRef.current) return;
15
+
16
+ // Determine the OpenAPI JSON URL — same origin as the page
17
+ const base = (import.meta.env.VITE_API_BASE as string | undefined) ?? "";
18
+ const url = `${base}/openapi.json`;
19
+
20
+ try {
21
+ SwaggerUIBundle({
22
+ url,
23
+ domNode: containerRef.current,
24
+ presets: [SwaggerUIBundle.presets.apis, SwaggerUIBundle.SwaggerUIStandalonePreset],
25
+ layout: "BaseLayout",
26
+ deepLinking: true,
27
+ displayRequestDuration: true,
28
+ filter: true,
29
+ tryItOutEnabled: true,
30
+ onComplete: () => setLoading(false),
31
+ });
32
+ setLoading(false);
33
+ } catch (e) {
34
+ setError(String(e));
35
+ setLoading(false);
36
+ }
37
+ }, []);
38
+
39
+ return (
40
+ <div className="flex flex-col h-screen" style={{ background: "var(--bg-base)", color: "var(--text-primary)" }}>
41
+ {/* Header */}
42
+ <header
43
+ className="flex items-center gap-3 px-4 py-2.5 flex-shrink-0"
44
+ style={{ borderBottom: "1px solid var(--border)", background: "var(--bg-surface)" }}
45
+ >
46
+ <Link href="/">
47
+ <button
48
+ className="flex items-center gap-1.5 h-7 px-2 text-xs rounded transition-colors"
49
+ style={{ color: "var(--text-secondary)" }}
50
+ onMouseEnter={(e) => (e.currentTarget.style.color = "var(--text-primary)")}
51
+ onMouseLeave={(e) => (e.currentTarget.style.color = "var(--text-secondary)")}
52
+ >
53
+ <ArrowLeft size={12} />
54
+ Back to Analyzer
55
+ </button>
56
+ </Link>
57
+ <span className="text-sm font-semibold">API Documentation</span>
58
+ <span
59
+ className="text-[10px] px-1.5 py-0.5 rounded"
60
+ style={{
61
+ background: "rgba(88,166,255,0.12)",
62
+ color: "var(--accent-blue)",
63
+ border: "1px solid rgba(88,166,255,0.25)",
64
+ fontFamily: "var(--font-mono)",
65
+ }}
66
+ >
67
+ OpenAPI 3.1
68
+ </span>
69
+ </header>
70
+
71
+ {/* Swagger UI */}
72
+ <div className="flex-1 overflow-auto" style={{ background: "#fff" }}>
73
+ {loading && (
74
+ <div className="flex items-center justify-center h-full gap-2" style={{ color: "#666" }}>
75
+ <Loader2 size={16} className="animate-spin" />
76
+ <span className="text-sm">Loading API docs…</span>
77
+ </div>
78
+ )}
79
+ {error && (
80
+ <div className="flex items-center justify-center h-full">
81
+ <p className="text-sm text-red-600">Failed to load API docs: {error}</p>
82
+ </div>
83
+ )}
84
+ <div ref={containerRef} className={loading ? "hidden" : ""} />
85
+ </div>
86
+ </div>
87
+ );
88
+ }
frontend/tsconfig.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
6
+ "types": ["vite/client"],
7
+ "module": "ESNext",
8
+ "skipLibCheck": true,
9
+ "moduleResolution": "bundler",
10
+ "allowImportingTsExtensions": true,
11
+ "isolatedModules": true,
12
+ "moduleDetection": "force",
13
+ "noEmit": true,
14
+ "jsx": "react-jsx",
15
+ "strict": true,
16
+ "noUnusedLocals": false,
17
+ "noUnusedParameters": false,
18
+ "noFallthroughCasesInSwitch": true,
19
+ "baseUrl": ".",
20
+ "paths": {
21
+ "@/*": ["./src/*"]
22
+ }
23
+ },
24
+ "include": ["src"]
25
+ }
frontend/vite.config.ts ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from "vite";
2
+ import react from "@vitejs/plugin-react";
3
+ import tailwindcss from "@tailwindcss/vite";
4
+ import path from "path";
5
+
6
+ export default defineConfig({
7
+ plugins: [react(), tailwindcss()],
8
+ resolve: {
9
+ alias: {
10
+ "@": path.resolve(__dirname, "./src"),
11
+ },
12
+ },
13
+ build: {
14
+ // Output directly into api/static so FastAPI serves it
15
+ outDir: "../api/static",
16
+ emptyOutDir: true,
17
+ },
18
+ server: {
19
+ port: 5173,
20
+ proxy: {
21
+ // In dev, proxy /api and /docs to the FastAPI server
22
+ "/api": "http://localhost:7860",
23
+ "/docs": "http://localhost:7860",
24
+ "/openapi.json": "http://localhost:7860",
25
+ },
26
+ },
27
+ });
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ sqlfluff>=4.0.0
2
+ fastapi>=0.110.0
3
+ uvicorn[standard]>=0.29.0
4
+ python-multipart>=0.0.9