diff --git a/.dockerignore b/.dockerignore index a4c22c7..e62c92b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,13 +1,17 @@ .git .github .venv +.vscode .env .ruff_cache .pytest_cache .mypy_cache +.cursor __pycache__ *.pyc data/ *.egg-info dist/ build/ +frontend/node_modules +frontend/dist diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 6779d05..30bdcc6 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -102,9 +102,9 @@ jobs: echo "Checking container status..." if docker compose -f docker-compose.yml ps | grep -q "Up"; then - echo "✅ Containers are running" + echo "Containers are running" else - echo "❌ Containers are not running" + echo "Containers are not running" docker compose -f docker-compose.yml logs --tail=20 exit 1 fi diff --git a/.gitignore b/.gitignore index 07abd31..499f9f3 100644 --- a/.gitignore +++ b/.gitignore @@ -14,8 +14,8 @@ dist/ downloads/ eggs/ .eggs/ -lib/ -lib64/ +/lib/ +/lib64/ parts/ sdist/ var/ @@ -200,6 +200,7 @@ cython_debug/ # refer to https://docs.cursor.com/context/ignore-files .cursorignore .cursorindexingignore +.cursor/skills/ # Marimo marimo/_static/ diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..3b4604b --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,33 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Backend (FastAPI)", + "type": "python", + "request": "launch", + "module": "uvicorn", + "args": ["flow.api.app:app", "--reload", "--host", "0.0.0.0", "--port", "8000"], + "env": { + "PYTHONPATH": "${workspaceFolder}/src" + }, + "cwd": "${workspaceFolder}", + "console": "integratedTerminal" + }, + { + "name": "Frontend (Vite)", + "type": "node", + "request": "launch", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "dev"], + "cwd": "${workspaceFolder}/frontend", + "console": "integratedTerminal" + } + ], + "compounds": [ + { + "name": "Full Stack (Backend + Frontend)", + "configurations": ["Backend (FastAPI)", "Frontend (Vite)"], + "stopAll": true + } + ] +} diff --git a/Dockerfile b/Dockerfile index 031593b..247ea0a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,13 @@ +FROM node:22-slim AS frontend + +WORKDIR /app/frontend + +COPY frontend/package.json frontend/package-lock.json ./ +RUN npm ci + +COPY frontend/ ./ +RUN npm run build + FROM python:3.11-slim AS builder WORKDIR /app @@ -18,6 +28,7 @@ WORKDIR /app COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages COPY --from=builder /usr/local/bin/flow /usr/local/bin/flow COPY --from=builder /usr/local/bin/uvicorn /usr/local/bin/uvicorn +COPY --from=frontend /app/frontend/dist /app/static ENV FLOW_DATA_DIR=/data diff --git a/README.md b/README.md index 3c6683b..9acc16f 100644 --- a/README.md +++ b/README.md @@ -1 +1,162 @@ -# flow \ No newline at end of file +# Flow + +Stock market data platform with OHLCV ingestion, technical charting, and a professional dark-themed web interface. + +## Architecture + +``` +┌─────────────────┐ ┌──────────────────┐ ┌────────────────┐ +│ React Frontend │──────▶│ FastAPI Backend │──────▶│ Parquet + DuckDB│ +│ (Vite + TW) │ /api │ (uvicorn) │ │ (data/equity) │ +└─────────────────┘ └──────────────────┘ └────────────────┘ + │ + ▼ + ┌──────────────────┐ + │ OpenBB / yfinance │ + │ (data provider) │ + └──────────────────┘ +``` + +**Backend** — FastAPI serving OHLCV price data, ticker management, and summary statistics. Data is ingested from yfinance via OpenBB, stored in Parquet files with Hive-style partitioning, and queried through DuckDB. + +**Frontend** — React SPA with candlestick charting (Lightweight Charts), client-side technical indicators, and a dark fintech UI. Communicates with the backend via REST API. + +## Quick Start + +### Prerequisites + +- Python 3.11+ +- Node.js 18+ +- [uv](https://docs.astral.sh/uv/) (recommended) or pip + +### Backend + +```bash +# Install dependencies +uv sync + +# Add a ticker and backfill historical data +uv run flow add AAPL +uv run flow add MSFT + +# Start the API server +uv run flow serve +``` + +The API runs at `http://localhost:8000`. See [API Endpoints](#api-endpoints) below. + +### Frontend + +```bash +cd frontend +npm install +npm run dev +``` + +Opens at `http://localhost:5173` with automatic proxy to the backend. + +### Docker + +```bash +cp .env.example .env +docker compose up -d +``` + +## API Endpoints + +| Method | Endpoint | Description | +|--------|----------|-------------| +| `GET` | `/health` | Health check | +| `GET` | `/api/tickers` | List registered tickers | +| `POST` | `/api/tickers` | Add ticker (triggers backfill) | +| `DELETE` | `/api/tickers/{symbol}` | Remove ticker and its data | +| `GET` | `/api/equity/{symbol}/prices` | OHLCV price data (`?start=&end=`) | +| `GET` | `/api/equity/{symbol}/summary` | 52w high/low, avg volume, latest close | +| `POST` | `/api/ingest` | Run incremental ingest for all tickers | + +## Frontend Pages + +| Page | Route | Description | +|------|-------|-------------| +| Dashboard | `/` | Ticker card grid with sparkline charts and summary stats | +| Chart | `/chart/:symbol` | Candlestick chart with volume, SMA/EMA/Bollinger overlays, RSI/MACD sub-panels | +| Watchlist | `/watchlist` | Sortable data table of all tracked tickers | +| Portfolio | `/portfolio` | Coming soon | +| Screener | `/screener` | Coming soon | + +## Tech Stack + +### Backend + +| Component | Technology | +|-----------|-----------| +| API | FastAPI + Uvicorn | +| Data ingestion | OpenBB + yfinance | +| Storage | Parquet (PyArrow) | +| Query engine | DuckDB | +| CLI | Typer | +| Config | Pydantic Settings | + +### Frontend + +| Component | Technology | +|-----------|-----------| +| Framework | React 19 + TypeScript + Vite | +| Styling | Tailwind CSS v4 (dark theme) | +| Charting | Lightweight Charts (TradingView) | +| Indicators | technicalindicators (client-side) | +| Server state | TanStack React Query v5 | +| Client state | Zustand | +| Routing | React Router v7 | +| Icons | Lucide React | + +## CLI Reference + +```bash +flow add # Add ticker and backfill from 2005 +flow remove # Remove ticker and its data +flow list # List registered tickers +flow backfill # Re-backfill a ticker +flow ingest # Incremental ingest for all tickers +flow serve # Start the API server +``` + +## Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `PORT` | `8510` | Host port mapping (Docker) | +| `FLOW_DATA_DIR` | `data` | Directory for Parquet files and ticker registry | +| `FLOW_DEFAULT_PROVIDER` | `yfinance` | Data provider | +| `FLOW_BACKFILL_START_DATE` | `2005-01-01` | How far back to fetch on first add | +| `FLOW_OVERLAP_DAYS` | `5` | Days of overlap during incremental ingest | + +## Project Structure + +``` +flow/ +├── src/flow/ +│ ├── api/ # FastAPI app and routes +│ ├── ingestion/ # Data fetching and pipeline +│ ├── storage/ # Parquet writing and DuckDB queries +│ ├── cli.py # Typer CLI +│ ├── config.py # Pydantic settings +│ └── models.py # Request/response models +├── frontend/ +│ └── src/ +│ ├── api/ # Axios client +│ ├── components/ # Chart, layout, common, ticker components +│ ├── hooks/ # React Query hooks +│ ├── lib/ # Indicators, formatters, constants +│ ├── pages/ # Dashboard, Chart, Watchlist, Portfolio, Screener +│ ├── stores/ # Zustand UI store +│ └── types/ # TypeScript interfaces +├── tests/ # pytest test suite +├── docker-compose.yml +├── Dockerfile +└── pyproject.toml +``` + +## License + +Private. diff --git a/docs/plans/2026-03-09-frontend-design.md b/docs/plans/2026-03-09-frontend-design.md new file mode 100644 index 0000000..3590020 --- /dev/null +++ b/docs/plans/2026-03-09-frontend-design.md @@ -0,0 +1,299 @@ +# Flow Frontend Design + +## Overview + +A professional stock market charting web application built with React, Vite, and Tailwind CSS. The frontend consumes the existing Flow FastAPI backend to display OHLCV price data, technical indicators, and ticker management across five pages: Dashboard, Charting, Watchlist, Portfolio (placeholder), and Screener (placeholder). + +## Decisions + +| Decision | Choice | Rationale | +|----------|--------|-----------| +| Charting library | Lightweight Charts (TradingView) | Open-source, ~40KB, professional candlestick rendering, full data control | +| Technical indicators | Client-side (`technicalindicators`) | No backend changes needed, instant parameter tweaking | +| Portfolio page | Placeholder | Defer to focus on core charting/watchlist features | +| Screener page | Placeholder | Defer to focus on core features | +| Visual direction | Dark-first polished/modern | Standard for trading platforms, optimized for data density | +| Architecture | TanStack Query + Zustand | Query handles server state caching/dedup; Zustand for UI-only state | + +## Tech Stack + +| Layer | Choice | +|-------|--------| +| Framework | React 19 + Vite | +| Styling | Tailwind CSS v4 | +| Routing | React Router v7 | +| Server State | TanStack React Query v5 | +| Client State | Zustand | +| Charts | Lightweight Charts (TradingView) | +| Indicators | `technicalindicators` (client-side) | +| Icons | Lucide React | +| HTTP | Axios | + +## Project Structure + +``` +frontend/ +├── index.html +├── vite.config.ts +├── tailwind.config.ts +├── tsconfig.json +├── package.json +├── public/ +├── src/ +│ ├── main.tsx +│ ├── App.tsx +│ ├── api/ +│ │ └── client.ts +│ ├── hooks/ +│ │ ├── usePrices.ts +│ │ ├── useSummary.ts +│ │ ├── useTickers.ts +│ │ └── useIndicators.ts +│ ├── stores/ +│ │ └── uiStore.ts +│ ├── components/ +│ │ ├── layout/ +│ │ │ ├── Sidebar.tsx +│ │ │ ├── TopBar.tsx +│ │ │ └── AppLayout.tsx +│ │ ├── chart/ +│ │ │ ├── CandlestickChart.tsx +│ │ │ ├── VolumeChart.tsx +│ │ │ └── IndicatorOverlay.tsx +│ │ ├── common/ +│ │ │ ├── Card.tsx +│ │ │ ├── Skeleton.tsx +│ │ │ ├── Badge.tsx +│ │ │ └── SearchInput.tsx +│ │ └── ticker/ +│ │ ├── TickerRow.tsx +│ │ ├── TickerSummary.tsx +│ │ └── PriceChange.tsx +│ ├── pages/ +│ │ ├── Dashboard.tsx +│ │ ├── Chart.tsx +│ │ ├── Watchlist.tsx +│ │ ├── Portfolio.tsx +│ │ └── Screener.tsx +│ ├── lib/ +│ │ ├── indicators.ts +│ │ ├── formatters.ts +│ │ └── constants.ts +│ └── types/ +│ └── index.ts +``` + +The `frontend/` directory lives at the repo root alongside the existing Python `src/` backend. + +## Design System + +### Color Palette + +| Token | Hex | Usage | +|-------|-----|-------| +| `bg-primary` | `#020617` (slate-950) | Main background | +| `bg-secondary` | `#0F172A` (slate-900) | Cards, sidebar | +| `bg-tertiary` | `#1E293B` (slate-800) | Hover states, inputs, elevated cards | +| `text-primary` | `#F8FAFC` (slate-50) | Headings, primary text | +| `text-secondary` | `#94A3B8` (slate-400) | Muted text, labels | +| `border` | `#334155` (slate-700) | Card borders, dividers | +| `accent` | `#3B82F6` (blue-500) | Active nav, links, primary actions | +| `bullish` | `#22C55E` (green-500) | Price up, positive P&L | +| `bearish` | `#EF4444` (red-500) | Price down, negative P&L | +| `volume` | `#3B82F6` at 40% opacity | Volume bars | + +### Typography + +- **Headings & UI**: Inter -- clean, professional, excellent readability +- **Data & Numbers**: Fira Code (monospace) -- aligned decimal columns, price tickers +- Weights: 400 (body), 500 (labels), 600 (headings), 700 (key metrics) + +### Component Styling + +- **Cards**: `bg-slate-900 border border-slate-700 rounded-xl` with `hover:border-slate-600 transition-colors duration-200` +- **Sidebar**: Fixed left, `w-64`, `bg-slate-900/95 backdrop-blur-sm border-r border-slate-700` +- **Inputs**: `bg-slate-800 border-slate-700 focus:border-blue-500 focus:ring-1 focus:ring-blue-500/50` +- **Skeleton loading**: `bg-slate-800 animate-pulse rounded` + +### Chart Colors + +| Element | Color | +|---------|-------| +| Candlestick bullish | `#22C55E`, wick `#16A34A` | +| Candlestick bearish | `#EF4444`, wick `#DC2626` | +| Chart background | `#020617` | +| Grid lines | `#1E293B` | +| Crosshair | `#94A3B8` at 50% | +| SMA overlay | `#3B82F6` (blue) | +| EMA overlay | `#F59E0B` (amber) | +| Bollinger overlay | `#8B5CF6` (violet) | + +### UX Rules + +- No emojis as icons -- Lucide React SVGs throughout +- `cursor-pointer` on all clickable elements +- Skeleton screens during loading (no blank states) +- Active nav: accent color + left border indicator +- All numbers in monospace font for alignment +- Price changes show color + directional arrow icon (not color alone) +- Transitions: `transition-colors duration-200` on all interactive elements + +## Layout & Navigation + +### App Shell + +Fixed sidebar (left) + top bar (top) layout. + +**TopBar** (h-14): +- Left: "Flow" logo text (Inter 700, accent blue) +- Center: Global ticker search (type to search registered tickers, click navigates to chart) +- Right: "Ingest" button (triggers `POST /api/ingest`), health status indicator (green dot) + +**Sidebar** (w-64, fixed): +- Nav items: icon + label, stacked vertically +- Active: `bg-slate-800 text-blue-500 border-l-2 border-blue-500` +- Inactive: `text-slate-400 hover:text-slate-200 hover:bg-slate-800/50` +- Collapsible: icon-only (`w-16`) at `< 1024px`; hidden with hamburger at `< 768px` + +### Responsive Behavior + +| Breakpoint | Sidebar | Content | +|------------|---------|---------| +| >= 1440px | Full (w-64) | Spacious | +| >= 1024px | Full (w-64) | Standard | +| >= 768px | Collapsed (w-16, icons only) | Full width | +| < 768px | Hidden, hamburger overlay | Full width | + +### Routes + +| Path | Page | +|------|------| +| `/` | Dashboard | +| `/chart/:symbol?` | Chart | +| `/watchlist` | Watchlist | +| `/portfolio` | Portfolio (placeholder) | +| `/screener` | Screener (placeholder) | + +## Pages + +### Dashboard + +Market overview showing all registered tickers as a card grid. + +**Ticker Card** (data from `/api/equity/{symbol}/summary`): +- Symbol (bold) +- Latest close price (large, monospace) +- Daily change percent (green/red + arrow) +- Mini sparkline: last 30 days closing prices (Lightweight Charts area, accent blue, no axes) +- 30d avg volume (formatted: 52.3M) +- 52-week range (low -- high) +- Clicking navigates to `/chart/{symbol}` + +**Grid**: 4 cols at 1440px, 3 at 1024px, 2 at 768px, 1 below. + +**Add Ticker**: Button opens modal with symbol input. Calls `POST /api/tickers`. Shows loading during backfill. + +**States**: Skeleton cards (loading), "No tickers yet" + add button (empty), error card with retry. + +### Charting + +Full-featured candlestick chart with indicators. + +**Toolbar**: +- Symbol selector dropdown (registered tickers, also via URL param) +- Time range buttons: 1M, 3M, 6M, 1Y, 5Y, ALL (filters `start` param) +- Indicators toggle dropdown + +**Chart Area**: +- Candlestick chart via Lightweight Charts, fills available width, min height ~400px +- Volume bars in bottom pane (20% of chart height) +- Crosshair with OHLCV tooltip on hover +- Mouse scroll zoom, click-drag pan, double-click auto-fit + +**Symbol Header**: +- Symbol + latest close (large monospace) + change (colored + arrow) +- 52-week range as visual progress bar + +**Overlay Indicators** (on price chart): + +| Indicator | Default Params | Color | +|-----------|---------------|-------| +| SMA | 20, 50, 200 | Blue, Amber, Violet | +| EMA | 12, 26 | Cyan, Orange | +| Bollinger Bands | 20 period, 2 std | Gray with fill | + +**Sub-chart Indicators** (separate panels below): + +| Indicator | Default Params | Rendering | +|-----------|---------------|-----------| +| RSI | 14 period | Line with 30/70 dashed bands | +| MACD | 12, 26, 9 | Two lines + histogram bars | + +Indicator toggle and parameter editing via Indicators dropdown. State persists in Zustand across symbol changes. + +**States**: Prompt to select ticker (no symbol), skeleton (loading), error with retry, "no data" message. + +### Watchlist + +Dense sortable table of all registered tickers. + +**Columns**: Symbol (clickable, links to chart), Last price, Change %, 30d Avg Volume, 52w Range (progress bar), Remove button. + +**Behaviors**: +- Sortable columns (click header, asc/desc toggle) +- Row hover highlight +- Remove: X button with confirmation tooltip, calls `DELETE /api/tickers/{symbol}` +- Add ticker: Same modal as Dashboard + +**States**: Skeleton rows (loading), "watchlist is empty" + add button (empty), error banner with retry. + +### Portfolio (Placeholder) + +Centered card with Briefcase icon, "Portfolio Coming Soon" heading, short description. Styled to match dark theme. + +### Screener (Placeholder) + +Centered card with SlidersHorizontal icon, "Screener Coming Soon" heading, short description. Styled to match dark theme. + +## Data Layer + +### API Client + +Axios instance with `baseURL` from `VITE_API_URL` (defaults to `/api`). Response interceptors for error normalization. TypeScript types matching backend Pydantic models. + +### React Query Hooks + +| Hook | Endpoint | Cache Key | Stale Time | +|------|----------|-----------|------------| +| `useTickers` | `GET /api/tickers` | `['tickers']` | 30s | +| `usePrices(symbol, start?, end?)` | `GET /api/equity/{symbol}/prices` | `['prices', symbol, start, end]` | 5 min | +| `useSummary(symbol)` | `GET /api/equity/{symbol}/summary` | `['summary', symbol]` | 60s | +| `useAddTicker` | `POST /api/tickers` | mutation, invalidates `['tickers']` | -- | +| `useRemoveTicker` | `DELETE /api/tickers/{symbol}` | mutation, invalidates `['tickers']` | -- | +| `useIngest` | `POST /api/ingest` | mutation, invalidates `['prices', 'summary']` | -- | +| `useHealth` | `GET /health` | `['health']` | 10s | + +### Zustand Store + +```typescript +interface UIState { + sidebarCollapsed: boolean; + selectedIndicators: IndicatorConfig[]; + chartTimeRange: '1M' | '3M' | '6M' | '1Y' | '5Y' | 'ALL'; + toggleSidebar: () => void; + setIndicators: (indicators: IndicatorConfig[]) => void; + setTimeRange: (range: string) => void; +} +``` + +Only pure UI state -- no server data. + +### Indicator Computation + +Thin wrapper around `technicalindicators` in `lib/indicators.ts`: +- `computeSMA(data, period)`, `computeEMA(data, period)`, `computeRSI(data, period)`, `computeMACD(data, fast, slow, signal)`, `computeBollingerBands(data, period, stdDev)` +- Each returns data in Lightweight Charts format: `{ time, value }[]` + +### Vite Dev Proxy + +Dev server proxies `/api` to `http://localhost:8000` for seamless backend integration during development. diff --git a/docs/plans/2026-03-09-frontend-implementation.md b/docs/plans/2026-03-09-frontend-implementation.md new file mode 100644 index 0000000..1999213 --- /dev/null +++ b/docs/plans/2026-03-09-frontend-implementation.md @@ -0,0 +1,1054 @@ +# Flow Frontend Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Build a professional stock market charting webapp frontend that consumes the existing Flow FastAPI backend. + +**Architecture:** React SPA with TanStack Query for server state management, Zustand for UI state, Lightweight Charts for candlestick rendering, and client-side indicator computation. Dark-first polished design using Tailwind CSS custom theme tokens. + +**Tech Stack:** React 19, Vite, TypeScript, Tailwind CSS v4, TanStack React Query v5, Zustand, Lightweight Charts, technicalindicators, Lucide React, Axios, React Router v7 + +**Design doc:** `docs/plans/2026-03-09-frontend-design.md` + +--- + +### Task 1: Scaffold Vite + React + TypeScript Project + +**Files:** +- Create: `frontend/package.json` +- Create: `frontend/vite.config.ts` +- Create: `frontend/tsconfig.json` +- Create: `frontend/tsconfig.app.json` +- Create: `frontend/tsconfig.node.json` +- Create: `frontend/index.html` +- Create: `frontend/src/main.tsx` +- Create: `frontend/src/App.tsx` +- Create: `frontend/src/vite-env.d.ts` + +**Step 1: Create Vite project** + +```bash +cd /Users/michael/repo/flow +npm create vite@latest frontend -- --template react-ts +``` + +**Step 2: Install core dependencies** + +```bash +cd frontend +npm install @tanstack/react-query axios zustand react-router-dom lightweight-charts lucide-react technicalindicators +npm install -D tailwindcss @tailwindcss/vite @types/node +``` + +**Step 3: Verify it runs** + +```bash +npm run dev +``` + +Expected: Vite dev server starts on `http://localhost:5173`, default React page renders. + +**Step 4: Commit** + +```bash +git add frontend/ +git commit -m "feat: scaffold Vite + React + TypeScript frontend with dependencies" +``` + +--- + +### Task 2: Configure Tailwind CSS + Design System + +**Files:** +- Modify: `frontend/vite.config.ts` +- Modify: `frontend/src/main.tsx` (or create `frontend/src/index.css`) +- Create: `frontend/src/index.css` + +**Step 1: Add Tailwind Vite plugin** + +In `frontend/vite.config.ts`: +```typescript +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import tailwindcss from '@tailwindcss/vite' + +export default defineConfig({ + plugins: [react(), tailwindcss()], + server: { + proxy: { + '/api': 'http://localhost:8000', + '/health': 'http://localhost:8000', + }, + }, +}) +``` + +**Step 2: Create CSS with Tailwind + custom theme** + +In `frontend/src/index.css`: +```css +@import "tailwindcss"; + +@theme { + --font-sans: 'Inter', sans-serif; + --font-mono: 'Fira Code', monospace; + + --color-surface-primary: #020617; + --color-surface-secondary: #0F172A; + --color-surface-tertiary: #1E293B; + --color-border-primary: #334155; + --color-border-hover: #475569; + --color-accent: #3B82F6; + --color-bullish: #22C55E; + --color-bearish: #EF4444; +} +``` + +**Step 3: Import CSS and Google Fonts in index.html** + +In `frontend/index.html`, add to ``: +```html + + + +``` + +In `frontend/src/main.tsx`, ensure `import './index.css'` is present. + +**Step 4: Verify Tailwind works** + +In `App.tsx`, add `
Tailwind works
`. Run `npm run dev`, verify dark background with white text. + +**Step 5: Commit** + +```bash +git add -A +git commit -m "feat: configure Tailwind CSS v4 with dark fintech design tokens" +``` + +--- + +### Task 3: TypeScript Types + +**Files:** +- Create: `frontend/src/types/index.ts` + +**Step 1: Define types matching backend Pydantic models** + +```typescript +export interface OHLCVRow { + date: string; + open: number; + high: number; + low: number; + close: number; + volume: number; + vwap: number; + split_ratio: number; + dividend: number; +} + +export interface TickerInfo { + added: string; + last_ingested: string | null; +} + +export interface TickerResponse { + symbol: string; + info: TickerInfo; +} + +export interface PriceResponse { + symbol: string; + count: number; + data: OHLCVRow[]; +} + +export interface SummaryResponse { + symbol: string; + latest_date: string; + latest_close: number; + high_52w: number; + low_52w: number; + avg_volume_30d: number; + total_rows: number; +} + +export interface IngestResult { + symbol: string; + rows_fetched: number; + rows_written: number; + status: string; +} + +export interface IngestResponse { + results: IngestResult[]; +} + +export interface IndicatorConfig { + type: 'sma' | 'ema' | 'rsi' | 'macd' | 'bollinger'; + params: Record; + enabled: boolean; + color?: string; +} +``` + +**Step 2: Commit** + +```bash +git add frontend/src/types/ +git commit -m "feat: add TypeScript types matching backend API models" +``` + +--- + +### Task 4: API Client + React Query Provider + +**Files:** +- Create: `frontend/src/api/client.ts` +- Modify: `frontend/src/main.tsx` + +**Step 1: Create Axios API client** + +`frontend/src/api/client.ts`: +```typescript +import axios from 'axios'; + +const apiClient = axios.create({ + baseURL: import.meta.env.VITE_API_URL || '', + headers: { 'Content-Type': 'application/json' }, +}); + +apiClient.interceptors.response.use( + (response) => response, + (error) => { + const message = error.response?.data?.detail || error.message || 'An error occurred'; + return Promise.reject(new Error(message)); + } +); + +export default apiClient; +``` + +**Step 2: Wrap app in QueryClientProvider** + +In `frontend/src/main.tsx`: +```typescript +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import './index.css' +import App from './App' + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: 1, + refetchOnWindowFocus: false, + }, + }, +}) + +createRoot(document.getElementById('root')!).render( + + + + + , +) +``` + +**Step 3: Verify app still renders** + +Run `npm run dev`. Page should load without errors. + +**Step 4: Commit** + +```bash +git add frontend/src/api/ frontend/src/main.tsx +git commit -m "feat: add Axios API client and React Query provider" +``` + +--- + +### Task 5: React Query Hooks + +**Files:** +- Create: `frontend/src/hooks/useTickers.ts` +- Create: `frontend/src/hooks/usePrices.ts` +- Create: `frontend/src/hooks/useSummary.ts` +- Create: `frontend/src/hooks/useHealth.ts` + +**Step 1: Create useTickers hook** + +```typescript +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import apiClient from '../api/client'; +import type { TickerResponse } from '../types'; + +export function useTickers() { + return useQuery({ + queryKey: ['tickers'], + queryFn: async (): Promise => { + const { data } = await apiClient.get('/api/tickers'); + return data; + }, + staleTime: 30_000, + }); +} + +export function useAddTicker() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async (symbol: string): Promise => { + const { data } = await apiClient.post('/api/tickers', { symbol }); + return data; + }, + onSuccess: () => qc.invalidateQueries({ queryKey: ['tickers'] }), + }); +} + +export function useRemoveTicker() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async (symbol: string): Promise => { + await apiClient.delete(`/api/tickers/${symbol}`); + }, + onSuccess: () => qc.invalidateQueries({ queryKey: ['tickers'] }), + }); +} +``` + +**Step 2: Create usePrices hook** + +```typescript +import { useQuery } from '@tanstack/react-query'; +import apiClient from '../api/client'; +import type { PriceResponse } from '../types'; + +export function usePrices(symbol: string, start?: string, end?: string) { + return useQuery({ + queryKey: ['prices', symbol, start, end], + queryFn: async (): Promise => { + const params: Record = {}; + if (start) params.start = start; + if (end) params.end = end; + const { data } = await apiClient.get(`/api/equity/${symbol}/prices`, { params }); + return data; + }, + enabled: !!symbol, + staleTime: 5 * 60_000, + }); +} +``` + +**Step 3: Create useSummary hook** + +```typescript +import { useQuery } from '@tanstack/react-query'; +import apiClient from '../api/client'; +import type { SummaryResponse } from '../types'; + +export function useSummary(symbol: string) { + return useQuery({ + queryKey: ['summary', symbol], + queryFn: async (): Promise => { + const { data } = await apiClient.get(`/api/equity/${symbol}/summary`); + return data; + }, + enabled: !!symbol, + staleTime: 60_000, + }); +} +``` + +**Step 4: Create useHealth hook and useIngest mutation** + +`frontend/src/hooks/useHealth.ts`: +```typescript +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import apiClient from '../api/client'; +import type { IngestResponse } from '../types'; + +export function useHealth() { + return useQuery({ + queryKey: ['health'], + queryFn: async () => { + const { data } = await apiClient.get('/health'); + return data; + }, + staleTime: 10_000, + }); +} + +export function useIngest() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async (): Promise => { + const { data } = await apiClient.post('/api/ingest'); + return data; + }, + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['prices'] }); + qc.invalidateQueries({ queryKey: ['summary'] }); + }, + }); +} +``` + +**Step 5: Commit** + +```bash +git add frontend/src/hooks/ +git commit -m "feat: add React Query hooks for all API endpoints" +``` + +--- + +### Task 6: Zustand Store + +**Files:** +- Create: `frontend/src/stores/uiStore.ts` + +**Step 1: Create UI store** + +```typescript +import { create } from 'zustand'; +import type { IndicatorConfig } from '../types'; + +const DEFAULT_INDICATORS: IndicatorConfig[] = [ + { type: 'sma', params: { period: 20 }, enabled: false, color: '#3B82F6' }, + { type: 'sma', params: { period: 50 }, enabled: false, color: '#F59E0B' }, + { type: 'sma', params: { period: 200 }, enabled: false, color: '#8B5CF6' }, + { type: 'ema', params: { period: 12 }, enabled: false, color: '#06B6D4' }, + { type: 'ema', params: { period: 26 }, enabled: false, color: '#F97316' }, + { type: 'rsi', params: { period: 14 }, enabled: false }, + { type: 'macd', params: { fast: 12, slow: 26, signal: 9 }, enabled: false }, + { type: 'bollinger', params: { period: 20, stdDev: 2 }, enabled: false, color: '#6B7280' }, +]; + +interface UIState { + sidebarCollapsed: boolean; + sidebarMobileOpen: boolean; + selectedIndicators: IndicatorConfig[]; + chartTimeRange: '1M' | '3M' | '6M' | '1Y' | '5Y' | 'ALL'; + toggleSidebar: () => void; + setSidebarMobileOpen: (open: boolean) => void; + toggleIndicator: (index: number) => void; + updateIndicatorParams: (index: number, params: Record) => void; + setTimeRange: (range: UIState['chartTimeRange']) => void; +} + +export const useUIStore = create((set) => ({ + sidebarCollapsed: false, + sidebarMobileOpen: false, + selectedIndicators: DEFAULT_INDICATORS, + chartTimeRange: '1Y', + toggleSidebar: () => set((s) => ({ sidebarCollapsed: !s.sidebarCollapsed })), + setSidebarMobileOpen: (open) => set({ sidebarMobileOpen: open }), + toggleIndicator: (index) => + set((s) => { + const indicators = [...s.selectedIndicators]; + indicators[index] = { ...indicators[index], enabled: !indicators[index].enabled }; + return { selectedIndicators: indicators }; + }), + updateIndicatorParams: (index, params) => + set((s) => { + const indicators = [...s.selectedIndicators]; + indicators[index] = { ...indicators[index], params }; + return { selectedIndicators: indicators }; + }), + setTimeRange: (range) => set({ chartTimeRange: range }), +})); +``` + +**Step 2: Commit** + +```bash +git add frontend/src/stores/ +git commit -m "feat: add Zustand UI store for sidebar, indicators, and time range" +``` + +--- + +### Task 7: Utility Functions + +**Files:** +- Create: `frontend/src/lib/constants.ts` +- Create: `frontend/src/lib/formatters.ts` +- Create: `frontend/src/lib/indicators.ts` + +**Step 1: Create constants** + +`frontend/src/lib/constants.ts`: +```typescript +export const CHART_COLORS = { + bullish: '#22C55E', + bullishWick: '#16A34A', + bearish: '#EF4444', + bearishWick: '#DC2626', + volume: 'rgba(59, 130, 246, 0.4)', + background: '#020617', + grid: '#1E293B', + crosshair: 'rgba(148, 163, 184, 0.5)', + sma: ['#3B82F6', '#F59E0B', '#8B5CF6'], + ema: ['#06B6D4', '#F97316'], +} as const; + +export const TIME_RANGES = ['1M', '3M', '6M', '1Y', '5Y', 'ALL'] as const; + +export function getStartDateForRange(range: string): string | undefined { + const now = new Date(); + const map: Record = { + '1M': 30, '3M': 90, '6M': 180, '1Y': 365, '5Y': 1825, + }; + const days = map[range]; + if (!days) return undefined; + const start = new Date(now.getTime() - days * 86400000); + return start.toISOString().split('T')[0]; +} +``` + +**Step 2: Create formatters** + +`frontend/src/lib/formatters.ts`: +```typescript +export function formatPrice(value: number): string { + return value.toLocaleString('en-US', { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }); +} + +export function formatVolume(value: number): string { + if (value >= 1_000_000_000) return `${(value / 1_000_000_000).toFixed(1)}B`; + if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`; + if (value >= 1_000) return `${(value / 1_000).toFixed(1)}K`; + return value.toString(); +} + +export function formatPercent(value: number): string { + const sign = value >= 0 ? '+' : ''; + return `${sign}${value.toFixed(2)}%`; +} + +export function formatDate(dateStr: string): string { + return new Date(dateStr).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + }); +} +``` + +**Step 3: Create indicator computation wrapper** + +`frontend/src/lib/indicators.ts`: +```typescript +import { SMA, EMA, RSI, MACD, BollingerBands } from 'technicalindicators'; + +interface TimeValue { + time: string; + value: number; +} + +export function computeSMA(closes: number[], dates: string[], period: number): TimeValue[] { + const values = SMA.calculate({ period, values: closes }); + const offset = closes.length - values.length; + return values.map((value, i) => ({ time: dates[i + offset], value })); +} + +export function computeEMA(closes: number[], dates: string[], period: number): TimeValue[] { + const values = EMA.calculate({ period, values: closes }); + const offset = closes.length - values.length; + return values.map((value, i) => ({ time: dates[i + offset], value })); +} + +export function computeRSI(closes: number[], dates: string[], period: number): TimeValue[] { + const values = RSI.calculate({ period, values: closes }); + const offset = closes.length - values.length; + return values.map((value, i) => ({ time: dates[i + offset], value })); +} + +export function computeMACD( + closes: number[], + dates: string[], + fastPeriod: number, + slowPeriod: number, + signalPeriod: number +) { + const results = MACD.calculate({ + values: closes, + fastPeriod, + slowPeriod, + signalPeriod, + SimpleMAOscillator: false, + SimpleMASignal: false, + }); + const offset = closes.length - results.length; + return results.map((r, i) => ({ + time: dates[i + offset], + macd: r.MACD ?? 0, + signal: r.signal ?? 0, + histogram: r.histogram ?? 0, + })); +} + +export function computeBollingerBands( + closes: number[], + dates: string[], + period: number, + stdDev: number +) { + const results = BollingerBands.calculate({ period, values: closes, stdDev }); + const offset = closes.length - results.length; + return results.map((r, i) => ({ + time: dates[i + offset], + upper: r.upper, + middle: r.middle, + lower: r.lower, + })); +} +``` + +**Step 4: Commit** + +```bash +git add frontend/src/lib/ +git commit -m "feat: add constants, formatters, and indicator computation utilities" +``` + +--- + +### Task 8: Common Components + +**Files:** +- Create: `frontend/src/components/common/Card.tsx` +- Create: `frontend/src/components/common/Skeleton.tsx` +- Create: `frontend/src/components/common/Badge.tsx` +- Create: `frontend/src/components/common/SearchInput.tsx` +- Create: `frontend/src/components/ticker/PriceChange.tsx` + +**Step 1: Create Card component** + +```tsx +interface CardProps { + children: React.ReactNode; + className?: string; + onClick?: () => void; +} + +export function Card({ children, className = '', onClick }: CardProps) { + return ( +
+ {children} +
+ ); +} +``` + +**Step 2: Create Skeleton component** + +```tsx +interface SkeletonProps { + className?: string; +} + +export function Skeleton({ className = '' }: SkeletonProps) { + return
; +} +``` + +**Step 3: Create Badge component** + +```tsx +interface BadgeProps { + children: React.ReactNode; + variant?: 'default' | 'bullish' | 'bearish'; +} + +export function Badge({ children, variant = 'default' }: BadgeProps) { + const colors = { + default: 'bg-accent/20 text-accent', + bullish: 'bg-bullish/20 text-bullish', + bearish: 'bg-bearish/20 text-bearish', + }; + return ( + + {children} + + ); +} +``` + +**Step 4: Create SearchInput component** + +```tsx +import { Search } from 'lucide-react'; + +interface SearchInputProps { + value: string; + onChange: (value: string) => void; + placeholder?: string; +} + +export function SearchInput({ value, onChange, placeholder = 'Search...' }: SearchInputProps) { + return ( +
+ + onChange(e.target.value)} + placeholder={placeholder} + className="w-full pl-9 pr-3 py-2 bg-surface-tertiary border border-border-primary rounded-lg + text-slate-50 placeholder-slate-500 + focus:border-accent focus:ring-1 focus:ring-accent/50 focus:outline-none + transition-colors duration-200" + /> +
+ ); +} +``` + +**Step 5: Create PriceChange component** + +```tsx +import { TrendingUp, TrendingDown, Minus } from 'lucide-react'; + +interface PriceChangeProps { + value: number; + className?: string; +} + +export function PriceChange({ value, className = '' }: PriceChangeProps) { + const isPositive = value > 0; + const isNegative = value < 0; + const Icon = isPositive ? TrendingUp : isNegative ? TrendingDown : Minus; + const color = isPositive ? 'text-bullish' : isNegative ? 'text-bearish' : 'text-slate-400'; + const sign = isPositive ? '+' : ''; + + return ( + + + {sign}{value.toFixed(2)}% + + ); +} +``` + +**Step 6: Verify components render** + +Import and render each component in `App.tsx` temporarily. Run `npm run dev`, check no errors and styles look correct. + +**Step 7: Commit** + +```bash +git add frontend/src/components/ +git commit -m "feat: add common UI components (Card, Skeleton, Badge, SearchInput, PriceChange)" +``` + +--- + +### Task 9: Layout Components + +**Files:** +- Create: `frontend/src/components/layout/Sidebar.tsx` +- Create: `frontend/src/components/layout/TopBar.tsx` +- Create: `frontend/src/components/layout/AppLayout.tsx` + +**Step 1: Create Sidebar** + +Navigation items: Dashboard (LayoutDashboard), Chart (CandlestickChart), Watchlist (Eye), Portfolio (Briefcase), Screener (SlidersHorizontal). + +Uses `react-router-dom`'s `NavLink` for active state. Reads `sidebarCollapsed` from Zustand. Responsive: full at >= 1024px, collapsed at >= 768px, hidden at < 768px with overlay. + +**Step 2: Create TopBar** + +Logo left, search center, ingest button + health indicator right. Search filters registered tickers and navigates to `/chart/{symbol}`. + +**Step 3: Create AppLayout** + +Wraps Sidebar + TopBar + `` from React Router. Handles the padding/offset for fixed sidebar and top bar. + +**Step 4: Verify layout renders** + +Set up basic routing in `App.tsx` with `BrowserRouter`, `Routes`, `Route` wrapping `AppLayout` as parent route with child placeholder routes. Run `npm run dev`, verify sidebar + topbar render with correct dark styling. + +**Step 5: Commit** + +```bash +git add frontend/src/components/layout/ frontend/src/App.tsx +git commit -m "feat: add layout components (Sidebar, TopBar, AppLayout) with routing" +``` + +--- + +### Task 10: Routing Setup + +**Files:** +- Modify: `frontend/src/App.tsx` +- Create: `frontend/src/pages/Dashboard.tsx` (stub) +- Create: `frontend/src/pages/Chart.tsx` (stub) +- Create: `frontend/src/pages/Watchlist.tsx` (stub) +- Create: `frontend/src/pages/Portfolio.tsx` (stub) +- Create: `frontend/src/pages/Screener.tsx` (stub) + +**Step 1: Create page stubs** + +Each page is a simple component with the page title for now: +```tsx +export default function Dashboard() { + return

Dashboard

; +} +``` + +**Step 2: Set up routes in App.tsx** + +```tsx +import { BrowserRouter, Routes, Route } from 'react-router-dom'; +import AppLayout from './components/layout/AppLayout'; +import Dashboard from './pages/Dashboard'; +import Chart from './pages/Chart'; +import Watchlist from './pages/Watchlist'; +import Portfolio from './pages/Portfolio'; +import Screener from './pages/Screener'; + +export default function App() { + return ( + + + }> + } /> + } /> + } /> + } /> + } /> + + + + ); +} +``` + +**Step 3: Verify navigation** + +Run `npm run dev`. Click each sidebar nav item, verify URL changes and correct page title renders. Verify active state styling on sidebar. + +**Step 4: Commit** + +```bash +git add frontend/src/App.tsx frontend/src/pages/ +git commit -m "feat: add React Router routes with page stubs" +``` + +--- + +### Task 11: Dashboard Page + +**Files:** +- Modify: `frontend/src/pages/Dashboard.tsx` +- Create: `frontend/src/components/ticker/TickerCard.tsx` +- Create: `frontend/src/components/ticker/AddTickerModal.tsx` +- Create: `frontend/src/components/ticker/SparklineChart.tsx` + +**Step 1: Create SparklineChart component** + +A tiny Lightweight Charts area chart (no axes, no grid) that renders last 30 days of closing prices. Takes `data: { time: string; value: number }[]` as prop. Uses a `useRef` for the chart container and `useEffect` for chart lifecycle. + +**Step 2: Create TickerCard component** + +Displays: symbol, latest close, daily change (PriceChange component), sparkline, volume, 52w range. Fetches data via `useSummary(symbol)` and `usePrices(symbol)` (last 30 days for sparkline). Clicking navigates to `/chart/{symbol}` via `useNavigate`. + +**Step 3: Create AddTickerModal component** + +A dialog/modal with: symbol input (uppercase, validated), submit button, loading state, error display. Uses `useAddTicker` mutation. Closes on success. + +**Step 4: Build Dashboard page** + +Header with "Market Overview" title and "Add Ticker" button. Grid of TickerCards (responsive columns via Tailwind grid). Uses `useTickers()` to get list. Maps each ticker to ``. Handles loading (skeleton grid), empty ("No tickers yet"), and error states. + +**Step 5: Verify with backend** + +Start backend (`uvicorn flow.api.app:app`), start frontend (`npm run dev`). Add a ticker, verify card appears with real data. Verify sparkline renders. + +**Step 6: Commit** + +```bash +git add frontend/src/pages/Dashboard.tsx frontend/src/components/ticker/ +git commit -m "feat: build Dashboard page with ticker cards, sparklines, and add ticker modal" +``` + +--- + +### Task 12: Charting Page - Candlestick Chart + +**Files:** +- Modify: `frontend/src/pages/Chart.tsx` +- Create: `frontend/src/components/chart/CandlestickChart.tsx` +- Create: `frontend/src/components/chart/ChartToolbar.tsx` +- Create: `frontend/src/components/chart/SymbolHeader.tsx` + +**Step 1: Create CandlestickChart component** + +Uses Lightweight Charts `createChart()` in a `useRef` container. Renders candlestick series from OHLCV data. Adds volume as a histogram series in the same chart (bottom pane, 20% height). Configures colors from `CHART_COLORS` constants. Handles resize via `ResizeObserver`. Cleans up chart on unmount. + +Props: `data: OHLCVRow[]`, `overlays?: { data: TimeValue[]; color: string }[]` + +**Step 2: Create ChartToolbar** + +Symbol selector dropdown (populated from `useTickers`), time range buttons (1M-ALL, active state styled), indicators toggle dropdown. Uses Zustand store for time range and indicator state. + +**Step 3: Create SymbolHeader** + +Displays symbol, latest close (large monospace), change amount/percent (PriceChange), 52-week range progress bar. Uses `useSummary(symbol)`. + +**Step 4: Build Chart page** + +Reads `:symbol` from URL params. If no symbol, shows prompt to select one. Fetches prices via `usePrices(symbol, startDate)` where `startDate` comes from `getStartDateForRange(timeRange)`. Passes data to CandlestickChart. Loading/error states. + +**Step 5: Verify chart renders** + +Navigate to `/chart/AAPL` (or any registered ticker). Verify candlesticks render with correct colors, volume bars show below, crosshair tooltip works, zoom/pan works. + +**Step 6: Commit** + +```bash +git add frontend/src/pages/Chart.tsx frontend/src/components/chart/ +git commit -m "feat: build Chart page with candlestick chart, toolbar, and symbol header" +``` + +--- + +### Task 13: Charting Page - Indicators + +**Files:** +- Modify: `frontend/src/components/chart/CandlestickChart.tsx` +- Create: `frontend/src/components/chart/IndicatorPanel.tsx` +- Create: `frontend/src/components/chart/IndicatorsDropdown.tsx` + +**Step 1: Add overlay indicators to CandlestickChart** + +When SMA/EMA/Bollinger are enabled in Zustand, compute them from price data using `lib/indicators.ts` and add as line series to the chart. Bollinger adds upper/lower as additional line series. + +**Step 2: Create IndicatorPanel for sub-chart indicators** + +RSI: Separate Lightweight Charts instance below main chart (~150px height). Renders RSI line with horizontal dashed lines at 30 and 70. + +MACD: Separate chart below RSI (~150px). Renders MACD line, signal line, and histogram as a histogram series. + +**Step 3: Create IndicatorsDropdown** + +Popover/dropdown listing all indicators with toggle switches. When an indicator is toggled, it shows/hides on the chart. For each indicator, show inline parameter editing (e.g., period input for SMA). + +**Step 4: Wire indicators into Chart page** + +Read enabled indicators from Zustand. Compute indicator data from price data. Pass overlays to CandlestickChart. Render IndicatorPanel components below chart for RSI/MACD when enabled. + +**Step 5: Verify indicators** + +Enable SMA 20 and RSI -- verify SMA line overlays on chart, RSI panel appears below. Toggle off -- verify they disappear. Change SMA period -- verify line updates. + +**Step 6: Commit** + +```bash +git add frontend/src/components/chart/ frontend/src/pages/Chart.tsx +git commit -m "feat: add technical indicators (SMA, EMA, RSI, MACD, Bollinger) to chart" +``` + +--- + +### Task 14: Watchlist Page + +**Files:** +- Modify: `frontend/src/pages/Watchlist.tsx` +- Create: `frontend/src/components/ticker/TickerRow.tsx` +- Create: `frontend/src/components/ticker/RangeBar.tsx` + +**Step 1: Create RangeBar component** + +A small visual progress bar showing where current price sits within 52-week range. Props: `low`, `high`, `current`. + +**Step 2: Create TickerRow component** + +A table row showing: symbol (clickable, links to chart), last price (monospace), change %, volume (formatted), 52w range (RangeBar), remove button (X icon with confirmation). + +**Step 3: Build Watchlist page** + +Header with "Watchlist" title and "Add Ticker" button (reuses AddTickerModal). Sortable table: click column headers to sort. Default sort: alphabetical. Uses `useTickers()` for the list, `useSummary(symbol)` for each row's data. Loading state: skeleton rows. Empty state: centered message with add button. + +**Step 4: Verify watchlist** + +Navigate to `/watchlist`. Verify table renders with data, sorting works, clicking symbol navigates to chart, remove button deletes ticker. + +**Step 5: Commit** + +```bash +git add frontend/src/pages/Watchlist.tsx frontend/src/components/ticker/ +git commit -m "feat: build Watchlist page with sortable table and ticker management" +``` + +--- + +### Task 15: Placeholder Pages + +**Files:** +- Modify: `frontend/src/pages/Portfolio.tsx` +- Modify: `frontend/src/pages/Screener.tsx` + +**Step 1: Build Portfolio placeholder** + +Centered Card with Briefcase icon (Lucide, 48px, slate-500), "Portfolio Coming Soon" heading (Inter 600), description text (slate-400). Styled consistently with app theme. + +**Step 2: Build Screener placeholder** + +Same pattern with SlidersHorizontal icon and "Screener Coming Soon" copy. + +**Step 3: Verify both pages** + +Navigate to `/portfolio` and `/screener`. Verify they render the placeholder UI correctly. + +**Step 4: Commit** + +```bash +git add frontend/src/pages/Portfolio.tsx frontend/src/pages/Screener.tsx +git commit -m "feat: add placeholder pages for Portfolio and Screener" +``` + +--- + +### Task 16: Polish & Integration + +**Files:** +- Various components for refinements + +**Step 1: Verify responsive behavior** + +Test at 375px, 768px, 1024px, 1440px. Verify sidebar collapses, grid columns adjust, chart resizes, table scrolls horizontally on mobile. + +**Step 2: Verify loading states** + +Throttle network in DevTools. Verify skeleton screens appear on Dashboard (cards), Chart (chart area), Watchlist (table rows). + +**Step 3: Verify error handling** + +Stop the backend. Verify error states render with retry buttons on all pages. + +**Step 4: Accessibility check** + +- All clickable elements have `cursor-pointer` +- Focus states visible on all interactive elements +- Color is not the only indicator (arrows + color for price changes) +- All images/icons have accessible labels where needed + +**Step 5: Final commit** + +```bash +git add -A +git commit -m "feat: polish responsive behavior, loading states, error handling, and accessibility" +``` diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..d2e7761 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..5e6b472 --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..d164843 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,15 @@ + + + + + + Flow + + + + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..96d9fa9 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,4330 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "@tanstack/react-query": "^5.90.21", + "axios": "^1.13.6", + "lightweight-charts": "^5.1.0", + "lucide-react": "^0.577.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-router-dom": "^7.13.1", + "technicalindicators": "^3.1.0", + "zustand": "^5.0.11" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@tailwindcss/vite": "^4.2.1", + "@types/node": "^24.12.0", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "tailwindcss": "^4.2.1", + "typescript": "~5.9.3", + "typescript-eslint": "^8.48.0", + "vite": "^7.3.1" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz", + "integrity": "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.31.1", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.1" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.1.tgz", + "integrity": "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-x64": "4.2.1", + "@tailwindcss/oxide-freebsd-x64": "4.2.1", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-x64-musl": "4.2.1", + "@tailwindcss/oxide-wasm32-wasi": "4.2.1", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz", + "integrity": "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz", + "integrity": "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz", + "integrity": "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz", + "integrity": "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz", + "integrity": "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz", + "integrity": "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz", + "integrity": "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz", + "integrity": "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz", + "integrity": "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz", + "integrity": "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz", + "integrity": "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz", + "integrity": "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.1.tgz", + "integrity": "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.2.1", + "@tailwindcss/oxide": "4.2.1", + "tailwindcss": "4.2.1" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.90.20", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz", + "integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.21", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.21.tgz", + "integrity": "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.12.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz", + "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.0.tgz", + "integrity": "sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.57.0", + "@typescript-eslint/type-utils": "8.57.0", + "@typescript-eslint/utils": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.57.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.0.tgz", + "integrity": "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.57.0", + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.0.tgz", + "integrity": "sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.57.0", + "@typescript-eslint/types": "^8.57.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.0.tgz", + "integrity": "sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.0.tgz", + "integrity": "sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.0.tgz", + "integrity": "sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0", + "@typescript-eslint/utils": "8.57.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.0.tgz", + "integrity": "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.0.tgz", + "integrity": "sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.57.0", + "@typescript-eslint/tsconfig-utils": "8.57.0", + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.0.tgz", + "integrity": "sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.57.0", + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.0.tgz", + "integrity": "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.4.tgz", + "integrity": "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001777", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz", + "integrity": "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.307", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz", + "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", + "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fancy-canvas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fancy-canvas/-/fancy-canvas-2.1.0.tgz", + "integrity": "sha512-nifxXJ95JNLFR2NgRV4/MxVP45G9909wJTEKz5fg/TZS20JJZA6hfgRVh/bC9bwl2zBtBNcYPjiBE4njQHVBwQ==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz", + "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", + "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.31.1", + "lightningcss-darwin-arm64": "1.31.1", + "lightningcss-darwin-x64": "1.31.1", + "lightningcss-freebsd-x64": "1.31.1", + "lightningcss-linux-arm-gnueabihf": "1.31.1", + "lightningcss-linux-arm64-gnu": "1.31.1", + "lightningcss-linux-arm64-musl": "1.31.1", + "lightningcss-linux-x64-gnu": "1.31.1", + "lightningcss-linux-x64-musl": "1.31.1", + "lightningcss-win32-arm64-msvc": "1.31.1", + "lightningcss-win32-x64-msvc": "1.31.1" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", + "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", + "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", + "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", + "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", + "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", + "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", + "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", + "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", + "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", + "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", + "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightweight-charts": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/lightweight-charts/-/lightweight-charts-5.1.0.tgz", + "integrity": "sha512-jEAYR4ODYeyNZcWUigsoLTl52rbPmgXnvd5FLIv/ZoA/2sSDw63YKnef8n4yhzum7W926yHeFwlm7ididKb7YQ==", + "license": "Apache-2.0", + "dependencies": { + "fancy-canvas": "2.1.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.577.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.577.0.tgz", + "integrity": "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz", + "integrity": "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.1.tgz", + "integrity": "sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==", + "license": "MIT", + "dependencies": { + "react-router": "7.13.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tailwindcss": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz", + "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/technicalindicators": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/technicalindicators/-/technicalindicators-3.1.0.tgz", + "integrity": "sha512-f16mOc+Y05hNy/of+UbGxhxQQmxUztCiluhsqC5QLUYz4WowUgKde9m6nIjK1Kay0wGHigT0IkOabpp0+22UfA==", + "license": "MIT", + "dependencies": { + "@types/node": "^6.0.96" + } + }, + "node_modules/technicalindicators/node_modules/@types/node": { + "version": "6.14.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-6.14.13.tgz", + "integrity": "sha512-J1F0XJ/9zxlZel5ZlbeSuHW2OpabrUAqpFuC2sm2I3by8sERQ8+KCjNKUcq8QHuzpGMWiJpo9ZxeHrqrP2KzQw==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.0.tgz", + "integrity": "sha512-W8GcigEMEeB07xEZol8oJ26rigm3+bfPHxHvwbYUlu1fUDsGuQ7Hiskx5xGW/xM4USc9Ephe3jtv7ZYPQntHeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.57.0", + "@typescript-eslint/parser": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0", + "@typescript-eslint/utils": "8.57.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, + "node_modules/zustand": { + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.11.tgz", + "integrity": "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..93e60a1 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,39 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@tanstack/react-query": "^5.90.21", + "axios": "^1.13.6", + "lightweight-charts": "^5.1.0", + "lucide-react": "^0.577.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-router-dom": "^7.13.1", + "technicalindicators": "^3.1.0", + "zustand": "^5.0.11" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@tailwindcss/vite": "^4.2.1", + "@types/node": "^24.12.0", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "tailwindcss": "^4.2.1", + "typescript": "~5.9.3", + "typescript-eslint": "^8.48.0", + "vite": "^7.3.1" + } +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..b1232ec --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,23 @@ +import { BrowserRouter, Routes, Route } from 'react-router-dom'; +import { AppLayout } from './components/layout/AppLayout'; +import Dashboard from './pages/Dashboard'; +import Chart from './pages/Chart'; +import Watchlist from './pages/Watchlist'; +import Portfolio from './pages/Portfolio'; +import Screener from './pages/Screener'; + +export default function App() { + return ( + + + }> + } /> + } /> + } /> + } /> + } /> + + + + ); +} diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts new file mode 100644 index 0000000..5a29faf --- /dev/null +++ b/frontend/src/api/client.ts @@ -0,0 +1,16 @@ +import axios from 'axios'; + +const apiClient = axios.create({ + baseURL: import.meta.env.VITE_API_URL || '', + headers: { 'Content-Type': 'application/json' }, +}); + +apiClient.interceptors.response.use( + (response) => response, + (error) => { + const message = error.response?.data?.detail || error.message || 'An error occurred'; + return Promise.reject(new Error(message)); + } +); + +export default apiClient; diff --git a/frontend/src/components/chart/CandlestickChart.tsx b/frontend/src/components/chart/CandlestickChart.tsx new file mode 100644 index 0000000..3e4feb4 --- /dev/null +++ b/frontend/src/components/chart/CandlestickChart.tsx @@ -0,0 +1,114 @@ +import { useEffect, useRef } from 'react'; +import { + createChart, + ColorType, + CrosshairMode, + CandlestickSeries, + HistogramSeries, + LineSeries, + type IChartApi, +} from 'lightweight-charts'; +import { CHART_COLORS } from '../../lib/constants'; +import type { OHLCVRow } from '../../types'; + +interface OverlayData { + data: { time: string; value: number }[]; + color: string; +} + +interface CandlestickChartProps { + data: OHLCVRow[]; + overlays?: OverlayData[]; +} + +export function CandlestickChart({ data, overlays = [] }: CandlestickChartProps) { + const containerRef = useRef(null); + const chartRef = useRef(null); + + useEffect(() => { + if (!containerRef.current || data.length === 0) return; + + const chart = createChart(containerRef.current, { + width: containerRef.current.clientWidth, + height: containerRef.current.clientHeight, + layout: { + background: { type: ColorType.Solid, color: CHART_COLORS.background }, + textColor: '#94A3B8', + }, + grid: { + vertLines: { color: CHART_COLORS.grid }, + horzLines: { color: CHART_COLORS.grid }, + }, + crosshair: { + mode: CrosshairMode.Normal, + vertLine: { color: CHART_COLORS.crosshair }, + horzLine: { color: CHART_COLORS.crosshair }, + }, + rightPriceScale: { borderColor: CHART_COLORS.grid }, + timeScale: { borderColor: CHART_COLORS.grid, timeVisible: false }, + }); + chartRef.current = chart; + + const candleSeries = chart.addSeries(CandlestickSeries, { + upColor: CHART_COLORS.bullish, + downColor: CHART_COLORS.bearish, + wickUpColor: CHART_COLORS.bullishWick, + wickDownColor: CHART_COLORS.bearishWick, + borderVisible: false, + }); + candleSeries.setData( + data.map((row) => ({ + time: row.date, + open: row.open, + high: row.high, + low: row.low, + close: row.close, + })), + ); + + const volumeSeries = chart.addSeries(HistogramSeries, { + color: CHART_COLORS.volume, + priceFormat: { type: 'volume' }, + priceScaleId: 'volume', + }); + chart.priceScale('volume').applyOptions({ + scaleMargins: { top: 0.8, bottom: 0 }, + }); + volumeSeries.setData( + data.map((row) => ({ + time: row.date, + value: row.volume, + color: + row.close >= row.open + ? 'rgba(34, 197, 94, 0.4)' + : 'rgba(239, 68, 68, 0.4)', + })), + ); + + for (const overlay of overlays) { + const lineSeries = chart.addSeries(LineSeries, { + color: overlay.color, + lineWidth: 2, + priceLineVisible: false, + lastValueVisible: false, + }); + lineSeries.setData(overlay.data); + } + + chart.timeScale().fitContent(); + + const resizeObserver = new ResizeObserver((entries) => { + const { width, height } = entries[0].contentRect; + chart.applyOptions({ width, height }); + }); + resizeObserver.observe(containerRef.current); + + return () => { + resizeObserver.disconnect(); + chart.remove(); + chartRef.current = null; + }; + }, [data, overlays]); + + return
; +} diff --git a/frontend/src/components/chart/ChartToolbar.tsx b/frontend/src/components/chart/ChartToolbar.tsx new file mode 100644 index 0000000..06b06ac --- /dev/null +++ b/frontend/src/components/chart/ChartToolbar.tsx @@ -0,0 +1,49 @@ +import { useUIStore } from '../../stores/uiStore'; +import { TIME_RANGES } from '../../lib/constants'; +import { IndicatorsDropdown } from './IndicatorsDropdown'; + +interface ChartToolbarProps { + symbols: string[]; + selectedSymbol: string; + onSymbolChange: (symbol: string) => void; +} + +export function ChartToolbar({ symbols, selectedSymbol, onSymbolChange }: ChartToolbarProps) { + const chartTimeRange = useUIStore((s) => s.chartTimeRange); + const setTimeRange = useUIStore((s) => s.setTimeRange); + + return ( +
+ + +
+ {TIME_RANGES.map((range) => ( + + ))} +
+ + +
+ ); +} diff --git a/frontend/src/components/chart/IndicatorPanel.tsx b/frontend/src/components/chart/IndicatorPanel.tsx new file mode 100644 index 0000000..56728e1 --- /dev/null +++ b/frontend/src/components/chart/IndicatorPanel.tsx @@ -0,0 +1,145 @@ +import { useEffect, useRef } from 'react'; +import { + createChart, + ColorType, + LineSeries, + HistogramSeries, + type IChartApi, +} from 'lightweight-charts'; +import { CHART_COLORS } from '../../lib/constants'; + +interface RSIDataPoint { + time: string; + value: number; +} + +interface MACDDataPoint { + time: string; + macd: number; + signal: number; + histogram: number; +} + +interface IndicatorPanelProps { + type: 'rsi' | 'macd'; + data: RSIDataPoint[] | MACDDataPoint[]; +} + +export function IndicatorPanel({ type, data }: IndicatorPanelProps) { + const containerRef = useRef(null); + const chartRef = useRef(null); + + useEffect(() => { + if (!containerRef.current || data.length === 0) return; + + const chart = createChart(containerRef.current, { + width: containerRef.current.clientWidth, + height: 150, + layout: { + background: { type: ColorType.Solid, color: CHART_COLORS.background }, + textColor: '#94A3B8', + }, + grid: { + vertLines: { color: CHART_COLORS.grid }, + horzLines: { color: CHART_COLORS.grid }, + }, + rightPriceScale: { borderColor: CHART_COLORS.grid }, + timeScale: { borderColor: CHART_COLORS.grid, timeVisible: false }, + crosshair: { + vertLine: { color: CHART_COLORS.crosshair }, + horzLine: { color: CHART_COLORS.crosshair }, + }, + }); + chartRef.current = chart; + + if (type === 'rsi') { + const rsiData = data as RSIDataPoint[]; + chart.priceScale('right').applyOptions({ + autoScale: false, + scaleMargins: { top: 0.05, bottom: 0.05 }, + }); + + const rsiSeries = chart.addSeries(LineSeries, { + color: '#3B82F6', + lineWidth: 2, + priceLineVisible: false, + lastValueVisible: false, + }); + rsiSeries.setData(rsiData); + + rsiSeries.createPriceLine({ + price: 70, + color: '#EF4444', + lineWidth: 1, + lineStyle: 2, + axisLabelVisible: true, + }); + rsiSeries.createPriceLine({ + price: 30, + color: '#22C55E', + lineWidth: 1, + lineStyle: 2, + axisLabelVisible: true, + }); + } else { + const macdData = data as MACDDataPoint[]; + + const histSeries = chart.addSeries(HistogramSeries, { + priceLineVisible: false, + lastValueVisible: false, + }); + histSeries.setData( + macdData.map((d) => ({ + time: d.time, + value: d.histogram, + color: d.histogram >= 0 ? 'rgba(34, 197, 94, 0.6)' : 'rgba(239, 68, 68, 0.6)', + })), + ); + + const macdLineSeries = chart.addSeries(LineSeries, { + color: '#3B82F6', + lineWidth: 2, + priceLineVisible: false, + lastValueVisible: false, + }); + macdLineSeries.setData( + macdData.map((d) => ({ time: d.time, value: d.macd })), + ); + + const signalSeries = chart.addSeries(LineSeries, { + color: '#F59E0B', + lineWidth: 2, + priceLineVisible: false, + lastValueVisible: false, + }); + signalSeries.setData( + macdData.map((d) => ({ time: d.time, value: d.signal })), + ); + } + + chart.timeScale().fitContent(); + + const resizeObserver = new ResizeObserver((entries) => { + const { width } = entries[0].contentRect; + chart.applyOptions({ width }); + }); + resizeObserver.observe(containerRef.current); + + return () => { + resizeObserver.disconnect(); + chart.remove(); + chartRef.current = null; + }; + }, [type, data]); + + const label = type === 'rsi' ? 'RSI' : 'MACD'; + + return ( +
+
+ {label} +
+
+
+ ); +} diff --git a/frontend/src/components/chart/IndicatorsDropdown.tsx b/frontend/src/components/chart/IndicatorsDropdown.tsx new file mode 100644 index 0000000..742699c --- /dev/null +++ b/frontend/src/components/chart/IndicatorsDropdown.tsx @@ -0,0 +1,92 @@ +import { useState, useRef, useEffect } from 'react'; +import { BarChart3 } from 'lucide-react'; +import { useUIStore } from '../../stores/uiStore'; +import type { IndicatorConfig } from '../../types'; + +function formatIndicator(ind: IndicatorConfig): string { + switch (ind.type) { + case 'sma': + return `SMA (${ind.params.period})`; + case 'ema': + return `EMA (${ind.params.period})`; + case 'rsi': + return `RSI (${ind.params.period})`; + case 'macd': + return `MACD (${ind.params.fast}, ${ind.params.slow}, ${ind.params.signal})`; + case 'bollinger': + return `BB (${ind.params.period}, ${ind.params.stdDev})`; + } +} + +export function IndicatorsDropdown() { + const [open, setOpen] = useState(false); + const containerRef = useRef(null); + const selectedIndicators = useUIStore((s) => s.selectedIndicators); + const toggleIndicator = useUIStore((s) => s.toggleIndicator); + + const enabledCount = selectedIndicators.filter((i) => i.enabled).length; + + useEffect(() => { + if (!open) return; + function handleClick(e: MouseEvent) { + if (containerRef.current && !containerRef.current.contains(e.target as Node)) { + setOpen(false); + } + } + document.addEventListener('mousedown', handleClick); + return () => document.removeEventListener('mousedown', handleClick); + }, [open]); + + return ( +
+ + + {open && ( +
+
+ Indicators +
+ {selectedIndicators.map((ind, index) => ( + + ))} +
+ )} +
+ ); +} diff --git a/frontend/src/components/chart/SymbolHeader.tsx b/frontend/src/components/chart/SymbolHeader.tsx new file mode 100644 index 0000000..82d7682 --- /dev/null +++ b/frontend/src/components/chart/SymbolHeader.tsx @@ -0,0 +1,56 @@ +import { useSummary } from '../../hooks/useSummary'; +import { PriceChange } from '../ticker/PriceChange'; +import { Skeleton } from '../common/Skeleton'; +import { formatPrice } from '../../lib/formatters'; + +interface SymbolHeaderProps { + symbol: string; +} + +export function SymbolHeader({ symbol }: SymbolHeaderProps) { + const { data: summary, isLoading } = useSummary(symbol); + + if (isLoading || !summary) { + return ( +
+ + + +
+ ); + } + + const { latest_close, high_52w, low_52w } = summary; + const range52w = high_52w - low_52w; + const positionPct = range52w > 0 ? ((latest_close - low_52w) / range52w) * 100 : 50; + const changePct = low_52w > 0 ? ((latest_close - low_52w) / low_52w) * 100 : 0; + + return ( +
+

{symbol}

+ +
+ + ${formatPrice(latest_close)} + + +
+ +
+ {formatPrice(low_52w)} +
+
+
+
+ {formatPrice(high_52w)} + 52w +
+
+ ); +} diff --git a/frontend/src/components/common/Badge.tsx b/frontend/src/components/common/Badge.tsx new file mode 100644 index 0000000..d0acbbf --- /dev/null +++ b/frontend/src/components/common/Badge.tsx @@ -0,0 +1,18 @@ +interface BadgeProps { + children: React.ReactNode; + variant?: 'default' | 'bullish' | 'bearish'; +} + +const variantClasses: Record, string> = { + default: 'bg-accent/20 text-accent', + bullish: 'bg-bullish/20 text-bullish', + bearish: 'bg-bearish/20 text-bearish', +}; + +export function Badge({ children, variant = 'default' }: BadgeProps) { + return ( + + {children} + + ); +} diff --git a/frontend/src/components/common/Card.tsx b/frontend/src/components/common/Card.tsx new file mode 100644 index 0000000..fda646b --- /dev/null +++ b/frontend/src/components/common/Card.tsx @@ -0,0 +1,18 @@ +interface CardProps { + children: React.ReactNode; + className?: string; + onClick?: () => void; +} + +export function Card({ children, className = '', onClick }: CardProps) { + const base = 'bg-surface-secondary border border-border-primary rounded-xl p-4'; + const interactive = onClick + ? 'cursor-pointer hover:border-border-hover transition-colors duration-200' + : ''; + + return ( +
+ {children} +
+ ); +} diff --git a/frontend/src/components/common/SearchInput.tsx b/frontend/src/components/common/SearchInput.tsx new file mode 100644 index 0000000..7282a84 --- /dev/null +++ b/frontend/src/components/common/SearchInput.tsx @@ -0,0 +1,22 @@ +import { Search } from 'lucide-react'; + +interface SearchInputProps { + value: string; + onChange: (value: string) => void; + placeholder?: string; +} + +export function SearchInput({ value, onChange, placeholder = 'Search...' }: SearchInputProps) { + return ( +
+ + onChange(e.target.value)} + placeholder={placeholder} + className="w-full bg-surface-tertiary border border-border-primary rounded-lg py-1.5 pl-9 pr-3 text-sm text-slate-50 placeholder-slate-500 focus:border-accent focus:outline-none transition-colors duration-200" + /> +
+ ); +} diff --git a/frontend/src/components/common/Skeleton.tsx b/frontend/src/components/common/Skeleton.tsx new file mode 100644 index 0000000..bcc21f2 --- /dev/null +++ b/frontend/src/components/common/Skeleton.tsx @@ -0,0 +1,7 @@ +interface SkeletonProps { + className?: string; +} + +export function Skeleton({ className = '' }: SkeletonProps) { + return
; +} diff --git a/frontend/src/components/layout/AppLayout.tsx b/frontend/src/components/layout/AppLayout.tsx new file mode 100644 index 0000000..de72296 --- /dev/null +++ b/frontend/src/components/layout/AppLayout.tsx @@ -0,0 +1,15 @@ +import { Outlet } from 'react-router-dom'; +import { Sidebar } from './Sidebar'; +import { TopBar } from './TopBar'; + +export function AppLayout() { + return ( +
+ + +
+ +
+
+ ); +} diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx new file mode 100644 index 0000000..940960c --- /dev/null +++ b/frontend/src/components/layout/Sidebar.tsx @@ -0,0 +1,96 @@ +import { NavLink } from 'react-router-dom'; +import { + LayoutDashboard, + BarChart3, + Eye, + Briefcase, + SlidersHorizontal, + X, +} from 'lucide-react'; +import { useUIStore } from '../../stores/uiStore'; + +const navItems = [ + { label: 'Dashboard', icon: LayoutDashboard, path: '/' }, + { label: 'Chart', icon: BarChart3, path: '/chart' }, + { label: 'Watchlist', icon: Eye, path: '/watchlist' }, + { label: 'Portfolio', icon: Briefcase, path: '/portfolio' }, + { label: 'Screener', icon: SlidersHorizontal, path: '/screener' }, +]; + +export function Sidebar() { + const sidebarMobileOpen = useUIStore((s) => s.sidebarMobileOpen); + const setSidebarMobileOpen = useUIStore((s) => s.setSidebarMobileOpen); + + const navContent = ( + + ); + + return ( + <> + {/* Desktop / Tablet sidebar */} + + + {/* Mobile overlay */} + {sidebarMobileOpen && ( +
+
setSidebarMobileOpen(false)} + /> + +
+ )} + + ); +} diff --git a/frontend/src/components/layout/TopBar.tsx b/frontend/src/components/layout/TopBar.tsx new file mode 100644 index 0000000..310f80b --- /dev/null +++ b/frontend/src/components/layout/TopBar.tsx @@ -0,0 +1,106 @@ +import { useState, useRef, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Menu, Loader2, Download } from 'lucide-react'; +import { SearchInput } from '../common/SearchInput'; +import { useTickers } from '../../hooks/useTickers'; +import { useHealth, useIngest } from '../../hooks/useHealth'; +import { useUIStore } from '../../stores/uiStore'; + +export function TopBar() { + const navigate = useNavigate(); + const setSidebarMobileOpen = useUIStore((s) => s.setSidebarMobileOpen); + + const { data: tickers } = useTickers(); + const { data: health, isError: healthError } = useHealth(); + const ingest = useIngest(); + + const [search, setSearch] = useState(''); + const [dropdownOpen, setDropdownOpen] = useState(false); + const wrapperRef = useRef(null); + + const filtered = search.length > 0 && tickers + ? tickers.filter((t) => + t.symbol.toLowerCase().includes(search.toLowerCase()) + ) + : []; + + useEffect(() => { + function handleClickOutside(e: MouseEvent) { + if (wrapperRef.current && !wrapperRef.current.contains(e.target as Node)) { + setDropdownOpen(false); + } + } + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + const isHealthy = health && !healthError; + + return ( +
+ {/* Left */} +
+ + Flow +
+ + {/* Center - Search */} +
+ { + setSearch(v); + setDropdownOpen(v.length > 0); + }} + placeholder="Search tickers..." + /> + {dropdownOpen && filtered.length > 0 && ( +
+ {filtered.slice(0, 8).map((t) => ( + + ))} +
+ )} +
+ + {/* Right */} +
+ + +
+
+
+
+
+ ); +} diff --git a/frontend/src/components/ticker/AddTickerModal.tsx b/frontend/src/components/ticker/AddTickerModal.tsx new file mode 100644 index 0000000..5802d1b --- /dev/null +++ b/frontend/src/components/ticker/AddTickerModal.tsx @@ -0,0 +1,105 @@ +import { useState, useEffect, useCallback } from 'react'; +import { X } from 'lucide-react'; +import { useAddTicker } from '../../hooks/useTickers'; + +interface AddTickerModalProps { + isOpen: boolean; + onClose: () => void; +} + +export function AddTickerModal({ isOpen, onClose }: AddTickerModalProps) { + const [symbol, setSymbol] = useState(''); + const [error, setError] = useState(''); + const { mutateAsync, isPending } = useAddTicker(); + + const handleClose = useCallback(() => { + setSymbol(''); + setError(''); + onClose(); + }, [onClose]); + + useEffect(() => { + function onKeyDown(e: KeyboardEvent) { + if (e.key === 'Escape') handleClose(); + } + if (isOpen) { + document.addEventListener('keydown', onKeyDown); + return () => document.removeEventListener('keydown', onKeyDown); + } + }, [isOpen, handleClose]); + + if (!isOpen) return null; + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setError(''); + + const trimmed = symbol.trim().toUpperCase(); + if (!trimmed || !/^[A-Z]{1,5}$/.test(trimmed)) { + setError('Enter a valid ticker symbol (1-5 letters)'); + return; + } + + try { + await mutateAsync(trimmed); + handleClose(); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to add ticker'); + } + } + + return ( +
+
e.stopPropagation()} + > +
+

Add Ticker

+ +
+ +
+ setSymbol(e.target.value.toUpperCase())} + placeholder="e.g. AAPL" + maxLength={5} + className="w-full bg-surface-primary border border-border-primary rounded-lg px-3 py-2 text-slate-50 placeholder-slate-500 focus:outline-none focus:border-accent transition-colors duration-200 uppercase font-mono mb-3" + autoFocus + /> + + {error && ( +

{error}

+ )} + +
+ + +
+
+
+
+ ); +} diff --git a/frontend/src/components/ticker/PriceChange.tsx b/frontend/src/components/ticker/PriceChange.tsx new file mode 100644 index 0000000..1adcdcd --- /dev/null +++ b/frontend/src/components/ticker/PriceChange.tsx @@ -0,0 +1,29 @@ +import { TrendingUp, TrendingDown, Minus } from 'lucide-react'; + +interface PriceChangeProps { + value: number; + className?: string; +} + +export function PriceChange({ value, className = '' }: PriceChangeProps) { + let colorClass: string; + let Icon: typeof TrendingUp; + + if (value > 0) { + colorClass = 'text-bullish'; + Icon = TrendingUp; + } else if (value < 0) { + colorClass = 'text-bearish'; + Icon = TrendingDown; + } else { + colorClass = 'text-slate-400'; + Icon = Minus; + } + + return ( + + + {value > 0 ? '+' : ''}{value.toFixed(2)}% + + ); +} diff --git a/frontend/src/components/ticker/RangeBar.tsx b/frontend/src/components/ticker/RangeBar.tsx new file mode 100644 index 0000000..016f85d --- /dev/null +++ b/frontend/src/components/ticker/RangeBar.tsx @@ -0,0 +1,29 @@ +import { formatPrice } from '../../lib/formatters'; + +interface RangeBarProps { + low: number; + high: number; + current: number; +} + +export function RangeBar({ low, high, current }: RangeBarProps) { + const range = high - low; + const pct = range > 0 ? Math.min(Math.max(((current - low) / range) * 100, 0), 100) : 0; + + return ( +
+ {formatPrice(low)} +
+
+
+
+ {formatPrice(high)} +
+ ); +} diff --git a/frontend/src/components/ticker/SparklineChart.tsx b/frontend/src/components/ticker/SparklineChart.tsx new file mode 100644 index 0000000..756dea7 --- /dev/null +++ b/frontend/src/components/ticker/SparklineChart.tsx @@ -0,0 +1,62 @@ +import { useEffect, useRef } from 'react'; +import { createChart, ColorType, AreaSeries } from 'lightweight-charts'; + +interface SparklineChartProps { + data: { time: string; value: number }[]; +} + +export function SparklineChart({ data }: SparklineChartProps) { + const containerRef = useRef(null); + + useEffect(() => { + if (!containerRef.current || data.length === 0) return; + + const chart = createChart(containerRef.current, { + width: containerRef.current.clientWidth, + height: 60, + layout: { + background: { type: ColorType.Solid, color: 'transparent' }, + textColor: 'transparent', + }, + grid: { + vertLines: { visible: false }, + horzLines: { visible: false }, + }, + timeScale: { visible: false }, + rightPriceScale: { visible: false }, + leftPriceScale: { visible: false }, + crosshair: { + vertLine: { visible: false }, + horzLine: { visible: false }, + }, + handleScroll: false, + handleScale: false, + }); + + const series = chart.addSeries(AreaSeries, { + topColor: 'rgba(59, 130, 246, 0.4)', + bottomColor: 'rgba(59, 130, 246, 0.0)', + lineColor: '#3B82F6', + lineWidth: 2, + priceLineVisible: false, + lastValueVisible: false, + crosshairMarkerVisible: false, + }); + + series.setData(data); + chart.timeScale().fitContent(); + + const resizeObserver = new ResizeObserver((entries) => { + const { width } = entries[0].contentRect; + chart.applyOptions({ width }); + }); + resizeObserver.observe(containerRef.current); + + return () => { + resizeObserver.disconnect(); + chart.remove(); + }; + }, [data]); + + return
; +} diff --git a/frontend/src/components/ticker/TickerCard.tsx b/frontend/src/components/ticker/TickerCard.tsx new file mode 100644 index 0000000..2f2521c --- /dev/null +++ b/frontend/src/components/ticker/TickerCard.tsx @@ -0,0 +1,74 @@ +import { useMemo } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Card } from '../common/Card'; +import { Skeleton } from '../common/Skeleton'; +import { PriceChange } from './PriceChange'; +import { SparklineChart } from './SparklineChart'; +import { useSummary } from '../../hooks/useSummary'; +import { usePrices } from '../../hooks/usePrices'; +import { formatPrice, formatVolume } from '../../lib/formatters'; + +interface TickerCardProps { + symbol: string; +} + +const thirtyDaysAgo = new Date(Date.now() - 30 * 86400000) + .toISOString() + .split('T')[0]; + +export function TickerCard({ symbol }: TickerCardProps) { + const navigate = useNavigate(); + const { data: summary, isLoading: summaryLoading } = useSummary(symbol); + const { data: prices, isLoading: pricesLoading } = usePrices(symbol, thirtyDaysAgo); + + const sparklineData = useMemo(() => { + if (!prices?.data) return []; + return prices.data.map((row) => ({ + time: row.date, + value: row.close, + })); + }, [prices]); + + const changePercent = useMemo(() => { + if (!sparklineData.length || sparklineData.length < 2) return 0; + const first = sparklineData[0].value; + const last = sparklineData[sparklineData.length - 1].value; + return ((last - first) / first) * 100; + }, [sparklineData]); + + const isLoading = summaryLoading || pricesLoading; + + if (isLoading) { + return ( + + + + + + + + + ); + } + + if (!summary) return null; + + return ( + navigate(`/chart/${symbol}`)}> +

{symbol}

+

+ ${formatPrice(summary.latest_close)} +

+ +
+ +
+
+ Vol: {formatVolume(summary.avg_volume_30d)} + + 52w: {formatPrice(summary.low_52w)} - {formatPrice(summary.high_52w)} + +
+
+ ); +} diff --git a/frontend/src/components/ticker/TickerRow.tsx b/frontend/src/components/ticker/TickerRow.tsx new file mode 100644 index 0000000..8ef6de1 --- /dev/null +++ b/frontend/src/components/ticker/TickerRow.tsx @@ -0,0 +1,73 @@ +import { useNavigate } from 'react-router-dom'; +import { X } from 'lucide-react'; +import { useSummary } from '../../hooks/useSummary'; +import { formatPrice, formatVolume } from '../../lib/formatters'; +import { PriceChange } from './PriceChange'; +import { RangeBar } from './RangeBar'; +import { Skeleton } from '../common/Skeleton'; + +interface TickerRowProps { + symbol: string; + onRemove: (symbol: string) => void; +} + +export function TickerRow({ symbol, onRemove }: TickerRowProps) { + const navigate = useNavigate(); + const { data: summary, isLoading } = useSummary(symbol); + + if (isLoading || !summary) { + return ( + + + + + + + + + ); + } + + const changePercent = + summary.low_52w > 0 + ? ((summary.latest_close - summary.low_52w) / summary.low_52w) * 100 + : 0; + + return ( + + + + + + {formatPrice(summary.latest_close)} + + + + + + {formatVolume(summary.avg_volume_30d)} + + + + + + + + + ); +} diff --git a/frontend/src/hooks/useHealth.ts b/frontend/src/hooks/useHealth.ts new file mode 100644 index 0000000..1b72514 --- /dev/null +++ b/frontend/src/hooks/useHealth.ts @@ -0,0 +1,28 @@ +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import apiClient from '../api/client'; +import type { IngestResponse } from '../types'; + +export function useHealth() { + return useQuery({ + queryKey: ['health'], + queryFn: async () => { + const { data } = await apiClient.get('/health'); + return data; + }, + staleTime: 10_000, + }); +} + +export function useIngest() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async (): Promise => { + const { data } = await apiClient.post('/api/ingest'); + return data; + }, + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['prices'] }); + qc.invalidateQueries({ queryKey: ['summary'] }); + }, + }); +} diff --git a/frontend/src/hooks/usePrices.ts b/frontend/src/hooks/usePrices.ts new file mode 100644 index 0000000..5bc9718 --- /dev/null +++ b/frontend/src/hooks/usePrices.ts @@ -0,0 +1,18 @@ +import { useQuery } from '@tanstack/react-query'; +import apiClient from '../api/client'; +import type { PriceResponse } from '../types'; + +export function usePrices(symbol: string, start?: string, end?: string) { + return useQuery({ + queryKey: ['prices', symbol, start, end], + queryFn: async (): Promise => { + const params: Record = {}; + if (start) params.start = start; + if (end) params.end = end; + const { data } = await apiClient.get(`/api/equity/${symbol}/prices`, { params }); + return data; + }, + enabled: !!symbol, + staleTime: 5 * 60_000, + }); +} diff --git a/frontend/src/hooks/useSummary.ts b/frontend/src/hooks/useSummary.ts new file mode 100644 index 0000000..cd03d64 --- /dev/null +++ b/frontend/src/hooks/useSummary.ts @@ -0,0 +1,15 @@ +import { useQuery } from '@tanstack/react-query'; +import apiClient from '../api/client'; +import type { SummaryResponse } from '../types'; + +export function useSummary(symbol: string) { + return useQuery({ + queryKey: ['summary', symbol], + queryFn: async (): Promise => { + const { data } = await apiClient.get(`/api/equity/${symbol}/summary`); + return data; + }, + enabled: !!symbol, + staleTime: 60_000, + }); +} diff --git a/frontend/src/hooks/useTickers.ts b/frontend/src/hooks/useTickers.ts new file mode 100644 index 0000000..53e2635 --- /dev/null +++ b/frontend/src/hooks/useTickers.ts @@ -0,0 +1,35 @@ +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import apiClient from '../api/client'; +import type { TickerResponse } from '../types'; + +export function useTickers() { + return useQuery({ + queryKey: ['tickers'], + queryFn: async (): Promise => { + const { data } = await apiClient.get('/api/tickers'); + return data; + }, + staleTime: 30_000, + }); +} + +export function useAddTicker() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async (symbol: string): Promise => { + const { data } = await apiClient.post('/api/tickers', { symbol }); + return data; + }, + onSuccess: () => qc.invalidateQueries({ queryKey: ['tickers'] }), + }); +} + +export function useRemoveTicker() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async (symbol: string): Promise => { + await apiClient.delete(`/api/tickers/${symbol}`); + }, + onSuccess: () => qc.invalidateQueries({ queryKey: ['tickers'] }), + }); +} diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..37681df --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,15 @@ +@import "tailwindcss"; + +@theme { + --font-sans: 'Inter', sans-serif; + --font-mono: 'Fira Code', monospace; + + --color-surface-primary: #020617; + --color-surface-secondary: #0F172A; + --color-surface-tertiary: #1E293B; + --color-border-primary: #334155; + --color-border-hover: #475569; + --color-accent: #3B82F6; + --color-bullish: #22C55E; + --color-bearish: #EF4444; +} diff --git a/frontend/src/lib/constants.ts b/frontend/src/lib/constants.ts new file mode 100644 index 0000000..1af527d --- /dev/null +++ b/frontend/src/lib/constants.ts @@ -0,0 +1,25 @@ +export const CHART_COLORS = { + bullish: '#22C55E', + bullishWick: '#16A34A', + bearish: '#EF4444', + bearishWick: '#DC2626', + volume: 'rgba(59, 130, 246, 0.4)', + background: '#020617', + grid: '#1E293B', + crosshair: 'rgba(148, 163, 184, 0.5)', + sma: ['#3B82F6', '#F59E0B', '#8B5CF6'], + ema: ['#06B6D4', '#F97316'], +} as const; + +export const TIME_RANGES = ['1M', '3M', '6M', '1Y', '5Y', 'ALL'] as const; + +export function getStartDateForRange(range: string): string | undefined { + const now = new Date(); + const map: Record = { + '1M': 30, '3M': 90, '6M': 180, '1Y': 365, '5Y': 1825, + }; + const days = map[range]; + if (!days) return undefined; + const start = new Date(now.getTime() - days * 86400000); + return start.toISOString().split('T')[0]; +} diff --git a/frontend/src/lib/formatters.ts b/frontend/src/lib/formatters.ts new file mode 100644 index 0000000..ec5b263 --- /dev/null +++ b/frontend/src/lib/formatters.ts @@ -0,0 +1,26 @@ +export function formatPrice(value: number): string { + return value.toLocaleString('en-US', { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }); +} + +export function formatVolume(value: number): string { + if (value >= 1_000_000_000) return `${(value / 1_000_000_000).toFixed(1)}B`; + if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`; + if (value >= 1_000) return `${(value / 1_000).toFixed(1)}K`; + return value.toString(); +} + +export function formatPercent(value: number): string { + const sign = value >= 0 ? '+' : ''; + return `${sign}${value.toFixed(2)}%`; +} + +export function formatDate(dateStr: string): string { + return new Date(dateStr).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + }); +} diff --git a/frontend/src/lib/indicators.ts b/frontend/src/lib/indicators.ts new file mode 100644 index 0000000..a557442 --- /dev/null +++ b/frontend/src/lib/indicators.ts @@ -0,0 +1,64 @@ +import { SMA, EMA, RSI, MACD, BollingerBands } from 'technicalindicators'; + +interface TimeValue { + time: string; + value: number; +} + +export function computeSMA(closes: number[], dates: string[], period: number): TimeValue[] { + const values = SMA.calculate({ period, values: closes }); + const offset = closes.length - values.length; + return values.map((value, i) => ({ time: dates[i + offset], value })); +} + +export function computeEMA(closes: number[], dates: string[], period: number): TimeValue[] { + const values = EMA.calculate({ period, values: closes }); + const offset = closes.length - values.length; + return values.map((value, i) => ({ time: dates[i + offset], value })); +} + +export function computeRSI(closes: number[], dates: string[], period: number): TimeValue[] { + const values = RSI.calculate({ period, values: closes }); + const offset = closes.length - values.length; + return values.map((value, i) => ({ time: dates[i + offset], value })); +} + +export function computeMACD( + closes: number[], + dates: string[], + fastPeriod: number, + slowPeriod: number, + signalPeriod: number +) { + const results = MACD.calculate({ + values: closes, + fastPeriod, + slowPeriod, + signalPeriod, + SimpleMAOscillator: false, + SimpleMASignal: false, + }); + const offset = closes.length - results.length; + return results.map((r, i) => ({ + time: dates[i + offset], + macd: r.MACD ?? 0, + signal: r.signal ?? 0, + histogram: r.histogram ?? 0, + })); +} + +export function computeBollingerBands( + closes: number[], + dates: string[], + period: number, + stdDev: number +) { + const results = BollingerBands.calculate({ period, values: closes, stdDev }); + const offset = closes.length - results.length; + return results.map((r, i) => ({ + time: dates[i + offset], + upper: r.upper, + middle: r.middle, + lower: r.lower, + })); +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..1764e35 --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,22 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import './index.css' +import App from './App' + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: 1, + refetchOnWindowFocus: false, + }, + }, +}) + +createRoot(document.getElementById('root')!).render( + + + + + , +) diff --git a/frontend/src/pages/Chart.tsx b/frontend/src/pages/Chart.tsx new file mode 100644 index 0000000..b4d173b --- /dev/null +++ b/frontend/src/pages/Chart.tsx @@ -0,0 +1,139 @@ +import { useMemo } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { CandlestickChart } from '../components/chart/CandlestickChart'; +import { ChartToolbar } from '../components/chart/ChartToolbar'; +import { SymbolHeader } from '../components/chart/SymbolHeader'; +import { IndicatorPanel } from '../components/chart/IndicatorPanel'; +import { Skeleton } from '../components/common/Skeleton'; +import { Card } from '../components/common/Card'; +import { usePrices } from '../hooks/usePrices'; +import { useTickers } from '../hooks/useTickers'; +import { useUIStore } from '../stores/uiStore'; +import { getStartDateForRange } from '../lib/constants'; +import { + computeSMA, + computeEMA, + computeRSI, + computeMACD, + computeBollingerBands, +} from '../lib/indicators'; +import { BarChart3, AlertCircle, RefreshCw } from 'lucide-react'; + +export default function Chart() { + const { symbol } = useParams<{ symbol: string }>(); + const navigate = useNavigate(); + const chartTimeRange = useUIStore((s) => s.chartTimeRange); + const selectedIndicators = useUIStore((s) => s.selectedIndicators); + const startDate = getStartDateForRange(chartTimeRange); + + const { data: tickers } = useTickers(); + const symbols = tickers?.map((t) => t.symbol) ?? []; + + const { + data: priceData, + isLoading, + error, + refetch, + } = usePrices(symbol ?? '', startDate); + + const closes = useMemo(() => priceData?.data.map((r) => r.close) ?? [], [priceData]); + const dates = useMemo(() => priceData?.data.map((r) => r.date) ?? [], [priceData]); + + const overlays = useMemo(() => { + if (closes.length === 0) return []; + + const result: { data: { time: string; value: number }[]; color: string }[] = []; + + for (const ind of selectedIndicators) { + if (!ind.enabled) continue; + if (ind.type === 'sma') { + result.push({ data: computeSMA(closes, dates, ind.params.period), color: ind.color! }); + } else if (ind.type === 'ema') { + result.push({ data: computeEMA(closes, dates, ind.params.period), color: ind.color! }); + } else if (ind.type === 'bollinger') { + const bb = computeBollingerBands(closes, dates, ind.params.period, ind.params.stdDev); + result.push({ data: bb.map((b) => ({ time: b.time, value: b.upper })), color: ind.color! }); + result.push({ data: bb.map((b) => ({ time: b.time, value: b.middle })), color: ind.color! }); + result.push({ data: bb.map((b) => ({ time: b.time, value: b.lower })), color: ind.color! }); + } + } + return result; + }, [closes, dates, selectedIndicators]); + + const rsiConfig = selectedIndicators.find((i) => i.type === 'rsi' && i.enabled); + const rsiData = useMemo( + () => (rsiConfig && closes.length > 0 ? computeRSI(closes, dates, rsiConfig.params.period) : null), + [rsiConfig, closes, dates], + ); + + const macdConfig = selectedIndicators.find((i) => i.type === 'macd' && i.enabled); + const macdData = useMemo( + () => + macdConfig && closes.length > 0 + ? computeMACD(closes, dates, macdConfig.params.fast, macdConfig.params.slow, macdConfig.params.signal) + : null, + [macdConfig, closes, dates], + ); + + const handleSymbolChange = (s: string) => { + if (s) navigate(`/chart/${s}`); + else navigate('/chart'); + }; + + return ( +
+ + + {!symbol && ( + + +

Select a ticker to begin charting

+

+ Choose a symbol from the dropdown above +

+
+ )} + + {symbol && isLoading && ( +
+
+ + + +
+ +
+ )} + + {symbol && error && ( + + +

Failed to load price data

+

+ {error instanceof Error ? error.message : 'Unknown error'} +

+ +
+ )} + + {symbol && !isLoading && !error && priceData && ( +
+ + + {rsiData && } + {macdData && } +
+ )} +
+ ); +} diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx new file mode 100644 index 0000000..67eeb5c --- /dev/null +++ b/frontend/src/pages/Dashboard.tsx @@ -0,0 +1,82 @@ +import { useState } from 'react'; +import { Plus, AlertCircle, BarChart3 } from 'lucide-react'; +import { useTickers } from '../hooks/useTickers'; +import { TickerCard } from '../components/ticker/TickerCard'; +import { AddTickerModal } from '../components/ticker/AddTickerModal'; +import { Card } from '../components/common/Card'; +import { Skeleton } from '../components/common/Skeleton'; + +export default function Dashboard() { + const [modalOpen, setModalOpen] = useState(false); + const { data: tickers, isLoading, error, refetch } = useTickers(); + + return ( +
+
+

Market Overview

+ +
+ + {isLoading && ( +
+ {Array.from({ length: 6 }).map((_, i) => ( + + + + + + + + + ))} +
+ )} + + {error && ( + + +

Failed to load tickers

+ +
+ )} + + {!isLoading && !error && tickers?.length === 0 && ( + + +

No tickers yet

+

+ Add your first ticker to get started. +

+ +
+ )} + + {!isLoading && !error && tickers && tickers.length > 0 && ( +
+ {tickers.map((t) => ( + + ))} +
+ )} + + setModalOpen(false)} /> +
+ ); +} diff --git a/frontend/src/pages/Portfolio.tsx b/frontend/src/pages/Portfolio.tsx new file mode 100644 index 0000000..c76ce2a --- /dev/null +++ b/frontend/src/pages/Portfolio.tsx @@ -0,0 +1,16 @@ +import { Briefcase } from 'lucide-react'; +import { Card } from '../components/common/Card'; + +export default function Portfolio() { + return ( +
+ + +

Portfolio Coming Soon

+

+ Track your holdings, monitor P&L, and view allocation breakdowns. +

+
+
+ ); +} diff --git a/frontend/src/pages/Screener.tsx b/frontend/src/pages/Screener.tsx new file mode 100644 index 0000000..f80b83d --- /dev/null +++ b/frontend/src/pages/Screener.tsx @@ -0,0 +1,16 @@ +import { SlidersHorizontal } from 'lucide-react'; +import { Card } from '../components/common/Card'; + +export default function Screener() { + return ( +
+ + +

Screener Coming Soon

+

+ Screen stocks by technical and fundamental criteria. +

+
+
+ ); +} diff --git a/frontend/src/pages/Watchlist.tsx b/frontend/src/pages/Watchlist.tsx new file mode 100644 index 0000000..70b4edf --- /dev/null +++ b/frontend/src/pages/Watchlist.tsx @@ -0,0 +1,214 @@ +import { useState, useMemo } from 'react'; +import { Plus, ChevronUp, ChevronDown, ListX, AlertTriangle, RefreshCw } from 'lucide-react'; +import { useTickers, useRemoveTicker } from '../hooks/useTickers'; +import { TickerRow } from '../components/ticker/TickerRow'; +import { AddTickerModal } from '../components/ticker/AddTickerModal'; +import { Card } from '../components/common/Card'; +import { Skeleton } from '../components/common/Skeleton'; + +type SortField = 'symbol' | 'last' | 'change' | 'volume' | 'range'; +type SortDirection = 'asc' | 'desc'; + +const COLUMNS: { key: SortField; label: string }[] = [ + { key: 'symbol', label: 'Symbol' }, + { key: 'last', label: 'Last' }, + { key: 'change', label: 'Change' }, + { key: 'volume', label: 'Volume' }, + { key: 'range', label: '52w Range' }, +]; + +export default function Watchlist() { + const { data: tickers, isLoading, isError, refetch } = useTickers(); + const removeTicker = useRemoveTicker(); + + const [modalOpen, setModalOpen] = useState(false); + const [sortField, setSortField] = useState('symbol'); + const [sortDirection, setSortDirection] = useState('asc'); + const [pendingRemove, setPendingRemove] = useState(null); + + function handleSort(field: SortField) { + if (sortField === field) { + setSortDirection((d) => (d === 'asc' ? 'desc' : 'asc')); + } else { + setSortField(field); + setSortDirection('asc'); + } + } + + const sortedSymbols = useMemo(() => { + if (!tickers) return []; + const symbols = tickers.map((t) => t.symbol); + if (sortField === 'symbol') { + symbols.sort((a, b) => + sortDirection === 'asc' ? a.localeCompare(b) : b.localeCompare(a), + ); + } + return symbols; + }, [tickers, sortField, sortDirection]); + + async function confirmRemove() { + if (!pendingRemove) return; + try { + await removeTicker.mutateAsync(pendingRemove); + } finally { + setPendingRemove(null); + } + } + + if (isLoading) { + return ( +
+
+

Watchlist

+
+
+ + + + {COLUMNS.map((col) => ( + + ))} + + + + {Array.from({ length: 5 }).map((_, i) => ( + + + + + + + + + ))} + +
+ {col.label} + +
+
+
+ ); + } + + if (isError) { + return ( +
+

Watchlist

+
+ + +

Failed to load watchlist

+

Something went wrong. Please try again.

+ +
+
+
+ ); + } + + if (!tickers || tickers.length === 0) { + return ( +
+

Watchlist

+
+ + +

Your watchlist is empty

+

+ Add tickers to start tracking stocks. +

+ +
+
+ setModalOpen(false)} /> +
+ ); + } + + return ( +
+
+

Watchlist

+ +
+ + {pendingRemove && ( +
+ + Remove {pendingRemove}? + + + +
+ )} + +
+ + + + {COLUMNS.map((col) => ( + + ))} + + + + {sortedSymbols.map((symbol) => ( + + ))} + +
handleSort(col.key)} + > + + {col.label} + {sortField === col.key && + (sortDirection === 'asc' ? ( + + ) : ( + + ))} + + +
+
+ + setModalOpen(false)} /> +
+ ); +} diff --git a/frontend/src/stores/uiStore.ts b/frontend/src/stores/uiStore.ts new file mode 100644 index 0000000..ee1e19c --- /dev/null +++ b/frontend/src/stores/uiStore.ts @@ -0,0 +1,47 @@ +import { create } from 'zustand'; +import type { IndicatorConfig } from '../types'; + +const DEFAULT_INDICATORS: IndicatorConfig[] = [ + { type: 'sma', params: { period: 20 }, enabled: false, color: '#3B82F6' }, + { type: 'sma', params: { period: 50 }, enabled: false, color: '#F59E0B' }, + { type: 'sma', params: { period: 200 }, enabled: false, color: '#8B5CF6' }, + { type: 'ema', params: { period: 12 }, enabled: false, color: '#06B6D4' }, + { type: 'ema', params: { period: 26 }, enabled: false, color: '#F97316' }, + { type: 'rsi', params: { period: 14 }, enabled: false }, + { type: 'macd', params: { fast: 12, slow: 26, signal: 9 }, enabled: false }, + { type: 'bollinger', params: { period: 20, stdDev: 2 }, enabled: false, color: '#6B7280' }, +]; + +interface UIState { + sidebarCollapsed: boolean; + sidebarMobileOpen: boolean; + selectedIndicators: IndicatorConfig[]; + chartTimeRange: '1M' | '3M' | '6M' | '1Y' | '5Y' | 'ALL'; + toggleSidebar: () => void; + setSidebarMobileOpen: (open: boolean) => void; + toggleIndicator: (index: number) => void; + updateIndicatorParams: (index: number, params: Record) => void; + setTimeRange: (range: UIState['chartTimeRange']) => void; +} + +export const useUIStore = create((set) => ({ + sidebarCollapsed: false, + sidebarMobileOpen: false, + selectedIndicators: DEFAULT_INDICATORS, + chartTimeRange: '1Y', + toggleSidebar: () => set((s) => ({ sidebarCollapsed: !s.sidebarCollapsed })), + setSidebarMobileOpen: (open) => set({ sidebarMobileOpen: open }), + toggleIndicator: (index) => + set((s) => { + const indicators = [...s.selectedIndicators]; + indicators[index] = { ...indicators[index], enabled: !indicators[index].enabled }; + return { selectedIndicators: indicators }; + }), + updateIndicatorParams: (index, params) => + set((s) => { + const indicators = [...s.selectedIndicators]; + indicators[index] = { ...indicators[index], params }; + return { selectedIndicators: indicators }; + }), + setTimeRange: (range) => set({ chartTimeRange: range }), +})); diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts new file mode 100644 index 0000000..787dcc1 --- /dev/null +++ b/frontend/src/types/index.ts @@ -0,0 +1,55 @@ +export interface OHLCVRow { + date: string; + open: number; + high: number; + low: number; + close: number; + volume: number; + vwap: number; + split_ratio: number; + dividend: number; +} + +export interface TickerInfo { + added: string; + last_ingested: string | null; +} + +export interface TickerResponse { + symbol: string; + info: TickerInfo; +} + +export interface PriceResponse { + symbol: string; + count: number; + data: OHLCVRow[]; +} + +export interface SummaryResponse { + symbol: string; + latest_date: string; + latest_close: number; + high_52w: number; + low_52w: number; + avg_volume_30d: number; + total_rows: number; +} + +export interface IngestResult { + symbol: string; + rows_fetched: number; + rows_written: number; + status: string; +} + +export interface IngestResponse { + results: IngestResult[]; +} + +export interface IndicatorConfig { + type: 'sma' | 'ema' | 'rsi' | 'macd' | 'bollinger'; + params: Record; + enabled: boolean; + color?: string; +} diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json new file mode 100644 index 0000000..a9b5a59 --- /dev/null +++ b/frontend/tsconfig.app.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000..8a67f62 --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..bcdc12a --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import tailwindcss from '@tailwindcss/vite' +import path from 'path' + +export default defineConfig({ + plugins: [react(), tailwindcss()], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, + server: { + proxy: { + '/api': 'http://localhost:8000', + '/health': 'http://localhost:8000', + }, + }, +}) diff --git a/src/flow/api/app.py b/src/flow/api/app.py index 15503e8..43b8b52 100644 --- a/src/flow/api/app.py +++ b/src/flow/api/app.py @@ -1,7 +1,13 @@ +from pathlib import Path + from fastapi import FastAPI +from fastapi.staticfiles import StaticFiles +from starlette.responses import FileResponse from flow.api.routes.equity import router as equity_router +STATIC_DIR = Path(__file__).resolve().parents[3] / "static" + def create_app() -> FastAPI: app = FastAPI( @@ -15,6 +21,16 @@ def create_app() -> FastAPI: async def health(): return {"status": "ok"} + if STATIC_DIR.is_dir(): + app.mount("/assets", StaticFiles(directory=STATIC_DIR / "assets"), name="assets") + + @app.get("/{full_path:path}") + async def serve_spa(full_path: str): + file_path = STATIC_DIR / full_path + if file_path.is_file(): + return FileResponse(file_path) + return FileResponse(STATIC_DIR / "index.html") + return app