diff --git a/.env.dev b/.env.dev deleted file mode 100644 index 799b692..0000000 --- a/.env.dev +++ /dev/null @@ -1,2 +0,0 @@ -DATABASE_URL=postgresql://dev:dev@db:5432/myapp_dev -DEBUG=true \ No newline at end of file diff --git a/.env.example b/.env.example index 3e8772e..2a3255c 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,11 @@ -POSTGRES_USER= -POSTGRES_PASSWORD= -POSTGRES_DB= -DEBUG=false \ No newline at end of file +OPENROUTER_API_KEY= +OPENROUTER_APP_TITLE=TaskCraft_v1_0_0 +OPENROUTER_APP_REFERER=TaskCraft_v1_0_0 +OPENROUTER_MODEL=anthropic/claude-3.5-haiku +POSTGRES_USER=dev +POSTGRES_PASSWORD=dev +POSTGRES_DB=myapp_dev +POSTGRES_HOST=db +POSTGRES_PORT=5432 +DEBUG=false +LOG_LEVEL=INFO \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2efc28d..17afa73 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -20,7 +20,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v6 with: - python-version: '3.10' + python-version: '3.12' - name: Install uv uses: astral-sh/setup-uv@v7 @@ -41,15 +41,22 @@ jobs: - name: Create test environment file run: | - cat > .env.test < .env.ci < str: + return ( + f"postgresql+asyncpg://{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}" + f"@{self.POSTGRES_HOST}:{self.POSTGRES_PORT}/{self.POSTGRES_DB}" + ) + + # OpenRouter + OPENROUTER_API_KEY: Optional[str] = os.getenv("OPENROUTER_API_KEY") + OPENROUTER_APP_TITLE: str = os.getenv("OPENROUTER_APP_TITLE", "TaskCraft") + OPENROUTER_APP_REFERER: str = os.getenv("OPENROUTER_APP_REFERER", "TaskCraft") + OPENROUTER_MODEL: str = os.getenv("OPENROUTER_MODEL", "anthropic/claude-3.5-haiku") + + # Logging + LOG_LEVEL: str = os.getenv("LOG_LEVEL", "INFO") + DEBUG: bool = os.getenv("DEBUG", "false").lower() == "true" + + +settings = Settings() diff --git a/app/database.py b/app/database.py new file mode 100644 index 0000000..6335732 --- /dev/null +++ b/app/database.py @@ -0,0 +1,29 @@ +from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker +from sqlalchemy.orm import declarative_base +from app.config import settings + +# Create async engine +engine = create_async_engine( + settings.DATABASE_URL, + echo=settings.DEBUG, + future=True, +) + +# Create async session factory +AsyncSessionLocal = async_sessionmaker( + engine, + class_=AsyncSession, + expire_on_commit=False, +) + +# Base class for models +Base = declarative_base() + + +async def get_db(): + """Dependency for getting database session""" + async with AsyncSessionLocal() as session: + try: + yield session + finally: + await session.close() diff --git a/app/main.py b/app/main.py index d945ba1..4be38c1 100644 --- a/app/main.py +++ b/app/main.py @@ -1,10 +1,32 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware import logging +from contextlib import asynccontextmanager +from app.config import settings +from app.database import engine -app = FastAPI() logger = logging.getLogger(__name__) +# Configure logging level from environment variable +logging.basicConfig( + level=getattr(logging, settings.LOG_LEVEL.upper(), logging.INFO), + format="%(asctime)s | %(levelname)-8s | %(module)s:%(funcName)s:%(lineno)d - %(message)s" +) + + +@asynccontextmanager +async def lifespan(app: FastAPI): + """Manage application lifecycle""" + logger.info("Starting up application...") + logger.info(f"Database URL: {settings.DATABASE_URL.split('@')[1]}") # Log DB host (not credentials) + yield + logger.info("Shutting down application...") + await engine.dispose() + + +app = FastAPI(lifespan=lifespan) + + # CORS if needed during development app.add_middleware( CORSMiddleware, @@ -15,10 +37,60 @@ @app.get("/api/health") async def health(): - return {"status": "ok"} + """Health check endpoint with database connectivity test""" + from sqlalchemy import text + from app.database import AsyncSessionLocal + + db_status = "unknown" + try: + async with AsyncSessionLocal() as session: + result = await session.execute(text("SELECT 1")) + result.scalar() + db_status = "connected" + except Exception as e: + logger.error(f"Database health check failed: {e}") + db_status = "disconnected" + + return { + "status": "ok", + "database": db_status + } + +from pydantic import BaseModel, Field +from typing import Optional, List +from datetime import datetime +from app.services.event_extractor import extract_event_from_text +from fastapi import HTTPException +from app.services.openrouter_client import OpenRouterError + +class TaskCreateRequest(BaseModel): + text: str = Field(..., min_length=1) + email: Optional[str] = None + timezone: str = "Asia/Jerusalem" + +class EventDraft(BaseModel): + title: str + start_at: Optional[datetime] = None + end_at: Optional[datetime] = None + notes: str = "" + missing_info: List[str] = [] + +class TaskCreateResponse(BaseModel): + raw_text: str + event: EventDraft + +@app.post("/api/tasks", response_model=TaskCreateResponse) +async def create_task(payload: TaskCreateRequest): + logger.info("Creating task from text: %s" % payload.text) + try: + event_dict = await extract_event_from_text(payload.text, payload.timezone) + except OpenRouterError as e: + logger.error('Failed to send OpenRouter request: %s' % str(e)) + raise HTTPException(status_code=503, detail=str(e)) + + logger.info("Response: %s" % str(event_dict)) + return TaskCreateResponse( + raw_text=payload.text, + event=EventDraft(**event_dict), + ) -# Your API routes here -@app.get("/api/items") -async def get_items(): - logger.info('Fetching items') - return {"items": ['apple']} \ No newline at end of file diff --git a/app/schemas/__init__.py b/app/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/services/__init__.py b/app/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/services/event_extractor.py b/app/services/event_extractor.py new file mode 100644 index 0000000..ddad1bc --- /dev/null +++ b/app/services/event_extractor.py @@ -0,0 +1,74 @@ +import os +from datetime import datetime, timezone +from typing import Any, Dict + +from app.services.openrouter_client import openrouter_chat_completion, OpenRouterError + +EVENT_JSON_SCHEMA: Dict[str, Any] = { + "name": "task_event", + "strict": True, + "schema": { + "type": "object", + "additionalProperties": False, + "properties": { + "title": {"type": "string", "minLength": 1}, + "start_at": {"type": ["string", "null"], "description": "ISO 8601 with timezone"}, + "end_at": {"type": ["string", "null"], "description": "ISO 8601 with timezone"}, + "all_day": {"type": "boolean"}, + "notes": {"type": "string"}, + "missing_info": { + "type": "array", + "items": {"type": "string"}, + }, + "confidence": {"type": "number", "minimum": 0, "maximum": 1}, + }, + "required": ["title", "start_at", "end_at", "all_day", "notes", "missing_info", "confidence"], + }, +} + +def _iso_now_utc() -> str: + return datetime.now(timezone.utc).isoformat() + +async def extract_event_from_text(text: str, user_timezone: str) -> dict: + model = os.getenv("OPENROUTER_MODEL", "anthropic/claude-3.5-sonnet") # placeholder + + system = ( + "You convert a user's task text into a calendar event object.\n" + "Return ONLY the JSON that matches the provided schema.\n" + "If the text is missing date/time, put start_at/end_at as null and list required fields in missing_info.\n" + "Assume the user's timezone when interpreting relative times.\n" + ) + + user = ( + f"User timezone: {user_timezone}\n" + f"Current time (UTC): {_iso_now_utc()}\n" + f"Task text: {text}\n" + ) + + payload = { + "model": model, + "messages": [ + {"role": "system", "content": system}, + {"role": "user", "content": user}, + ], + # Structured outputs: json_schema with strict mode + "response_format": { + "type": "json_schema", + "json_schema": EVENT_JSON_SCHEMA, + }, + } + + data = await openrouter_chat_completion(payload) + + # OpenRouter returns OpenAI-style payload; content is usually string JSON + try: + content = data["choices"][0]["message"]["content"] + except Exception as e: + raise OpenRouterError(f"Unexpected OpenRouter response shape: {e}; data={data}") + + # If structured outputs works, content should already be JSON (string). Parse it: + import json + try: + return json.loads(content) + except json.JSONDecodeError as e: + raise OpenRouterError(f"Model did not return valid JSON: {e}. Raw content: {content}") diff --git a/app/services/openrouter_client.py b/app/services/openrouter_client.py new file mode 100644 index 0000000..8e9db0c --- /dev/null +++ b/app/services/openrouter_client.py @@ -0,0 +1,31 @@ +import os +import httpx + +OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions" + +class OpenRouterError(RuntimeError): + pass + +async def openrouter_chat_completion(payload: dict) -> dict: + api_key = os.getenv("OPENROUTER_API_KEY") + if not api_key: + raise OpenRouterError("Missing OPENROUTER_API_KEY") + + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + } + + # Optional app identification headers (recommended by OpenRouter) + app_title = os.getenv("OPENROUTER_APP_TITLE") + app_referer = os.getenv("OPENROUTER_APP_REFERER") + if app_title: + headers["X-Title"] = app_title + if app_referer: + headers["HTTP-Referer"] = app_referer + + async with httpx.AsyncClient(timeout=60) as client: + r = await client.post(OPENROUTER_URL, headers=headers, json=payload) + if r.status_code >= 400: + raise OpenRouterError(f"OpenRouter error {r.status_code}: {r.text}") + return r.json() diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 9345548..14448d6 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -1,9 +1,18 @@ -version: '3.8' - services: app: build: . - env_file: .env.dev + environment: + - POSTGRES_USER=${POSTGRES_USER:-dev} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-dev} + - POSTGRES_DB=${POSTGRES_DB:-myapp_dev} + - POSTGRES_HOST=${POSTGRES_HOST:-db} + - POSTGRES_PORT=${POSTGRES_PORT:-5432} + - OPENROUTER_API_KEY=${OPENROUTER_API_KEY:-} + - OPENROUTER_APP_TITLE=${OPENROUTER_APP_TITLE:-TaskCraft} + - OPENROUTER_APP_REFERER=${OPENROUTER_APP_REFERER:-TaskCraft} + - OPENROUTER_MODEL=${OPENROUTER_MODEL:-anthropic/claude-3.5-haiku} + - DEBUG=${DEBUG:-false} + - LOG_LEVEL=${LOG_LEVEL:-INFO} ports: - "8000:8000" depends_on: @@ -19,9 +28,9 @@ services: db: image: postgres:18-alpine environment: - - POSTGRES_USER=dev - - POSTGRES_PASSWORD=dev - - POSTGRES_DB=myapp_dev + - POSTGRES_USER=${POSTGRES_USER:-dev} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-dev} + - POSTGRES_DB=${POSTGRES_DB:-myapp_dev} ports: - "5432:5432" volumes: diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..167c567 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,28 @@ +## Usage + +```bash +$ npm install # or pnpm install or yarn install +``` + +### Learn more on the [Solid Website](https://solidjs.com) and come chat with us on our [Discord](https://discord.com/invite/solidjs) + +## Available Scripts + +In the project directory, you can run: + +### `npm run dev` + +Runs the app in the development mode.
+Open [http://localhost:5173](http://localhost:5173) to view it in the browser. + +### `npm run build` + +Builds the app for production to the `dist` folder.
+It correctly bundles Solid in production mode and optimizes the build for the best performance. + +The build is minified and the filenames include the hashes.
+Your app is ready to be deployed! + +## Deployment + +Learn more about deploying your application with the [documentations](https://vite.dev/guide/static-deploy.html) diff --git a/frontend/css/common.css b/frontend/css/common.css index 122fc99..aa756db 100644 --- a/frontend/css/common.css +++ b/frontend/css/common.css @@ -1,8 +1,7 @@ /* ============================================ - IMPORT THEME VARIABLES - Import themes.css for all color and design token variables + BASE LAYOUT & TYPOGRAPHY + Note: Theme variables are imported via index.css ============================================ */ -@import url('themes.css'); /* ============================================ RESET & BASE STYLES @@ -45,7 +44,7 @@ body::before { } /* ============================================ - LAYOUT + PAGE LAYOUT ============================================ */ .page-wrapper { display: flex; @@ -56,6 +55,9 @@ body::before { z-index: 1; } +/* ============================================ + HEADER + ============================================ */ .header { display: flex; align-items: center; @@ -107,6 +109,9 @@ body::before { background-repeat: no-repeat; } +/* ============================================ + MAIN CONTAINER + ============================================ */ .main-container { flex: 1; display: flex; @@ -167,407 +172,6 @@ body::before { color: var(--color-text-primary); } -/* ============================================ - CARDS - ============================================ */ -.card { - background-color: var(--color-surface-dark); - border-radius: var(--radius-2xl); - padding: 1.5rem; - border: 1px solid var(--color-border); - position: relative; -} - -.card-input { - padding: 2rem; -} - -.card-primary { - background-color: var(--color-primary); - color: var(--color-text-dark); - overflow: hidden; -} - -.card-header { - padding: 1.25rem; - border-bottom: 1px solid var(--color-border); - display: flex; - justify-content: space-between; - align-items: center; -} - -/* ============================================ - BUTTONS - ============================================ */ -.btn { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 0.5rem; - border-radius: var(--radius-full); - font-weight: 700; - cursor: pointer; - border: none; - transition: all var(--transition-base); - white-space: nowrap; - font-family: inherit; -} - -.btn-icon { - width: 2.5rem; - height: 2.5rem; - padding: 0; -} - -.btn-sm { - height: 3rem; - padding: 0 1.5rem; - font-size: var(--text-sm); - letter-spacing: 0.025em; -} - -.btn-md { - height: 3rem; - padding: 0 1.5rem; - font-size: var(--text-base); -} - -.btn-primary { - background-color: var(--color-primary); - color: var(--color-text-dark); - box-shadow: var(--shadow-primary); -} - -.btn-primary:hover { - background-color: var(--color-primary-hover); -} - -.btn-primary:active { - transform: scale(0.95); -} - -.btn-secondary { - background-color: var(--color-surface-highlight); - color: var(--color-text-primary); -} - -.btn-secondary:hover { - background-color: var(--color-surface-highlight-hover); -} - -.btn-outline { - background-color: transparent; - border: 1px solid var(--color-border); - color: var(--color-text-secondary); -} - -.btn-outline:hover { - background-color: var(--color-surface-highlight-alpha-30); - color: var(--color-text-primary); - border-color: var(--color-primary); -} - -.btn-dark { - background-color: rgba(35, 30, 16, 0.1); - color: var(--color-text-dark); -} - -.btn-dark:hover { - background-color: rgba(0, 0, 0, 0.8); - color: var(--color-text-primary); -} - -.btn-text { - color: var(--color-text-secondary); - font-size: var(--text-xs); - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.05em; - background: none; - padding: 0; - height: auto; -} - -.btn-text:hover { - color: var(--color-primary); -} - -/* ============================================ - FORM ELEMENTS - ============================================ */ -.form-group { - display: flex; - flex-direction: column; - gap: 1rem; - width: 100%; -} - -.form-label { - font-size: var(--text-lg); - font-weight: 700; - color: var(--color-text-primary); -} - -.input-wrapper { - position: relative; - width: 100%; -} - -.textarea { - width: 100%; - min-height: 160px; - resize: none; - border-radius: var(--radius-xl); - background-color: var(--color-surface-highlight); - color: var(--color-text-primary); - border: 2px solid transparent; - padding: 1.5rem; - font-size: var(--text-xl); - font-weight: 500; - line-height: 1.6; - font-family: inherit; - transition: border-color var(--transition-base); -} - -.textarea::placeholder { - color: var(--color-text-placeholder); -} - -.textarea:focus { - outline: none; - border-color: var(--color-primary); -} - -.input-actions { - position: absolute; - bottom: 1rem; - right: 1rem; - display: flex; - align-items: center; - gap: 0.75rem; -} - -.input-hint { - font-size: var(--text-xs); - color: var(--color-text-secondary); - font-weight: 500; - text-transform: uppercase; - letter-spacing: 0.05em; - display: none; -} - -/* ============================================ - DECORATIVE ELEMENTS - ============================================ */ -.blob-decorator { - position: absolute; - top: -1rem; - right: -1rem; - width: 6rem; - height: 6rem; - background-color: var(--color-primary-light); - border-radius: var(--radius-full); - filter: blur(2rem); - pointer-events: none; -} - -.icon-decorator { - position: absolute; - top: 0; - right: 0; - padding: 2rem; - opacity: 0.1; - transform: translate(1rem, -1rem); - font-size: 120px; -} - -/* ============================================ - MOBILE CARD - ============================================ */ -.mobile-card-content { - position: relative; - z-index: 10; - display: flex; - flex-direction: column; - gap: 0.75rem; -} - -.icon-badge { - width: 3rem; - height: 3rem; - border-radius: var(--radius-full); - background-color: rgba(35, 30, 16, 0.1); - display: flex; - align-items: center; - justify-content: center; - margin-bottom: 0.5rem; -} - -.mobile-card-title { - font-size: var(--text-2xl); - font-weight: 900; - line-height: 1.2; -} - -.mobile-card-text { - font-size: var(--text-sm); - font-weight: 500; - color: rgba(35, 30, 16, 0.8); - line-height: 1.4; -} - -/* ============================================ - LIST ITEMS - ============================================ */ -.list-container { - display: flex; - flex-direction: column; - gap: 0.75rem; - padding: 1rem; - overflow-y: auto; - max-height: 500px; -} - -.list-item { - display: flex; - flex-direction: column; - gap: 0.5rem; - padding: 0.75rem; - border-radius: var(--radius-xl); - background-color: var(--color-surface-highlight-alpha-30); - border: 1px solid transparent; - cursor: pointer; - transition: all var(--transition-base); -} - -.list-item:hover { - background-color: var(--color-surface-highlight); - border-color: rgba(244, 192, 37, 0.3); -} - -.list-item-header { - display: flex; - align-items: center; - justify-content: space-between; -} - -.list-item-meta { - display: flex; - align-items: center; - gap: 0.5rem; -} - -.list-item-text { - font-size: var(--text-sm); - font-weight: 500; - color: var(--color-text-primary); - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; -} - -.list-item-footer { - display: flex; - align-items: center; - gap: 0.5rem; - font-size: var(--text-xs); - color: var(--color-text-secondary); -} - -.divider { - height: 1px; - width: 100%; - background-color: var(--color-divider); - margin: 0.25rem 0; -} - -/* ============================================ - BADGES - ============================================ */ -.badge { - display: inline-flex; - align-items: center; - font-size: 10px; - font-weight: 700; - padding: 0.125rem 0.5rem; - border-radius: var(--radius-full); - text-transform: uppercase; - letter-spacing: 0.025em; -} - -.badge-task { - background-color: var(--color-status-green-bg); - color: var(--color-status-green); -} - -.badge-event { - background-color: var(--color-status-blue-bg); - color: var(--color-status-blue); -} - -.badge-reminder { - background-color: var(--color-status-purple-bg); - color: var(--color-status-purple); -} - -.timestamp { - font-size: var(--text-xs); - color: var(--color-text-secondary); -} - -/* ============================================ - ICONS - ============================================ */ -.material-symbols-outlined { - font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24; -} - -.icon-sm { - font-size: 14px; -} - -.icon-md { - font-size: 18px; -} - -.icon-lg { - font-size: 20px; -} - -.icon-xl { - font-size: 24px; -} - -.chevron-icon { - color: var(--color-text-secondary); - transition: color var(--transition-fast); -} - -.list-item:hover .chevron-icon { - color: var(--color-primary); -} - -/* ============================================ - SCROLLBAR - ============================================ */ -.custom-scrollbar::-webkit-scrollbar { - width: 6px; -} - -.custom-scrollbar::-webkit-scrollbar-track { - background: transparent; -} - -.custom-scrollbar::-webkit-scrollbar-thumb { - background-color: var(--color-surface-highlight); - border-radius: var(--radius-full); -} - -.custom-scrollbar::-webkit-scrollbar-thumb:hover { - background-color: var(--color-surface-highlight-hover); -} - /* ============================================ FOOTER ============================================ */ @@ -625,7 +229,32 @@ body::before { color: var(--color-primary); } +/* ============================================ + RESPONSIVE DESIGN + ============================================ */ + +/* Tablet (768px and up) */ @media (min-width: 768px) { + .header { + padding: 1rem 2.5rem; + } + + .header-buttons { + display: flex; + } + + .main-container { + padding: 2.5rem; + } + + .heading-hero { + font-size: var(--text-6xl); + } + + .subtitle { + font-size: var(--text-xl); + } + .footer-content { flex-direction: row; justify-content: space-between; @@ -638,225 +267,11 @@ body::before { } } -/* ============================================ - UTILITY CLASSES - ============================================ */ -.flex { - display: flex; -} - -.flex-col { - flex-direction: column; -} - -.gap-2 { - gap: 0.5rem; -} - -.gap-3 { - gap: 0.75rem; -} - -.gap-4 { - gap: 1rem; -} - -.gap-6 { - gap: 1.5rem; -} - -.gap-8 { - gap: 2rem; -} - -.mt-2 { - margin-top: 0.5rem; -} - -.w-full { - width: 100%; -} - -/* ============================================ - MULTI-SELECT DROPDOWN - ============================================ */ -.multi-select { - position: relative; - width: 100%; -} - -.multi-select-trigger { - width: 100%; - display: flex; - align-items: center; - justify-content: space-between; - gap: 0.5rem; - padding: 0.75rem 1rem; - background-color: var(--color-surface-highlight); - border: 2px solid transparent; - border-radius: var(--radius-lg); - color: var(--color-text-primary); - font-size: var(--text-sm); - font-weight: 500; - cursor: pointer; - transition: all var(--transition-base); - font-family: inherit; -} - -.multi-select-trigger:hover { - background-color: var(--color-surface-highlight-hover); -} - -.multi-select.open .multi-select-trigger { - border-color: var(--color-primary); -} - -.multi-select-trigger-content { - flex: 1; - text-align: left; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.multi-select-trigger-content.placeholder { - color: var(--color-text-placeholder); -} - -.multi-select-chevron { - font-size: 20px; - color: var(--color-text-secondary); - transition: transform var(--transition-base); -} - -.multi-select.open .multi-select-chevron { - transform: rotate(180deg); -} - -.multi-select-dropdown { - position: absolute; - top: calc(100% + 0.5rem); - left: 0; - right: 0; - background-color: var(--color-surface-dark); - border: 1px solid var(--color-border); - border-radius: var(--radius-lg); - box-shadow: var(--shadow-lg); - z-index: 100; - max-height: 280px; - overflow-y: auto; - opacity: 0; - visibility: hidden; - transform: translateY(-0.5rem); - transition: all var(--transition-base); -} - -.multi-select.open .multi-select-dropdown { - opacity: 1; - visibility: visible; - transform: translateY(0); -} - -.multi-select-option { - display: flex; - align-items: center; - gap: 0.75rem; - padding: 0.75rem 1rem; - cursor: pointer; - transition: background-color var(--transition-fast); - font-size: var(--text-sm); - color: var(--color-text-primary); -} - -.multi-select-option:hover { - background-color: var(--color-surface-highlight-alpha-30); -} - -.multi-select-checkbox { - width: 1.25rem; - height: 1.25rem; - border: 2px solid var(--color-border); - border-radius: var(--radius-sm); - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - transition: all var(--transition-fast); -} - -.multi-select-checkbox .material-symbols-outlined { - font-size: 16px; - color: var(--color-text-dark); - opacity: 0; - transition: opacity var(--transition-fast); -} - -.multi-select-option.selected .multi-select-checkbox { - background-color: var(--color-primary); - border-color: var(--color-primary); -} - -.multi-select-option.selected .multi-select-checkbox .material-symbols-outlined { - opacity: 1; -} - -.multi-select-dropdown::-webkit-scrollbar { - width: 6px; -} - -.multi-select-dropdown::-webkit-scrollbar-track { - background: transparent; -} - -.multi-select-dropdown::-webkit-scrollbar-thumb { - background-color: var(--color-surface-highlight); - border-radius: var(--radius-full); -} - -.multi-select-dropdown::-webkit-scrollbar-thumb:hover { - background-color: var(--color-surface-highlight-hover); -} - -/* ============================================ - RESPONSIVE DESIGN - ============================================ */ - -/* Tablet (768px and up) */ -@media (min-width: 768px) { - .header { - padding: 1rem 2.5rem; - } - - .header-buttons { - display: flex; - } - - .main-container { - padding: 2.5rem; - } - - .heading-hero { - font-size: var(--text-6xl); - } - - .subtitle { - font-size: var(--text-xl); - } - - .card-input { - padding: 2rem; - } - - .input-hint { - display: block; - } -} - -/* Desktop (1024px and up) */ -@media (min-width: 1024px) { - .header { - padding: 1rem 2.5rem; - } +/* Desktop (1024px and up) */ +@media (min-width: 1024px) { + .header { + padding: 1rem 2.5rem; + } .main-container { padding: 3rem; @@ -875,895 +290,3 @@ body::before { padding: 3rem; } } - -/* ============================================ - ACTION BUTTONS ROW - ============================================ */ -.action-buttons { - display: flex; - flex-wrap: wrap; - gap: 1rem; - align-items: center; -} - -/* ============================================ - LEGAL MODALS - ============================================ */ -.legal-content { - display: flex; - flex-direction: column; - gap: 1.5rem; -} - -.legal-updated { - font-size: var(--text-sm); - color: var(--color-text-secondary); - font-style: italic; - padding-bottom: 1rem; - border-bottom: 1px solid var(--color-border); -} - -.legal-intro { - font-size: var(--text-base); - color: var(--color-text-secondary); - line-height: 1.6; - padding: 1rem; - background-color: var(--color-surface-highlight-alpha-30); - border-radius: var(--radius-lg); - border-left: 3px solid var(--color-primary); -} - -.legal-section { - padding-bottom: 1.5rem; - border-bottom: 1px solid var(--color-divider); -} - -.legal-section:last-child { - border-bottom: none; - padding-bottom: 0; -} - -.legal-section-title { - font-size: var(--text-lg); - font-weight: 700; - color: var(--color-text-primary); - margin-bottom: 0.75rem; -} - -.legal-section-content { - font-size: var(--text-sm); - color: var(--color-text-secondary); - line-height: 1.7; -} - -/* ============================================ - TOGGLE SWITCH - ============================================ */ -.toggle-switch { - position: relative; - display: inline-block; - width: 3rem; - height: 1.75rem; - cursor: pointer; - flex-shrink: 0; -} - -.toggle-switch input { - opacity: 0; - width: 0; - height: 0; -} - -.toggle-slider { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: var(--color-surface-highlight); - border: 2px solid var(--color-border); - border-radius: var(--radius-full); - transition: all var(--transition-base); -} - -.toggle-slider::before { - position: absolute; - content: ""; - height: 1.25rem; - width: 1.25rem; - left: 0.125rem; - bottom: 0.125rem; - background-color: var(--color-text-primary); - border-radius: var(--radius-full); - transition: all var(--transition-base); -} - -.toggle-switch input:checked + .toggle-slider { - background-color: var(--color-primary); - border-color: var(--color-primary); -} - -.toggle-switch input:checked + .toggle-slider::before { - transform: translateX(1.25rem); - background-color: var(--color-text-dark); -} - -.toggle-switch:hover .toggle-slider { - background-color: var(--color-surface-highlight-hover); -} - -.toggle-switch input:checked:hover + .toggle-slider { - background-color: var(--color-primary-hover); -} - -/* ============================================ - SETTINGS MODAL - ============================================ */ -.settings-content { - display: flex; - flex-direction: column; - gap: 2rem; -} - -.settings-section { - padding-bottom: 2rem; - border-bottom: 1px solid var(--color-border); -} - -.settings-section:last-child { - border-bottom: none; - padding-bottom: 0; -} - -.settings-section-title { - font-size: var(--text-xl); - font-weight: 700; - color: var(--color-text-primary); - margin-bottom: 1.5rem; -} - -.settings-section-content { - display: flex; - flex-direction: column; - gap: 1.25rem; -} - -.setting-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 1.5rem; - padding: 1rem; - background-color: var(--color-surface-highlight-alpha-30); - border-radius: var(--radius-lg); - transition: background-color var(--transition-fast); -} - -.setting-row:hover { - background-color: var(--color-surface-highlight); -} - -.setting-label { - display: flex; - align-items: center; - gap: 1rem; - flex: 1; - min-width: 0; -} - -.setting-label .material-symbols-outlined { - color: var(--color-text-secondary); - flex-shrink: 0; -} - -.setting-title { - font-size: var(--text-base); - font-weight: 600; - color: var(--color-text-primary); - margin-bottom: 0.25rem; -} - -.setting-description { - font-size: var(--text-sm); - color: var(--color-text-secondary); - line-height: 1.4; -} - -/* ============================================ - MODAL COMPONENT - ============================================ */ -.modal-overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgba(0, 0, 0, 0.7); - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; - padding: 1rem; - opacity: 0; - transition: opacity var(--transition-base); -} - -.modal-overlay.modal-open { - opacity: 1; -} - -.modal-container { - background-color: var(--color-surface-dark); - border-radius: var(--radius-2xl); - border: 1px solid var(--color-border); - max-width: 90vw; - max-height: 90vh; - display: flex; - flex-direction: column; - box-shadow: var(--shadow-lg); - transform: scale(0.9); - transition: transform var(--transition-base); -} - -.modal-open .modal-container { - transform: scale(1); -} - -.modal-small { - width: 400px; -} - -.modal-medium { - width: 600px; -} - -.modal-large { - width: 800px; -} - -.modal-full { - width: 95vw; - height: 95vh; -} - -.modal-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 1.5rem; - border-bottom: 1px solid var(--color-border); -} - -.modal-title { - font-size: var(--text-xl); - font-weight: 700; - color: var(--color-text-primary); -} - -.modal-close { - width: 2.5rem; - height: 2.5rem; - border-radius: var(--radius-full); - border: none; - background-color: var(--color-surface-highlight); - color: var(--color-text-primary); - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - transition: background-color var(--transition-fast); -} - -.modal-close:hover { - background-color: var(--color-surface-highlight-hover); -} - -.modal-body { - padding: 1.5rem; - overflow-y: auto; - flex: 1; -} - -/* ============================================ - PAGINATION COMPONENT - ============================================ */ -.pagination { - display: flex; - align-items: center; - justify-content: center; - gap: 0.5rem; - padding: 1.5rem 0; - flex-wrap: wrap; -} - -.pagination-btn { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.5rem 1rem; - border-radius: var(--radius-lg); - border: 1px solid var(--color-border); - background-color: var(--color-surface-dark); - color: var(--color-text-primary); - font-weight: 500; - cursor: pointer; - transition: all var(--transition-fast); -} - -.pagination-btn:hover:not(.disabled) { - background-color: var(--color-surface-highlight); - border-color: var(--color-primary); -} - -.pagination-btn.disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.pagination-pages { - display: flex; - gap: 0.25rem; -} - -.pagination-page { - min-width: 2.5rem; - height: 2.5rem; - border-radius: var(--radius-md); - border: 1px solid var(--color-border); - background-color: var(--color-surface-dark); - color: var(--color-text-primary); - font-weight: 500; - cursor: pointer; - transition: all var(--transition-fast); - display: flex; - align-items: center; - justify-content: center; -} - -.pagination-page:hover { - background-color: var(--color-surface-highlight); - border-color: var(--color-primary); -} - -.pagination-page.active { - background-color: var(--color-primary); - color: var(--color-text-dark); - border-color: var(--color-primary); -} - -.pagination-ellipsis { - display: flex; - align-items: center; - padding: 0 0.5rem; - color: var(--color-text-secondary); -} - -.pagination-info { - font-size: var(--text-sm); - color: var(--color-text-secondary); - margin-left: 1rem; -} - -/* ============================================ - FILTER SECTION - ============================================ */ -.filter-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 1.25rem; -} - -.filter-controls-compact { - display: grid; - grid-template-columns: 1fr; - gap: 0.75rem; -} - -.filter-group-compact { - display: flex; - flex-direction: column; - width: 100%; -} - -.filter-input { - width: 100%; - padding: 0.75rem 1rem; - border-radius: var(--radius-lg); - border: 2px solid transparent; - background-color: var(--color-surface-highlight); - color: var(--color-text-primary); - font-size: var(--text-sm); - font-weight: 500; - font-family: inherit; - transition: all var(--transition-base); -} - -.filter-input::placeholder { - color: var(--color-text-placeholder); -} - -.filter-input:focus { - outline: none; - border-color: var(--color-primary); - background-color: var(--color-surface-highlight-hover); -} - -.filter-input:hover { - background-color: var(--color-surface-highlight-hover); -} - -/* Date input styling */ -.filter-input[type="date"] { - cursor: pointer; -} - -.filter-input[type="date"]::-webkit-calendar-picker-indicator { - cursor: pointer; - filter: invert(1); - opacity: 0.6; -} - -.filter-input[type="date"]::-webkit-calendar-picker-indicator:hover { - opacity: 1; -} - -/* ============================================ - HISTORY PAGE - ============================================ */ -.history-page { - display: flex; - flex-direction: column; - gap: 1.5rem; -} - -.event-item { - position: relative; -} - -.event-item[data-status="failed"] { - border-left: 3px solid var(--color-status-purple); -} - -.event-item[data-status="pending"] { - opacity: 0.7; -} - -.event-actions { - display: flex; - align-items: center; - gap: 0.5rem; -} - -.btn-icon-sm { - width: 2rem; - height: 2rem; - border-radius: var(--radius-full); - border: none; - background-color: transparent; - color: var(--color-text-secondary); - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - transition: all var(--transition-fast); -} - -.btn-icon-sm:hover { - background-color: var(--color-surface-highlight-alpha-30); - color: var(--color-primary); -} - -.feedback-btn { - position: relative; -} - -.feedback-btn.active { - color: var(--color-primary); - background-color: var(--color-surface-highlight-alpha-30); -} - -.feedback-btn.active.up { - color: var(--color-status-green); -} - -.feedback-btn.active.down { - color: var(--color-status-purple); -} - -.btn-xs { - height: 2rem; - padding: 0 0.75rem; - font-size: var(--text-xs); -} - -.retry-btn { - display: flex; - align-items: center; - gap: 0.25rem; -} - -/* Status badges */ -.status-badge-success { - background-color: var(--color-status-green-bg); - color: var(--color-status-green); - font-size: 10px; - font-weight: 700; - padding: 0.125rem 0.5rem; - border-radius: var(--radius-full); - text-transform: uppercase; - letter-spacing: 0.025em; -} - -.status-badge-failed { - background-color: var(--color-status-purple-bg); - color: var(--color-status-purple); - font-size: 10px; - font-weight: 700; - padding: 0.125rem 0.5rem; - border-radius: var(--radius-full); - text-transform: uppercase; - letter-spacing: 0.025em; -} - -.status-badge-pending { - background-color: var(--color-status-blue-bg); - color: var(--color-status-blue); - font-size: 10px; - font-weight: 700; - padding: 0.125rem 0.5rem; - border-radius: var(--radius-full); - text-transform: uppercase; - letter-spacing: 0.025em; -} - -.list-item-text.truncated { - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; -} - -.event-details h3 { - font-size: var(--text-base); - font-weight: 700; - color: var(--color-text-primary); - margin-top: 1rem; - margin-bottom: 0.5rem; -} - -.event-details h3:first-child { - margin-top: 0; -} - -.event-details p { - color: var(--color-text-secondary); - line-height: 1.6; -} - -/* Responsive filters */ -@media (min-width: 768px) { - .filter-controls-compact { - grid-template-columns: repeat(2, 1fr); - } -} - -@media (min-width: 1024px) { - .filter-controls-compact { - grid-template-columns: 2fr 1fr 1fr 1fr 1fr; - } -} - -/* ============================================ - HELP & ABOUT PAGES - ============================================ */ -.help-page, -.about-page { - display: flex; - flex-direction: column; - gap: 1.5rem; -} - -.help-section { - display: flex; - flex-direction: column; - gap: 2rem; -} - -.help-step { - display: flex; - gap: 1.5rem; - align-items: flex-start; -} - -.help-step-number { - width: 3rem; - height: 3rem; - border-radius: var(--radius-full); - background-color: var(--color-primary); - color: var(--color-text-dark); - display: flex; - align-items: center; - justify-content: center; - font-size: var(--text-xl); - font-weight: 900; - flex-shrink: 0; -} - -.help-step-content { - flex: 1; -} - -.help-step-content h3 { - font-size: var(--text-lg); - font-weight: 700; - color: var(--color-text-primary); - margin-bottom: 0.5rem; -} - -.help-step-content p { - color: var(--color-text-secondary); - line-height: 1.6; - margin-bottom: 1rem; -} - -.help-example { - display: flex; - align-items: center; - gap: 0.75rem; - padding: 1rem 1.25rem; - background-color: var(--color-surface-highlight-alpha-30); - border-left: 3px solid var(--color-primary); - border-radius: var(--radius-lg); - font-size: var(--text-sm); - color: var(--color-text-primary); - font-style: italic; -} - -.help-example .material-symbols-outlined { - color: var(--color-primary); - font-size: 20px; - flex-shrink: 0; -} - -.features-grid { - display: grid; - grid-template-columns: 1fr; - gap: 1.5rem; -} - -.feature-card { - padding: 1.5rem; - background-color: var(--color-surface-highlight-alpha-30); - border-radius: var(--radius-xl); - transition: all var(--transition-base); -} - -.feature-card:hover { - background-color: var(--color-surface-highlight); - transform: translateY(-2px); -} - -.feature-icon { - width: 3.5rem; - height: 3.5rem; - border-radius: var(--radius-lg); - background-color: var(--color-primary-light); - display: flex; - align-items: center; - justify-content: center; - margin-bottom: 1rem; -} - -.feature-icon .material-symbols-outlined { - font-size: 28px; - color: var(--color-primary); -} - -.feature-card h3 { - font-size: var(--text-base); - font-weight: 700; - color: var(--color-text-primary); - margin-bottom: 0.5rem; -} - -.feature-card p { - font-size: var(--text-sm); - color: var(--color-text-secondary); - line-height: 1.6; -} - -.faq-list { - display: flex; - flex-direction: column; - gap: 1.5rem; -} - -.faq-item { - padding-bottom: 1.5rem; - border-bottom: 1px solid var(--color-border); -} - -.faq-item:last-child { - border-bottom: none; - padding-bottom: 0; -} - -.faq-item h3 { - font-size: var(--text-base); - font-weight: 700; - color: var(--color-text-primary); - margin-bottom: 0.75rem; -} - -.faq-item p { - font-size: var(--text-sm); - color: var(--color-text-secondary); - line-height: 1.6; -} - -@media (min-width: 768px) { - .features-grid { - grid-template-columns: repeat(2, 1fr); - } -} - -@media (min-width: 1024px) { - .features-grid { - grid-template-columns: repeat(3, 1fr); - } -} - -/* =================================== - Auth Pages - =================================== */ - -.auth-page { - min-height: 100vh; - display: flex; - align-items: center; - justify-content: center; - padding: 2rem 1rem; - background: var(--color-bg-dark); - position: relative; - z-index: 1; -} - -.auth-container { - width: 100%; - max-width: 28rem; -} - -.auth-card { - background: var(--color-surface-dark); - border-radius: var(--radius-2xl); - border: 1px solid var(--color-border); - padding: 3rem; - box-shadow: var(--shadow-lg); - position: relative; - overflow: hidden; -} - -.auth-card .heading-xl { - font-size: var(--text-4xl); - font-weight: 900; - line-height: 1.1; - letter-spacing: -0.02em; - margin-bottom: 0.5rem; - color: var(--color-text-primary); -} - -.auth-card .subtitle { - margin-bottom: 2.5rem; - font-size: var(--text-lg); - color: var(--color-text-secondary); -} - -.form-vertical { - display: flex; - flex-direction: column; - gap: 1.5rem; -} - -.form-vertical .form-group label { - font-size: var(--text-base); - font-weight: 600; - color: var(--color-text-primary); - margin-bottom: 0.5rem; - display: block; -} - -.form-vertical .form-group input { - width: 100%; - padding: 1rem 1.25rem; - border-radius: var(--radius-xl); - background-color: var(--color-surface-highlight); - color: var(--color-text-primary); - border: 2px solid transparent; - font-size: var(--text-base); - font-family: inherit; - transition: border-color var(--transition-base); -} - -.form-vertical .form-group input::placeholder { - color: var(--color-text-placeholder); -} - -.form-vertical .form-group input:focus { - outline: none; - border-color: var(--color-primary); -} - -.form-actions { - margin-top: 0.5rem; -} - -.btn-lg { - height: 3.5rem; - padding: 0 2rem; - font-size: var(--text-lg); - font-weight: 700; -} - -.btn-full-width { - width: 100%; - justify-content: center; -} - -.divider-horizontal { - display: flex; - align-items: center; - text-align: center; - margin: 1.5rem 0; - color: var(--color-text-secondary); -} - -.divider-horizontal::before, -.divider-horizontal::after { - content: ''; - flex: 1; - border-bottom: 1px solid var(--color-border); -} - -.divider-horizontal span { - padding: 0 1rem; - font-size: var(--text-sm); - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.05em; -} - -.auth-footer { - margin-top: 2rem; - text-align: center; - font-size: var(--text-sm); - color: var(--color-text-secondary); -} - -.auth-footer a { - color: var(--color-primary); - text-decoration: none; - font-weight: 600; - transition: color var(--transition-fast); -} - -.auth-footer a:hover { - color: var(--color-primary-hover); -} - -.badge-warning { - background-color: #d97706; - color: white; - padding: 0.25rem 0.5rem; - border-radius: var(--radius-sm); - font-size: var(--text-xs); - font-weight: 600; -} - -.mock-auth-section { - margin-top: 1.5rem; -} - -.text-muted { - color: var(--color-text-secondary); -} diff --git a/frontend/css/components/badges.css b/frontend/css/components/badges.css new file mode 100644 index 0000000..96aa74d --- /dev/null +++ b/frontend/css/components/badges.css @@ -0,0 +1,32 @@ +/* ============================================ + BADGES + Badge components and variants + ============================================ */ + +/* Base Badge */ +.badge { + display: inline-flex; + align-items: center; + font-size: 10px; + font-weight: 700; + padding: 0.125rem 0.5rem; + border-radius: var(--radius-full); + text-transform: uppercase; + letter-spacing: 0.025em; +} + +/* Badge Type Variants */ +.badge-task { + background-color: var(--color-status-green-bg); + color: var(--color-status-green); +} + +.badge-event { + background-color: var(--color-status-blue-bg); + color: var(--color-status-blue); +} + +.badge-reminder { + background-color: var(--color-status-purple-bg); + color: var(--color-status-purple); +} diff --git a/frontend/css/components/buttons.css b/frontend/css/components/buttons.css new file mode 100644 index 0000000..6cef505 --- /dev/null +++ b/frontend/css/components/buttons.css @@ -0,0 +1,138 @@ +/* ============================================ + BUTTONS + All button components and variants + ============================================ */ + +/* Base Button */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + border-radius: var(--radius-full); + font-weight: 700; + cursor: pointer; + border: none; + transition: all var(--transition-base); + white-space: nowrap; + font-family: inherit; +} + +/* Button Sizes */ +.btn-xs { + height: 2rem; + padding: 0 0.75rem; + font-size: var(--text-xs); +} + +.btn-sm { + height: 3rem; + padding: 0 1.5rem; + font-size: var(--text-sm); + letter-spacing: 0.025em; +} + +.btn-md { + height: 3rem; + padding: 0 1.5rem; + font-size: var(--text-base); +} + +.btn-lg { + height: 3.5rem; + padding: 0 2rem; + font-size: var(--text-lg); + font-weight: 700; +} + +.btn-icon { + width: 2.5rem; + height: 2.5rem; + padding: 0; +} + +.btn-icon-sm { + width: 2rem; + height: 2rem; + border-radius: var(--radius-full); + border: none; + background-color: transparent; + color: var(--color-text-secondary); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all var(--transition-fast); +} + +.btn-icon-sm:hover { + background-color: var(--color-surface-highlight-alpha-30); + color: var(--color-primary); +} + +/* Button Variants */ +.btn-primary { + background-color: var(--color-primary); + color: var(--color-text-dark); + box-shadow: var(--shadow-primary); +} + +.btn-primary:hover { + background-color: var(--color-primary-hover); +} + +.btn-primary:active { + transform: scale(0.95); +} + +.btn-secondary { + background-color: var(--color-surface-highlight); + color: var(--color-text-primary); +} + +.btn-secondary:hover { + background-color: var(--color-surface-highlight-hover); +} + +.btn-outline { + background-color: transparent; + border: 1px solid var(--color-border); + color: var(--color-text-secondary); +} + +.btn-outline:hover { + background-color: var(--color-surface-highlight-alpha-30); + color: var(--color-text-primary); + border-color: var(--color-primary); +} + +.btn-dark { + background-color: rgba(35, 30, 16, 0.1); + color: var(--color-text-dark); +} + +.btn-dark:hover { + background-color: rgba(0, 0, 0, 0.8); + color: var(--color-text-primary); +} + +.btn-text { + color: var(--color-text-secondary); + font-size: var(--text-xs); + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + background: none; + padding: 0; + height: auto; +} + +.btn-text:hover { + color: var(--color-primary); +} + +/* Button Modifiers */ +.btn-full-width { + width: 100%; + justify-content: center; +} diff --git a/frontend/css/components/cards.css b/frontend/css/components/cards.css new file mode 100644 index 0000000..12c863f --- /dev/null +++ b/frontend/css/components/cards.css @@ -0,0 +1,51 @@ +/* ============================================ + CARDS + Card components and variations + ============================================ */ + +/* Base Card */ +.card { + background-color: var(--color-surface-dark); + border-radius: var(--radius-2xl); + padding: 1.5rem; + border: 1px solid var(--color-border); + position: relative; +} + +/* Card Variants */ +.card-primary { + background-color: var(--color-primary); + color: var(--color-text-dark); + overflow: hidden; +} + +.card-hero { + padding: 2.5rem; + text-align: center; + background: linear-gradient(135deg, var(--color-surface-dark) 0%, var(--color-surface-highlight) 100%); + position: relative; + overflow: hidden; +} + +/* Card Sections */ +.card-header { + padding: 1.25rem; + border-bottom: 1px solid var(--color-border); + display: flex; + justify-content: space-between; + align-items: center; +} + +.card-content { + padding: 1.5rem; +} + +/* Card Modifiers */ +.card-input { + padding: 2rem; +} + +.card-centered { + padding: 2rem; + text-align: center; +} diff --git a/frontend/css/components/forms.css b/frontend/css/components/forms.css new file mode 100644 index 0000000..1faed6e --- /dev/null +++ b/frontend/css/components/forms.css @@ -0,0 +1,169 @@ +/* ============================================ + FORM ELEMENTS + Form inputs, textareas, and form layouts + ============================================ */ + +/* Form Layout */ +.form-group { + display: flex; + flex-direction: column; + gap: 1rem; + width: 100%; +} + +.form-vertical { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.form-actions { + margin-top: 0.5rem; +} + +/* Form Labels */ +.form-label { + font-size: var(--text-lg); + font-weight: 700; + color: var(--color-text-primary); +} + +.form-vertical .form-group label { + font-size: var(--text-base); + font-weight: 600; + color: var(--color-text-primary); + margin-bottom: 0.5rem; + display: block; +} + +/* Input Wrapper */ +.input-wrapper { + position: relative; + width: 100%; +} + +/* Textarea */ +.textarea { + width: 100%; + min-height: 160px; + resize: none; + border-radius: var(--radius-xl); + background-color: var(--color-surface-highlight); + color: var(--color-text-primary); + border: 2px solid transparent; + padding: 1.5rem; + font-size: var(--text-xl); + font-weight: 500; + line-height: 1.6; + font-family: inherit; + transition: border-color var(--transition-base); +} + +.textarea::placeholder { + color: var(--color-text-placeholder); +} + +.textarea:focus { + outline: none; + border-color: var(--color-primary); +} + +/* Text Input */ +.form-vertical .form-group input { + width: 100%; + padding: 1rem 1.25rem; + border-radius: var(--radius-xl); + background-color: var(--color-surface-highlight); + color: var(--color-text-primary); + border: 2px solid transparent; + font-size: var(--text-base); + font-family: inherit; + transition: border-color var(--transition-base); +} + +.form-vertical .form-group input::placeholder { + color: var(--color-text-placeholder); +} + +.form-vertical .form-group input:focus { + outline: none; + border-color: var(--color-primary); +} + +/* Input Actions (for actions inside input wrapper) */ +.input-actions { + position: absolute; + bottom: 1rem; + right: 1rem; + display: flex; + align-items: center; + gap: 0.75rem; +} + +.input-hint { + font-size: var(--text-xs); + color: var(--color-text-secondary); + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +/* ============================================ + TOGGLE SWITCH + ============================================ */ +.toggle-switch { + position: relative; + display: inline-block; + width: 3rem; + height: 1.75rem; + cursor: pointer; + flex-shrink: 0; +} + +.toggle-switch input { + opacity: 0; + width: 0; + height: 0; +} + +.toggle-slider { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: var(--color-surface-highlight); + border: 2px solid var(--color-border); + border-radius: var(--radius-full); + transition: all var(--transition-base); +} + +.toggle-slider::before { + position: absolute; + content: ""; + height: 1.25rem; + width: 1.25rem; + left: 0.125rem; + bottom: 0.125rem; + background-color: var(--color-text-primary); + border-radius: var(--radius-full); + transition: all var(--transition-base); +} + +.toggle-switch input:checked + .toggle-slider { + background-color: var(--color-primary); + border-color: var(--color-primary); +} + +.toggle-switch input:checked + .toggle-slider::before { + transform: translateX(1.25rem); + background-color: var(--color-text-dark); +} + +.toggle-switch:hover .toggle-slider { + background-color: var(--color-surface-highlight-hover); +} + +.toggle-switch input:checked:hover + .toggle-slider { + background-color: var(--color-primary-hover); +} diff --git a/frontend/css/components/modals.css b/frontend/css/components/modals.css new file mode 100644 index 0000000..e5729d5 --- /dev/null +++ b/frontend/css/components/modals.css @@ -0,0 +1,255 @@ +/* ============================================ + MODAL COMPONENT + Base modal structure and variants + ============================================ */ + +/* Modal Overlay & Container */ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.7); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + padding: 1rem; + opacity: 0; + transition: opacity var(--transition-base); +} + +.modal-overlay.modal-open { + opacity: 1; +} + +.modal-container { + background-color: var(--color-surface-dark); + border-radius: var(--radius-2xl); + border: 1px solid var(--color-border); + max-width: 90vw; + max-height: 90vh; + display: flex; + flex-direction: column; + box-shadow: var(--shadow-lg); + transform: scale(0.9); + transition: transform var(--transition-base); +} + +.modal-open .modal-container { + transform: scale(1); +} + +/* Modal Sizes */ +.modal-small { + width: 400px; +} + +.modal-medium { + width: 600px; +} + +.modal-large { + width: 800px; +} + +.modal-full { + width: 95vw; + height: 95vh; +} + +/* Modal Sections */ +.modal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1.5rem; + border-bottom: 1px solid var(--color-border); +} + +.modal-title { + font-size: var(--text-xl); + font-weight: 700; + color: var(--color-text-primary); +} + +.modal-close { + width: 2.5rem; + height: 2.5rem; + border-radius: var(--radius-full); + border: none; + background-color: var(--color-surface-highlight); + color: var(--color-text-primary); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background-color var(--transition-fast); +} + +.modal-close:hover { + background-color: var(--color-surface-highlight-hover); +} + +.modal-body { + padding: 1.5rem; + overflow-y: auto; + flex: 1; +} + +/* Modal Content Styles */ +.modal-content-text { + line-height: 1.6; +} + +.modal-content-text section { + margin-bottom: 1.5rem; +} + +.modal-content-text section:last-of-type { + margin-bottom: 1rem; +} + +.modal-content-text h3 { + font-size: var(--text-lg); + font-weight: 600; + color: var(--color-text-primary); + margin-bottom: 0.5rem; +} + +.modal-content-text p { + color: var(--color-text-secondary); + margin-bottom: 0.75rem; +} + +.modal-content-text p:last-child { + margin-bottom: 0; +} + +.modal-footer-text { + margin-top: 2rem; +} + +/* ============================================ + SETTINGS MODAL + ============================================ */ +.settings-content { + display: flex; + flex-direction: column; + gap: 2rem; +} + +.settings-section { + padding-bottom: 2rem; + border-bottom: 1px solid var(--color-border); +} + +.settings-section:last-child { + border-bottom: none; + padding-bottom: 0; +} + +.settings-section-title { + font-size: var(--text-xl); + font-weight: 700; + color: var(--color-text-primary); + margin-bottom: 1.5rem; +} + +.settings-section-content { + display: flex; + flex-direction: column; + gap: 1.25rem; +} + +.setting-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1.5rem; + padding: 1rem; + background-color: var(--color-surface-highlight-alpha-30); + border-radius: var(--radius-lg); + transition: background-color var(--transition-fast); +} + +.setting-row:hover { + background-color: var(--color-surface-highlight); +} + +.setting-label { + display: flex; + align-items: center; + gap: 1rem; + flex: 1; + min-width: 0; +} + +.setting-label .material-symbols-outlined { + color: var(--color-text-secondary); + flex-shrink: 0; +} + +.setting-title { + font-size: var(--text-base); + font-weight: 600; + color: var(--color-text-primary); + margin-bottom: 0.25rem; +} + +.setting-description { + font-size: var(--text-sm); + color: var(--color-text-secondary); + line-height: 1.4; +} + +/* ============================================ + LEGAL MODALS (Privacy Policy, Terms of Service) + ============================================ */ +.legal-content { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.legal-updated { + font-size: var(--text-sm); + color: var(--color-text-secondary); + font-style: italic; + padding-bottom: 1rem; + border-bottom: 1px solid var(--color-border); +} + +.legal-intro { + font-size: var(--text-base); + color: var(--color-text-secondary); + line-height: 1.6; + padding: 1rem; + background-color: var(--color-surface-highlight-alpha-30); + border-radius: var(--radius-lg); + border-left: 3px solid var(--color-primary); +} + +.legal-section { + padding-bottom: 1.5rem; + border-bottom: 1px solid var(--color-divider); +} + +.legal-section:last-child { + border-bottom: none; + padding-bottom: 0; +} + +.legal-section-title { + font-size: var(--text-lg); + font-weight: 700; + color: var(--color-text-primary); + margin-bottom: 0.75rem; +} + +.legal-section-content { + font-size: var(--text-sm); + color: var(--color-text-secondary); + line-height: 1.7; +} diff --git a/frontend/css/components/other.css b/frontend/css/components/other.css new file mode 100644 index 0000000..a9e4191 --- /dev/null +++ b/frontend/css/components/other.css @@ -0,0 +1,544 @@ +/* ============================================ + OTHER COMPONENTS + Collection of reusable UI components + ============================================ */ + +/* ============================================ + ICONS + ============================================ */ +.material-symbols-outlined { + font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24; +} + +.icon-sm { + font-size: 14px; +} + +.icon-md { + font-size: 18px; +} + +.icon-lg { + font-size: 20px; +} + +.icon-xl { + font-size: 24px; +} + +.chevron-icon { + color: var(--color-text-secondary); + transition: color var(--transition-fast); +} + +.list-item:hover .chevron-icon { + color: var(--color-primary); +} + +/* ============================================ + DECORATIVE ELEMENTS + ============================================ */ +.blob-decorator { + position: absolute; + top: -1rem; + right: -1rem; + width: 6rem; + height: 6rem; + background-color: var(--color-primary-light); + border-radius: var(--radius-full); + filter: blur(2rem); + pointer-events: none; +} + +.icon-decorator { + position: absolute; + top: 0; + right: 0; + padding: 2rem; + opacity: 0.1; + transform: translate(1rem, -1rem); + font-size: 120px; +} + +/* ============================================ + LIST ITEMS + ============================================ */ +.list-container { + display: flex; + flex-direction: column; + gap: 0.75rem; + padding: 1rem; + overflow-y: auto; + max-height: 500px; +} + +.list-item { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.75rem; + border-radius: var(--radius-xl); + background-color: var(--color-surface-highlight-alpha-30); + border: 1px solid transparent; + cursor: pointer; + transition: all var(--transition-base); +} + +.list-item:hover { + background-color: var(--color-surface-highlight); + border-color: rgba(244, 192, 37, 0.3); +} + +.list-item-header { + display: flex; + align-items: center; + justify-content: space-between; +} + +.list-item-meta { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.list-item-text { + font-size: var(--text-sm); + font-weight: 500; + color: var(--color-text-primary); + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.list-item-footer { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: var(--text-xs); + color: var(--color-text-secondary); +} + +.divider { + height: 1px; + width: 100%; + background-color: var(--color-divider); + margin: 0.25rem 0; +} + +/* ============================================ + TIMESTAMP STYLES + ============================================ */ +.timestamp { + font-size: var(--text-xs); + color: var(--color-text-secondary); +} + +/* ============================================ + SCROLLBAR + ============================================ */ +.custom-scrollbar::-webkit-scrollbar { + width: 6px; +} + +.custom-scrollbar::-webkit-scrollbar-track { + background: transparent; +} + +.custom-scrollbar::-webkit-scrollbar-thumb { + background-color: var(--color-surface-highlight); + border-radius: var(--radius-full); +} + +.custom-scrollbar::-webkit-scrollbar-thumb:hover { + background-color: var(--color-surface-highlight-hover); +} + +/* ============================================ + MULTI-SELECT DROPDOWN + ============================================ */ +.multi-select { + position: relative; + width: 100%; +} + +.multi-select-trigger { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; + padding: 0.75rem 1rem; + background-color: var(--color-surface-highlight); + border: 2px solid transparent; + border-radius: var(--radius-lg); + color: var(--color-text-primary); + font-size: var(--text-sm); + font-weight: 500; + cursor: pointer; + transition: all var(--transition-base); + font-family: inherit; +} + +.multi-select-trigger:hover { + background-color: var(--color-surface-highlight-hover); +} + +.multi-select.open .multi-select-trigger { + border-color: var(--color-primary); +} + +.multi-select-trigger-content { + flex: 1; + text-align: left; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.multi-select-trigger-content.placeholder { + color: var(--color-text-placeholder); +} + +.multi-select-chevron { + font-size: 20px; + color: var(--color-text-secondary); + transition: transform var(--transition-base); +} + +.multi-select.open .multi-select-chevron { + transform: rotate(180deg); +} + +.multi-select-dropdown { + position: absolute; + top: calc(100% + 0.5rem); + left: 0; + right: 0; + background-color: var(--color-surface-dark); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-lg); + z-index: 100; + max-height: 280px; + overflow-y: auto; + opacity: 0; + visibility: hidden; + transform: translateY(-0.5rem); + transition: all var(--transition-base); +} + +.multi-select.open .multi-select-dropdown { + opacity: 1; + visibility: visible; + transform: translateY(0); +} + +.multi-select-option { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem 1rem; + cursor: pointer; + transition: background-color var(--transition-fast); + font-size: var(--text-sm); + color: var(--color-text-primary); +} + +.multi-select-option:hover { + background-color: var(--color-surface-highlight-alpha-30); +} + +.multi-select-checkbox { + width: 1.25rem; + height: 1.25rem; + border: 2px solid var(--color-border); + border-radius: var(--radius-sm); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + transition: all var(--transition-fast); +} + +.multi-select-checkbox .material-symbols-outlined { + font-size: 16px; + color: var(--color-text-dark); + opacity: 0; + transition: opacity var(--transition-fast); +} + +.multi-select-option.selected .multi-select-checkbox { + background-color: var(--color-primary); + border-color: var(--color-primary); +} + +.multi-select-option.selected .multi-select-checkbox .material-symbols-outlined { + opacity: 1; +} + +.multi-select-dropdown::-webkit-scrollbar { + width: 6px; +} + +.multi-select-dropdown::-webkit-scrollbar-track { + background: transparent; +} + +.multi-select-dropdown::-webkit-scrollbar-thumb { + background-color: var(--color-surface-highlight); + border-radius: var(--radius-full); +} + +.multi-select-dropdown::-webkit-scrollbar-thumb:hover { + background-color: var(--color-surface-highlight-hover); +} + +/* ============================================ + ACTION BUTTONS ROW + ============================================ */ +.action-buttons { + display: flex; + flex-wrap: wrap; + gap: 1rem; + align-items: center; +} + +/* ============================================ + THEME DROPDOWN COMPONENT + ============================================ */ +.theme-dropdown-wrapper { + position: relative; +} + +.theme-dropdown { + position: absolute; + top: calc(100% + 0.5rem); + right: 0; + background: var(--color-surface-dark); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + padding: 0.5rem; + min-width: 200px; + box-shadow: var(--shadow-lg); + opacity: 0; + visibility: hidden; + transform: translateY(-0.5rem); + transition: opacity var(--transition-fast), transform var(--transition-fast), visibility var(--transition-fast); + z-index: 1000; +} + +.theme-dropdown-open { + opacity: 1; + visibility: visible; + transform: translateY(0); +} + +.theme-dropdown-item { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.625rem 0.75rem; + cursor: pointer; + border-radius: var(--radius-md); + transition: background var(--transition-fast); +} + +.theme-dropdown-item:hover { + background: var(--color-surface-highlight-alpha-30); +} + +.theme-dropdown-item-active { + background: var(--color-surface-highlight); +} + +.theme-dropdown-item-active:hover { + background: var(--color-surface-highlight); +} + +.theme-swatch { + width: 20px; + height: 20px; + border-radius: var(--radius-sm); + background: var(--color-primary); + flex-shrink: 0; + border: 1px solid var(--color-border); +} + +.theme-name { + flex: 1; + font-size: var(--text-sm); + color: var(--color-text-primary); +} + +.theme-dropdown-item-active .theme-name { + font-weight: 600; +} + +.theme-checkmark { + font-size: 18px; + color: var(--color-primary); + opacity: 0; + transition: opacity var(--transition-fast); +} + +.theme-dropdown-item-active .theme-checkmark { + opacity: 1; +} + +/* ============================================ + PAGINATION COMPONENT + ============================================ */ +.pagination { + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 1.5rem 0; + flex-wrap: wrap; +} + +.pagination-btn { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + border-radius: var(--radius-lg); + border: 1px solid var(--color-border); + background-color: var(--color-surface-dark); + color: var(--color-text-primary); + font-weight: 500; + cursor: pointer; + transition: all var(--transition-fast); +} + +.pagination-btn:hover:not(.disabled) { + background-color: var(--color-surface-highlight); + border-color: var(--color-primary); +} + +.pagination-btn.disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.pagination-pages { + display: flex; + gap: 0.25rem; +} + +.pagination-page { + min-width: 2.5rem; + height: 2.5rem; + border-radius: var(--radius-md); + border: 1px solid var(--color-border); + background-color: var(--color-surface-dark); + color: var(--color-text-primary); + font-weight: 500; + cursor: pointer; + transition: all var(--transition-fast); + display: flex; + align-items: center; + justify-content: center; +} + +.pagination-page:hover { + background-color: var(--color-surface-highlight); + border-color: var(--color-primary); +} + +.pagination-page.active { + background-color: var(--color-primary); + color: var(--color-text-dark); + border-color: var(--color-primary); +} + +.pagination-ellipsis { + display: flex; + align-items: center; + padding: 0 0.5rem; + color: var(--color-text-secondary); +} + +.pagination-info { + font-size: var(--text-sm); + color: var(--color-text-secondary); + margin-left: 1rem; +} + +/* ============================================ + FILTER SECTION + ============================================ */ +.filter-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 1.25rem; +} + +.filter-controls-compact { + display: grid; + grid-template-columns: 1fr; + gap: 0.75rem; +} + +.filter-group-compact { + display: flex; + flex-direction: column; + width: 100%; +} + +.filter-input { + width: 100%; + padding: 0.75rem 1rem; + border-radius: var(--radius-lg); + border: 2px solid transparent; + background-color: var(--color-surface-highlight); + color: var(--color-text-primary); + font-size: var(--text-sm); + font-weight: 500; + font-family: inherit; + transition: all var(--transition-base); +} + +.filter-input::placeholder { + color: var(--color-text-placeholder); +} + +.filter-input:focus { + outline: none; + border-color: var(--color-primary); + background-color: var(--color-surface-highlight-hover); +} + +.filter-input:hover { + background-color: var(--color-surface-highlight-hover); +} + +/* Date input styling */ +.filter-input[type="date"] { + cursor: pointer; +} + +.filter-input[type="date"]::-webkit-calendar-picker-indicator { + cursor: pointer; + filter: invert(1); + opacity: 0.6; +} + +.filter-input[type="date"]::-webkit-calendar-picker-indicator:hover { + opacity: 1; +} + +/* Responsive filters */ +@media (min-width: 768px) { + .filter-controls-compact { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (min-width: 1024px) { + .filter-controls-compact { + grid-template-columns: 2fr 1fr 1fr 1fr 1fr; + } +} diff --git a/frontend/css/pages/auth.css b/frontend/css/pages/auth.css new file mode 100644 index 0000000..c46d900 --- /dev/null +++ b/frontend/css/pages/auth.css @@ -0,0 +1,185 @@ +/* ============================================ + AUTH PAGE STYLES + ============================================ */ + +/* =================================== + Auth Page Layout + =================================== */ + +.auth-page { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 2rem 1rem; + background: var(--color-bg-dark); + position: relative; + z-index: 1; +} + +.auth-container { + width: 100%; + max-width: 28rem; +} + +.auth-card { + background: var(--color-surface-dark); + border-radius: var(--radius-2xl); + border: 1px solid var(--color-border); + padding: 3rem; + box-shadow: var(--shadow-lg); + position: relative; + overflow: hidden; +} + +/* =================================== + Auth Card Content + =================================== */ + +.auth-card .heading-xl { + font-size: var(--text-4xl); + font-weight: 900; + line-height: 1.1; + letter-spacing: -0.02em; + margin-bottom: 0.5rem; + color: var(--color-text-primary); +} + +.auth-card .subtitle { + margin-bottom: 2.5rem; + font-size: var(--text-lg); + color: var(--color-text-secondary); +} + +/* =================================== + Auth Forms + =================================== */ + +.form-vertical { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.form-vertical .form-group label { + font-size: var(--text-base); + font-weight: 600; + color: var(--color-text-primary); + margin-bottom: 0.5rem; + display: block; +} + +.form-vertical .form-group input { + width: 100%; + padding: 1rem 1.25rem; + border-radius: var(--radius-xl); + background-color: var(--color-surface-highlight); + color: var(--color-text-primary); + border: 2px solid transparent; + font-size: var(--text-base); + font-family: inherit; + transition: border-color var(--transition-base); +} + +.form-vertical .form-group input::placeholder { + color: var(--color-text-placeholder); +} + +.form-vertical .form-group input:focus { + outline: none; + border-color: var(--color-primary); +} + +/* =================================== + Auth Actions & Buttons + =================================== */ + +.form-actions { + margin-top: 0.5rem; +} + +.btn-lg { + height: 3.5rem; + padding: 0 2rem; + font-size: var(--text-lg); + font-weight: 700; +} + +.btn-full-width { + width: 100%; + justify-content: center; +} + +/* =================================== + Auth Divider + =================================== */ + +.divider-horizontal { + display: flex; + align-items: center; + text-align: center; + margin: 1.5rem 0; + color: var(--color-text-secondary); +} + +.divider-horizontal::before, +.divider-horizontal::after { + content: ''; + flex: 1; + border-bottom: 1px solid var(--color-border); +} + +.divider-horizontal span { + padding: 0 1rem; + font-size: var(--text-sm); + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +/* =================================== + Auth Footer + =================================== */ + +.auth-footer { + margin-top: 2rem; + text-align: center; + font-size: var(--text-sm); + color: var(--color-text-secondary); +} + +.auth-footer a { + color: var(--color-primary); + text-decoration: none; + font-weight: 600; + transition: color var(--transition-fast); +} + +.auth-footer a:hover { + color: var(--color-primary-hover); +} + +/* =================================== + Auth Badges & Status + =================================== */ + +.badge-warning { + background-color: #d97706; + color: white; + padding: 0.25rem 0.5rem; + border-radius: var(--radius-sm); + font-size: var(--text-xs); + font-weight: 600; +} + +/* =================================== + Mock Auth Section + =================================== */ + +.mock-auth-section { + margin-top: 1.5rem; +} + +.text-muted { + color: var(--color-text-secondary); +} diff --git a/frontend/css/pages/dashboard.css b/frontend/css/pages/dashboard.css new file mode 100644 index 0000000..e407199 --- /dev/null +++ b/frontend/css/pages/dashboard.css @@ -0,0 +1,172 @@ +/* ============================================ + DASHBOARD PAGE STYLES + ============================================ */ + +/* ============================================ + MOBILE CARD + ============================================ */ +.mobile-card-content { + position: relative; + z-index: 10; + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.icon-badge { + width: 3rem; + height: 3rem; + border-radius: var(--radius-full); + background-color: rgba(35, 30, 16, 0.1); + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 0.5rem; +} + +.mobile-card-title { + font-size: var(--text-2xl); + font-weight: 900; + line-height: 1.2; +} + +.mobile-card-text { + font-size: var(--text-sm); + font-weight: 500; + color: rgba(35, 30, 16, 0.8); + line-height: 1.4; +} + +/* ============================================ + ALERTS & NOTIFICATIONS + ============================================ */ +.alert { + padding: 1rem; + border-radius: var(--radius-xl); + border: 1px solid; + display: flex; + align-items: flex-start; + gap: 0.75rem; +} + +.alert-icon { + flex-shrink: 0; +} + +.alert-content { + flex: 1; +} + +.alert-error { + background-color: rgba(220, 38, 38, 0.1); + border-color: rgba(220, 38, 38, 0.3); + color: var(--color-text-primary); +} + +.alert-error .alert-icon { + color: #dc2626; +} + +.alert-success { + background-color: rgba(34, 197, 94, 0.1); + border-color: rgba(34, 197, 94, 0.3); + color: var(--color-text-primary); +} + +.alert-success .alert-icon { + color: var(--color-status-green); +} + +.alert-warning { + background-color: rgba(245, 158, 11, 0.1); + border-color: rgba(245, 158, 11, 0.3); + color: var(--color-text-primary); +} + +.alert-warning .alert-icon { + color: #f59e0b; +} + +/* ============================================ + EVENT RESULT CARD + ============================================ */ +.event-result { + padding: 1.5rem; + background-color: rgba(34, 197, 94, 0.1); + border: 1px solid rgba(34, 197, 94, 0.3); + border-radius: var(--radius-xl); +} + +.event-result-header { + display: flex; + align-items: flex-start; + gap: 0.75rem; + margin-bottom: 1rem; +} + +.event-result-icon { + color: var(--color-status-green); + flex-shrink: 0; +} + +.event-result-body { + flex: 1; +} + +.event-result-title { + margin: 0 0 0.5rem 0; + font-size: var(--text-lg); + font-weight: 600; + color: var(--color-text-primary); +} + +.event-result-meta { + margin-bottom: 0.5rem; + color: var(--color-text-secondary); +} + +.event-result-meta strong { + color: var(--color-text-primary); +} + +.event-result-notes { + margin-bottom: 0.5rem; + color: var(--color-text-secondary); +} + +.event-result-notes strong { + color: var(--color-text-primary); +} + +.event-missing-info { + margin-top: 1rem; + padding: 0.75rem; + background-color: rgba(245, 158, 11, 0.1); + border-left: 3px solid #f59e0b; + border-radius: var(--radius-md); +} + +.event-missing-info-header { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.event-missing-info-header .material-symbols-outlined { + color: #f59e0b; + font-size: 1.25rem; +} + +.event-missing-info-header strong { + color: #d97706; +} + +.event-missing-info ul { + margin: 0; + padding-left: 1.25rem; +} + +.event-missing-info li { + color: var(--color-text-secondary); +} diff --git a/frontend/css/pages/help-about.css b/frontend/css/pages/help-about.css new file mode 100644 index 0000000..3ad5673 --- /dev/null +++ b/frontend/css/pages/help-about.css @@ -0,0 +1,198 @@ +/* ============================================ + HELP & ABOUT PAGES + ============================================ */ + +/* ============================================ + PAGE LAYOUT + ============================================ */ +.help-page, +.about-page { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.about-hero { + padding: 3rem; +} + +.about-hero-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 5rem; + height: 5rem; + background-color: var(--color-primary); + border-radius: var(--radius-2xl); + margin-bottom: 2rem; + position: relative; + z-index: 10; +} + +.about-hero-icon svg { + width: 3rem; + height: 3rem; + color: var(--color-text-dark); +} + +/* ============================================ + HELP SECTION + ============================================ */ +.help-section { + display: flex; + flex-direction: column; + gap: 2rem; +} + +.help-step { + display: flex; + gap: 1.5rem; + align-items: flex-start; +} + +.help-step-number { + width: 3rem; + height: 3rem; + border-radius: var(--radius-full); + background-color: var(--color-primary); + color: var(--color-text-dark); + display: flex; + align-items: center; + justify-content: center; + font-size: var(--text-xl); + font-weight: 900; + flex-shrink: 0; +} + +.help-step-content { + flex: 1; +} + +.help-step-content h3 { + font-size: var(--text-lg); + font-weight: 700; + color: var(--color-text-primary); + margin-bottom: 0.5rem; +} + +.help-step-content p { + color: var(--color-text-secondary); + line-height: 1.6; + margin-bottom: 1rem; +} + +.help-example { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 1rem 1.25rem; + background-color: var(--color-surface-highlight-alpha-30); + border-left: 3px solid var(--color-primary); + border-radius: var(--radius-lg); + font-size: var(--text-sm); + color: var(--color-text-primary); + font-style: italic; +} + +.help-example .material-symbols-outlined { + color: var(--color-primary); + font-size: 20px; + flex-shrink: 0; +} + +/* ============================================ + FEATURES GRID + ============================================ */ +.features-grid { + display: grid; + grid-template-columns: 1fr; + gap: 1.5rem; +} + +.feature-card { + padding: 1.5rem; + background-color: var(--color-surface-highlight-alpha-30); + border-radius: var(--radius-xl); + transition: all var(--transition-base); +} + +.feature-card:hover { + background-color: var(--color-surface-highlight); + transform: translateY(-2px); +} + +.feature-icon { + width: 3.5rem; + height: 3.5rem; + border-radius: var(--radius-lg); + background-color: var(--color-primary-light); + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 1rem; +} + +.feature-icon .material-symbols-outlined { + font-size: 28px; + color: var(--color-primary); +} + +.feature-card h3 { + font-size: var(--text-base); + font-weight: 700; + color: var(--color-text-primary); + margin-bottom: 0.5rem; +} + +.feature-card p { + font-size: var(--text-sm); + color: var(--color-text-secondary); + line-height: 1.6; +} + +/* ============================================ + FAQ SECTION + ============================================ */ +.faq-list { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.faq-item { + padding-bottom: 1.5rem; + border-bottom: 1px solid var(--color-border); +} + +.faq-item:last-child { + border-bottom: none; + padding-bottom: 0; +} + +.faq-item h3 { + font-size: var(--text-base); + font-weight: 700; + color: var(--color-text-primary); + margin-bottom: 0.75rem; +} + +.faq-item p { + font-size: var(--text-sm); + color: var(--color-text-secondary); + line-height: 1.6; +} + +/* ============================================ + RESPONSIVE LAYOUT + ============================================ */ +@media (min-width: 768px) { + .features-grid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (min-width: 1024px) { + .features-grid { + grid-template-columns: repeat(3, 1fr); + } +} diff --git a/frontend/css/pages/history.css b/frontend/css/pages/history.css new file mode 100644 index 0000000..db0f376 --- /dev/null +++ b/frontend/css/pages/history.css @@ -0,0 +1,163 @@ +/* ============================================ + HISTORY PAGE STYLES + Note: Theme variables are imported via index.css + ============================================ */ + +/* ============================================ + PAGE LAYOUT + ============================================ */ +.history-page { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +/* ============================================ + EVENT ITEMS + ============================================ */ +.event-item { + position: relative; +} + +.event-item[data-status="failed"] { + border-left: 3px solid var(--color-status-purple); +} + +.event-item[data-status="pending"] { + opacity: 0.7; +} + +.list-item-text.truncated { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +/* ============================================ + EVENT DETAILS + ============================================ */ +.event-details h3 { + font-size: var(--text-base); + font-weight: 700; + color: var(--color-text-primary); + margin-top: 1rem; + margin-bottom: 0.5rem; +} + +.event-details h3:first-child { + margin-top: 0; +} + +.event-details p { + color: var(--color-text-secondary); + line-height: 1.6; +} + +/* ============================================ + EVENT ACTIONS + ============================================ */ +.event-actions { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.btn-icon-sm { + width: 2rem; + height: 2rem; + border-radius: var(--radius-full); + border: none; + background-color: transparent; + color: var(--color-text-secondary); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all var(--transition-fast); +} + +.btn-icon-sm:hover { + background-color: var(--color-surface-highlight-alpha-30); + color: var(--color-primary); +} + +.feedback-btn { + position: relative; +} + +.feedback-btn.active { + color: var(--color-primary); + background-color: var(--color-surface-highlight-alpha-30); +} + +.feedback-btn.active.up { + color: var(--color-status-green); +} + +.feedback-btn.active.down { + color: var(--color-status-purple); +} + +.btn-xs { + height: 2rem; + padding: 0 0.75rem; + font-size: var(--text-xs); +} + +.retry-btn { + display: flex; + align-items: center; + gap: 0.25rem; +} + +/* ============================================ + STATUS BADGES + ============================================ */ +.status-badge-success { + background-color: var(--color-status-green-bg); + color: var(--color-status-green); + font-size: 10px; + font-weight: 700; + padding: 0.125rem 0.5rem; + border-radius: var(--radius-full); + text-transform: uppercase; + letter-spacing: 0.025em; +} + +.status-badge-failed { + background-color: var(--color-status-purple-bg); + color: var(--color-status-purple); + font-size: 10px; + font-weight: 700; + padding: 0.125rem 0.5rem; + border-radius: var(--radius-full); + text-transform: uppercase; + letter-spacing: 0.025em; +} + +.status-badge-pending { + background-color: var(--color-status-blue-bg); + color: var(--color-status-blue); + font-size: 10px; + font-weight: 700; + padding: 0.125rem 0.5rem; + border-radius: var(--radius-full); + text-transform: uppercase; + letter-spacing: 0.025em; +} + +/* ============================================ + RESPONSIVE FILTERS + ============================================ */ +@media (min-width: 768px) { + .filter-controls-compact { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (min-width: 1024px) { + .filter-controls-compact { + grid-template-columns: 2fr 1fr 1fr 1fr 1fr; + } +} diff --git a/frontend/css/themes.css b/frontend/css/themes.css index 088c398..63b474d 100644 --- a/frontend/css/themes.css +++ b/frontend/css/themes.css @@ -205,6 +205,31 @@ --decal-color: 0, 0, 0; } +/* Light Theme 4: Pastel (Soft purple-mint light theme) */ +[data-theme="pastel"] { + --color-primary: #CCA8E9; + --color-primary-hover: #b88dd4; + --color-primary-light: rgba(204, 168, 233, 0.1); + --color-primary-shadow: rgba(204, 168, 233, 0.15); + + --color-bg-dark: #DEFCF9; + --color-surface-dark: #CADEFC; + --color-surface-highlight: #C3BEF0; + --color-surface-highlight-hover: #b3a8e0; + --color-surface-highlight-alpha-30: rgba(195, 190, 240, 0.6); + + --color-text-primary: #2d1b3d; + --color-text-secondary: #6b5580; + --color-text-placeholder: rgba(107, 85, 128, 0.6); + --color-text-dark: #2d1b3d; + + --color-border: #C3BEF0; + --color-divider: rgba(0, 0, 0, 0.06); + + --decal-opacity: 0.04; + --decal-color: 107, 85, 128; +} + /* ============================================ DESIGN TOKENS (Non-color variables) ============================================ */ diff --git a/frontend/css/utilities.css b/frontend/css/utilities.css new file mode 100644 index 0000000..a9cd833 --- /dev/null +++ b/frontend/css/utilities.css @@ -0,0 +1,220 @@ +/* ============================================ + UTILITY CLASSES + Reusable single-purpose utility classes + ============================================ */ + +/* Typography Utilities */ +.text-primary { + color: var(--color-text-primary); +} + +.text-secondary { + color: var(--color-text-secondary); +} + +.text-muted { + color: var(--color-text-muted); + font-size: var(--text-sm); +} + +.text-xs { + font-size: var(--text-xs); +} + +.text-sm { + font-size: var(--text-sm); +} + +.text-base { + font-size: var(--text-base); +} + +.text-lg { + font-size: var(--text-lg); +} + +.text-xl { + font-size: var(--text-xl); +} + +.text-center { + text-align: center; +} + +.text-left { + text-align: left; +} + +.text-right { + text-align: right; +} + +.line-height-normal { + line-height: 1.5; +} + +.line-height-relaxed { + line-height: 1.8; +} + +.font-bold { + font-weight: 700; +} + +.font-semibold { + font-weight: 600; +} + +/* Spacing Utilities */ +.m-0 { + margin: 0; +} + +.mt-1 { + margin-top: 0.25rem; +} + +.mt-2 { + margin-top: 0.5rem; +} + +.mt-4 { + margin-top: 1rem; +} + +.mb-sm { + margin-bottom: 0.5rem; +} + +.mb-md { + margin-bottom: 1rem; +} + +.mb-lg { + margin-bottom: 1rem; +} + +.mb-xl { + margin-bottom: 1.5rem; +} + +.mb-2xl { + margin-bottom: 2rem; +} + +.p-0 { + padding: 0; +} + +/* Flexbox Utilities */ +.flex { + display: flex; +} + +.flex-col { + flex-direction: column; +} + +.flex-row { + flex-direction: row; +} + +.flex-wrap { + flex-wrap: wrap; +} + +.items-center { + align-items: center; +} + +.items-start { + align-items: flex-start; +} + +.items-end { + align-items: flex-end; +} + +.justify-center { + justify-content: center; +} + +.justify-between { + justify-content: space-between; +} + +.justify-start { + justify-content: flex-start; +} + +.justify-end { + justify-content: flex-end; +} + +/* Gap Utilities */ +.gap-1 { + gap: 0.25rem; +} + +.gap-2 { + gap: 0.5rem; +} + +.gap-3 { + gap: 0.75rem; +} + +.gap-4 { + gap: 1rem; +} + +.gap-6 { + gap: 1.5rem; +} + +.gap-8 { + gap: 2rem; +} + +/* Width/Height Utilities */ +.w-full { + width: 100%; +} + +.h-full { + height: 100%; +} + +/* Display Utilities */ +.block { + display: block; +} + +.inline-block { + display: inline-block; +} + +.hidden { + display: none; +} + +/* Position Utilities */ +.relative { + position: relative; +} + +.absolute { + position: absolute; +} + +.fixed { + position: fixed; +} + +/* Common Component Patterns */ +.button-group { + display: flex; + gap: 1rem; + justify-content: center; + flex-wrap: wrap; +} diff --git a/frontend/index.html b/frontend/index.html index c270d08..6305483 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -9,65 +9,9 @@ - - -
- -
- - -
- - -
- -
- - - -
- - - +
+ diff --git a/frontend/js/api.js b/frontend/js/api.js index ddf896e..535e70a 100644 --- a/frontend/js/api.js +++ b/frontend/js/api.js @@ -4,7 +4,7 @@ */ class ApiClient { - constructor(baseURL = '/api') { + constructor(baseURL = 'http://localhost:8000/api') { this.baseURL = baseURL; this.defaultHeaders = { 'Content-Type': 'application/json', diff --git a/frontend/js/app.js b/frontend/js/app.js index a5f5c3f..3d27dd3 100644 --- a/frontend/js/app.js +++ b/frontend/js/app.js @@ -17,7 +17,7 @@ import { openPrivacyModal, openTermsModal } from './components/legal-modals.js'; */ async function loadPage(pageName) { try { - const html = await loadHTML(`/pages/${pageName}.html`); + const html = await loadHTML(`/pages/${pageName}`); return html; } catch (error) { console.error(`Failed to load page: ${pageName}`, error); @@ -45,10 +45,17 @@ function initDashboard() { generateBtn.disabled = true; generateBtn.textContent = 'Generating...'; - // TODO: Call API to generate calendar events from task - const response = await api.post('/tasks/generate', { text: taskText }); + // Call API to generate calendar events from task + const user = auth.getUser(); + const payload = { + text: taskText, + email: user ? user.email : null, + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || "Asia/Jerusalem" + }; - console.log('Generated tasks:', response); + const response = await api.post('/tasks', payload); + + console.log('Generated task output:', response); // Clear input on success taskInput.value = ''; diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..3430445 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,1943 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "@solidjs/router": "^0.15.4", + "solid-js": "^1.9.10" + }, + "devDependencies": { + "@types/node": "^24.10.1", + "typescript": "~5.9.3", + "vite": "^7.2.4", + "vite-plugin-solid": "^2.11.10" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", + "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", + "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.28.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz", + "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", + "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6", + "@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.28.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", + "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6", + "@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.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "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.28.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", + "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "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.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "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/@rollup/rollup-android-arm-eabi": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.56.0.tgz", + "integrity": "sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.56.0.tgz", + "integrity": "sha512-lfbVUbelYqXlYiU/HApNMJzT1E87UPGvzveGg2h0ktUNlOCxKlWuJ9jtfvs1sKHdwU4fzY7Pl8sAl49/XaEk6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.56.0.tgz", + "integrity": "sha512-EgxD1ocWfhoD6xSOeEEwyE7tDvwTgZc8Bss7wCWe+uc7wO8G34HHCUH+Q6cHqJubxIAnQzAsyUsClt0yFLu06w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.56.0.tgz", + "integrity": "sha512-1vXe1vcMOssb/hOF8iv52A7feWW2xnu+c8BV4t1F//m9QVLTfNVpEdja5ia762j/UEJe2Z1jAmEqZAK42tVW3g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.56.0.tgz", + "integrity": "sha512-bof7fbIlvqsyv/DtaXSck4VYQ9lPtoWNFCB/JY4snlFuJREXfZnm+Ej6yaCHfQvofJDXLDMTVxWscVSuQvVWUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.56.0.tgz", + "integrity": "sha512-KNa6lYHloW+7lTEkYGa37fpvPq+NKG/EHKM8+G/g9WDU7ls4sMqbVRV78J6LdNuVaeeK5WB9/9VAFbKxcbXKYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.56.0.tgz", + "integrity": "sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.56.0.tgz", + "integrity": "sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.56.0.tgz", + "integrity": "sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.56.0.tgz", + "integrity": "sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.56.0.tgz", + "integrity": "sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.56.0.tgz", + "integrity": "sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.56.0.tgz", + "integrity": "sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.56.0.tgz", + "integrity": "sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.56.0.tgz", + "integrity": "sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.56.0.tgz", + "integrity": "sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.56.0.tgz", + "integrity": "sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.56.0.tgz", + "integrity": "sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.56.0.tgz", + "integrity": "sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.56.0.tgz", + "integrity": "sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.56.0.tgz", + "integrity": "sha512-LhN/Reh+7F3RCgQIRbgw8ZMwUwyqJM+8pXNT6IIJAqm2IdKkzpCh/V9EdgOMBKuebIrzswqy4ATlrDgiOwbRcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.56.0.tgz", + "integrity": "sha512-kbFsOObXp3LBULg1d3JIUQMa9Kv4UitDmpS+k0tinPBz3watcUiV2/LUDMMucA6pZO3WGE27P7DsfaN54l9ing==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.56.0.tgz", + "integrity": "sha512-vSSgny54D6P4vf2izbtFm/TcWYedw7f8eBrOiGGecyHyQB9q4Kqentjaj8hToe+995nob/Wv48pDqL5a62EWtg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.56.0.tgz", + "integrity": "sha512-FeCnkPCTHQJFbiGG49KjV5YGW/8b9rrXAM2Mz2kiIoktq2qsJxRD5giEMEOD2lPdgs72upzefaUvS+nc8E3UzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.56.0.tgz", + "integrity": "sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@solidjs/router": { + "version": "0.15.4", + "resolved": "https://registry.npmjs.org/@solidjs/router/-/router-0.15.4.tgz", + "integrity": "sha512-WOpgg9a9T638cR+5FGbFi/IV4l2FpmBs1GpIMSPa0Ce9vyJN7Wts+X2PqMf9IYn0zUj2MlSJtm1gp7/HI/n5TQ==", + "license": "MIT", + "peerDependencies": { + "solid-js": "^1.8.6" + } + }, + "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/node": { + "version": "24.10.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.9.tgz", + "integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/babel-plugin-jsx-dom-expressions": { + "version": "0.40.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jsx-dom-expressions/-/babel-plugin-jsx-dom-expressions-0.40.3.tgz", + "integrity": "sha512-5HOwwt0BYiv/zxl7j8Pf2bGL6rDXfV6nUhLs8ygBX+EFJXzBPHM/euj9j/6deMZ6wa52Wb2PBaAV5U/jKwIY1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "7.18.6", + "@babel/plugin-syntax-jsx": "^7.18.6", + "@babel/types": "^7.20.7", + "html-entities": "2.3.3", + "parse5": "^7.1.2" + }, + "peerDependencies": { + "@babel/core": "^7.20.12" + } + }, + "node_modules/babel-plugin-jsx-dom-expressions/node_modules/@babel/helper-module-imports": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", + "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/babel-preset-solid": { + "version": "1.9.10", + "resolved": "https://registry.npmjs.org/babel-preset-solid/-/babel-preset-solid-1.9.10.tgz", + "integrity": "sha512-HCelrgua/Y+kqO8RyL04JBWS/cVdrtUv/h45GntgQY+cJl4eBcKkCDV3TdMjtKx1nXwRaR9QXslM/Npm1dxdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jsx-dom-expressions": "^0.40.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "solid-js": "^1.9.10" + }, + "peerDependenciesMeta": { + "solid-js": { + "optional": true + } + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.17", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.17.tgz", + "integrity": "sha512-agD0MgJFUP/4nvjqzIB29zRPUuCF7Ge6mEv9s8dHrtYD7QWXRcx75rOADE/d5ah1NI+0vkDl0yorDd5U852IQQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "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/caniuse-lite": { + "version": "1.0.30001765", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001765.tgz", + "integrity": "sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ==", + "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/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/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "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/electron-to-chromium": { + "version": "1.5.277", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.277.tgz", + "integrity": "sha512-wKXFZw4erWmmOz5N/grBoJ2XrNJGDFMu2+W5ACHza5rHtvsqrK4gb6rnLC7XxKB9WlJ+RmyQatuEXmtm86xbnw==", + "dev": true, + "license": "ISC" + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "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/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/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/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/html-entities": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz", + "integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "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/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/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/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/merge-anything": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/merge-anything/-/merge-anything-5.1.7.tgz", + "integrity": "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "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/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "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.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "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/rollup": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.56.0.tgz", + "integrity": "sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg==", + "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.56.0", + "@rollup/rollup-android-arm64": "4.56.0", + "@rollup/rollup-darwin-arm64": "4.56.0", + "@rollup/rollup-darwin-x64": "4.56.0", + "@rollup/rollup-freebsd-arm64": "4.56.0", + "@rollup/rollup-freebsd-x64": "4.56.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.56.0", + "@rollup/rollup-linux-arm-musleabihf": "4.56.0", + "@rollup/rollup-linux-arm64-gnu": "4.56.0", + "@rollup/rollup-linux-arm64-musl": "4.56.0", + "@rollup/rollup-linux-loong64-gnu": "4.56.0", + "@rollup/rollup-linux-loong64-musl": "4.56.0", + "@rollup/rollup-linux-ppc64-gnu": "4.56.0", + "@rollup/rollup-linux-ppc64-musl": "4.56.0", + "@rollup/rollup-linux-riscv64-gnu": "4.56.0", + "@rollup/rollup-linux-riscv64-musl": "4.56.0", + "@rollup/rollup-linux-s390x-gnu": "4.56.0", + "@rollup/rollup-linux-x64-gnu": "4.56.0", + "@rollup/rollup-linux-x64-musl": "4.56.0", + "@rollup/rollup-openbsd-x64": "4.56.0", + "@rollup/rollup-openharmony-arm64": "4.56.0", + "@rollup/rollup-win32-arm64-msvc": "4.56.0", + "@rollup/rollup-win32-ia32-msvc": "4.56.0", + "@rollup/rollup-win32-x64-gnu": "4.56.0", + "@rollup/rollup-win32-x64-msvc": "4.56.0", + "fsevents": "~2.3.2" + } + }, + "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/seroval": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.3.2.tgz", + "integrity": "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/seroval-plugins": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.3.3.tgz", + "integrity": "sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "seroval": "^1.0" + } + }, + "node_modules/solid-js": { + "version": "1.9.10", + "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.10.tgz", + "integrity": "sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew==", + "license": "MIT", + "dependencies": { + "csstype": "^3.1.0", + "seroval": "~1.3.0", + "seroval-plugins": "~1.3.0" + } + }, + "node_modules/solid-refresh": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/solid-refresh/-/solid-refresh-0.6.3.tgz", + "integrity": "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/generator": "^7.23.6", + "@babel/helper-module-imports": "^7.22.15", + "@babel/types": "^7.23.6" + }, + "peerDependencies": { + "solid-js": "^1.3" + } + }, + "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/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/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/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/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/vite-plugin-solid": { + "version": "2.11.10", + "resolved": "https://registry.npmjs.org/vite-plugin-solid/-/vite-plugin-solid-2.11.10.tgz", + "integrity": "sha512-Yr1dQybmtDtDAHkii6hXuc1oVH9CPcS/Zb2jN/P36qqcrkNnVPsMTzQ06jyzFPFjj3U1IYKMVt/9ZqcwGCEbjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.23.3", + "@types/babel__core": "^7.20.4", + "babel-preset-solid": "^1.8.4", + "merge-anything": "^5.1.7", + "solid-refresh": "^0.6.3", + "vitefu": "^1.0.4" + }, + "peerDependencies": { + "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", + "solid-js": "^1.7.2", + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "@testing-library/jest-dom": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.1.tgz", + "integrity": "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==", + "dev": true, + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "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" + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..31f293f --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,21 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@solidjs/router": "^0.15.4", + "solid-js": "^1.9.10" + }, + "devDependencies": { + "@types/node": "^24.10.1", + "typescript": "~5.9.3", + "vite": "^7.2.4", + "vite-plugin-solid": "^2.11.10" + } +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..923483f --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,23 @@ +import { Router, Route } from '@solidjs/router'; +import Layout from './components/Layout'; +import Dashboard from './pages/Dashboard'; +import Login from './pages/Login'; +import Help from './pages/Help'; +import About from './pages/About'; +import History from './pages/History'; +import NotFound from './pages/NotFound'; + +function App() { + return ( + + + + + + + + + ); +} + +export default App; diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx new file mode 100644 index 0000000..4d96e9a --- /dev/null +++ b/frontend/src/components/Layout.tsx @@ -0,0 +1,129 @@ +import { A } from '@solidjs/router'; +import { Show, createSignal } from 'solid-js'; +import type { JSX } from 'solid-js'; +import { auth } from '../lib/auth'; +import SettingsModal from './SettingsModal'; +import PrivacyPolicyModal from './PrivacyPolicyModal'; +import TermsOfServiceModal from './TermsOfServiceModal'; +import ThemeDropdown from './ThemeDropdown'; + +export default function Layout(props: { children?: JSX.Element }) { + const [isSettingsOpen, setIsSettingsOpen] = createSignal(false); + const [isPrivacyOpen, setIsPrivacyOpen] = createSignal(false); + const [isTermsOpen, setIsTermsOpen] = createSignal(false); + + const handleSettingsClick = (e: Event) => { + e.preventDefault(); + setIsSettingsOpen(true); + }; + + const handlePrivacyClick = (e: Event) => { + e.preventDefault(); + setIsPrivacyOpen(true); + }; + + const handleTermsClick = (e: Event) => { + e.preventDefault(); + setIsTermsOpen(true); + }; + + return ( + <> +
+
+ +
+
+ + + + help + +
+
+ Log In + } + > + + + DEMO + + +
+ +
+
+
+ +
+ {props.children} +
+ + +
+ + setIsSettingsOpen(false)} + /> + setIsPrivacyOpen(false)} + /> + setIsTermsOpen(false)} + /> + + ); +} diff --git a/frontend/src/components/Modal.tsx b/frontend/src/components/Modal.tsx new file mode 100644 index 0000000..d233d64 --- /dev/null +++ b/frontend/src/components/Modal.tsx @@ -0,0 +1,87 @@ +import { createSignal, Show, createEffect, onCleanup } from 'solid-js'; +import { Portal } from 'solid-js/web'; +import type { JSX } from 'solid-js'; + +interface ModalProps { + isOpen: boolean; + onClose: () => void; + title: string; + children: JSX.Element; + size?: 'small' | 'medium' | 'large' | 'full'; + closeOnEscape?: boolean; + closeOnOverlay?: boolean; +} + +export default function Modal(props: ModalProps) { + const size = () => props.size || 'medium'; + const closeOnEscape = () => props.closeOnEscape ?? true; + const closeOnOverlay = () => props.closeOnOverlay ?? true; + + const [showContent, setShowContent] = createSignal(false); + + const handleEscape = (e: KeyboardEvent) => { + if (closeOnEscape() && e.key === 'Escape') { + handleClose(); + } + }; + + const handleOverlayClick = (e: MouseEvent) => { + if (closeOnOverlay() && e.target === e.currentTarget) { + handleClose(); + } + }; + + createEffect(() => { + if (props.isOpen) { + document.body.style.overflow = 'hidden'; + setTimeout(() => setShowContent(true), 10); + + if (closeOnEscape()) { + document.addEventListener('keydown', handleEscape); + } + } else { + document.body.style.overflow = ''; + setShowContent(false); + document.removeEventListener('keydown', handleEscape); + } + }); + + onCleanup(() => { + document.body.style.overflow = ''; + document.removeEventListener('keydown', handleEscape); + }); + + const handleClose = () => { + setShowContent(false); + setTimeout(() => { + props.onClose(); + }, 200); + }; + + return ( + + +
+
+ + +
+
+
+
+ ); +} diff --git a/frontend/src/components/PrivacyPolicyModal.tsx b/frontend/src/components/PrivacyPolicyModal.tsx new file mode 100644 index 0000000..d4639cb --- /dev/null +++ b/frontend/src/components/PrivacyPolicyModal.tsx @@ -0,0 +1,80 @@ +import Modal from './Modal'; + +interface PrivacyPolicyModalProps { + isOpen: boolean; + onClose: () => void; +} + +export default function PrivacyPolicyModal(props: PrivacyPolicyModalProps) { + return ( + + + + ); +} diff --git a/frontend/src/components/SettingsModal.tsx b/frontend/src/components/SettingsModal.tsx new file mode 100644 index 0000000..e41f297 --- /dev/null +++ b/frontend/src/components/SettingsModal.tsx @@ -0,0 +1,285 @@ +import { createSignal } from 'solid-js'; +import Modal from './Modal'; + +interface SettingsModalProps { + isOpen: boolean; + onClose: () => void; +} + +export default function SettingsModal(props: SettingsModalProps) { + const [enableNotifications, setEnableNotifications] = createSignal( + localStorage.getItem('setting_enable_notifications') === 'true' + ); + const [emailNotifications, setEmailNotifications] = createSignal( + localStorage.getItem('setting_email_notifications') === 'true' + ); + const [notificationSound, setNotificationSound] = createSignal( + localStorage.getItem('setting_notification_sound') !== 'false' + ); + const [autoSync, setAutoSync] = createSignal( + localStorage.getItem('setting_auto_sync') !== 'false' + ); + const [compactMode, setCompactMode] = createSignal( + localStorage.getItem('setting_compact_mode') === 'true' + ); + const [usageAnalytics, setUsageAnalytics] = createSignal( + localStorage.getItem('setting_usage_analytics') !== 'false' + ); + + const handleToggle = (key: string, setter: (value: boolean) => void) => (e: InputEvent) => { + const checked = (e.currentTarget as HTMLInputElement).checked; + setter(checked); + localStorage.setItem(`setting_${key}`, String(checked)); + }; + + return ( + +
+ {/* Account Section */} +
+

Account

+
+
+
+ email +
+
Email Address
+
demo@taskcraft.com
+
+
+
+ +
+
+ person +
+
Display Name
+
Demo User
+
+
+
+ +
+
+ lock +
+
Password
+
Change your account password
+
+
+ +
+
+
+ + {/* Notifications Section */} +
+

Notifications

+
+
+
+ notifications +
+
Enable Notifications
+
Receive notifications for upcoming events and reminders
+
+
+ +
+ +
+
+ email +
+
Email Notifications
+
Get email summaries of your daily tasks
+
+
+ +
+ +
+
+ volume_up +
+
Notification Sound
+
Play a sound when you receive notifications
+
+
+ +
+
+
+ + {/* Calendar Integration Section */} +
+

Calendar Integration

+
+
+
+ event +
+
Connected Calendar
+
Google Calendar - demo@taskcraft.com
+
+
+ +
+ +
+
+ calendar_today +
+
Default Calendar
+
Primary
+
+
+
+ +
+
+ sync +
+
Auto-Sync
+
Automatically sync events to your calendar
+
+
+ +
+
+
+ + {/* Appearance Section */} +
+

Appearance

+
+
+
+ palette +
+
Theme
+
Choose your color theme from the header
+
+
+
+ +
+
+ compress +
+
Compact Mode
+
Reduce spacing for a denser interface
+
+
+ +
+
+
+ + {/* Privacy & Security Section */} +
+

Privacy & Security

+
+
+
+ analytics +
+
Usage Analytics
+
Help improve TaskCraft by sharing anonymous usage data
+
+
+ +
+ +
+
+ download +
+
Export Data
+
Download all your data in JSON format
+
+
+ +
+ +
+
+ + delete_forever + +
+
Delete Account
+
Permanently delete your account and all data
+
+
+ +
+
+
+
+
+ ); +} diff --git a/frontend/src/components/TermsOfServiceModal.tsx b/frontend/src/components/TermsOfServiceModal.tsx new file mode 100644 index 0000000..97ca7ec --- /dev/null +++ b/frontend/src/components/TermsOfServiceModal.tsx @@ -0,0 +1,98 @@ +import Modal from './Modal'; + +interface TermsOfServiceModalProps { + isOpen: boolean; + onClose: () => void; +} + +export default function TermsOfServiceModal(props: TermsOfServiceModalProps) { + return ( + + + + ); +} diff --git a/frontend/src/components/ThemeDropdown.tsx b/frontend/src/components/ThemeDropdown.tsx new file mode 100644 index 0000000..0a48782 --- /dev/null +++ b/frontend/src/components/ThemeDropdown.tsx @@ -0,0 +1,66 @@ +import { createSignal, For, onCleanup } from 'solid-js'; +import { theme } from '../lib/theme'; + +export default function ThemeDropdown() { + const [isOpen, setIsOpen] = createSignal(false); + + const handleClickOutside = (e: MouseEvent) => { + const target = e.target as HTMLElement; + if (!target.closest('.theme-dropdown-wrapper')) { + setIsOpen(false); + } + }; + + const toggleDropdown = (e: Event) => { + e.stopPropagation(); + const nextState = !isOpen(); + setIsOpen(nextState); + + if (nextState) { + setTimeout(() => { + document.addEventListener('click', handleClickOutside); + }, 0); + } else { + document.removeEventListener('click', handleClickOutside); + } + }; + + const handleThemeSelect = (themeId: string) => { + theme.setTheme(themeId); + setIsOpen(false); + document.removeEventListener('click', handleClickOutside); + }; + + onCleanup(() => { + document.removeEventListener('click', handleClickOutside); + }); + + return ( +
+ + +
+ + {(themeItem) => ( +
handleThemeSelect(themeItem.id)} + > +
+
{themeItem.name}
+ + check + +
+ )} +
+
+
+ ); +} diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..52c1e5b --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,27 @@ +/* ============================================ + CSS ENTRY POINT + Import order: themes → base → utilities → components → pages + ============================================ */ + +/* 1. Theme Variables (must load first) */ +@import '../css/themes.css'; + +/* 2. Base Styles & Layout */ +@import '../css/common.css'; + +/* 3. Utilities (reusable utility classes) */ +@import '../css/utilities.css'; + +/* 4. Components */ +@import '../css/components/badges.css'; +@import '../css/components/buttons.css'; +@import '../css/components/cards.css'; +@import '../css/components/forms.css'; +@import '../css/components/modals.css'; +@import '../css/components/other.css'; + +/* 5. Pages */ +@import '../css/pages/auth.css'; +@import '../css/pages/dashboard.css'; +@import '../css/pages/help-about.css'; +@import '../css/pages/history.css'; diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx new file mode 100644 index 0000000..f67cd20 --- /dev/null +++ b/frontend/src/index.tsx @@ -0,0 +1,8 @@ +/* @refresh reload */ +import { render } from 'solid-js/web' +import './index.css' +import App from './App.tsx' + +const root = document.getElementById('root') + +render(() => , root!) diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts new file mode 100644 index 0000000..59b4c56 --- /dev/null +++ b/frontend/src/lib/api.ts @@ -0,0 +1,113 @@ +class ApiClient { + private baseURL: string; + private defaultHeaders: Record; + + constructor(baseURL = 'http://localhost:8000/api') { + this.baseURL = baseURL; + this.defaultHeaders = { + 'Content-Type': 'application/json', + }; + } + + setAuthToken(token: string | null) { + if (token) { + this.defaultHeaders['Authorization'] = `Bearer ${token}`; + } else { + delete this.defaultHeaders['Authorization']; + } + } + + getURL(endpoint: string): string { + return `${this.baseURL}${endpoint}`; + } + + async handleResponse(response: Response) { + const contentType = response.headers.get('content-type'); + const isJSON = contentType && contentType.includes('application/json'); + + if (!response.ok) { + const error: any = new Error('API request failed'); + error.status = response.status; + error.statusText = response.statusText; + + if (isJSON) { + error.data = await response.json(); + } else { + error.data = await response.text(); + } + + throw error; + } + + return isJSON ? await response.json() : await response.text(); + } + + async get(endpoint: string, options: RequestInit = {}): Promise { + const response = await fetch(this.getURL(endpoint), { + method: 'GET', + headers: { ...this.defaultHeaders, ...options.headers }, + ...options, + }); + + return this.handleResponse(response); + } + + async post(endpoint: string, data: any = null, options: RequestInit = {}): Promise { + const response = await fetch(this.getURL(endpoint), { + method: 'POST', + headers: { ...this.defaultHeaders, ...options.headers }, + body: data ? JSON.stringify(data) : null, + ...options, + }); + + return this.handleResponse(response); + } + + async put(endpoint: string, data: any = null, options: RequestInit = {}): Promise { + const response = await fetch(this.getURL(endpoint), { + method: 'PUT', + headers: { ...this.defaultHeaders, ...options.headers }, + body: data ? JSON.stringify(data) : null, + ...options, + }); + + return this.handleResponse(response); + } + + async patch(endpoint: string, data: any = null, options: RequestInit = {}): Promise { + const response = await fetch(this.getURL(endpoint), { + method: 'PATCH', + headers: { ...this.defaultHeaders, ...options.headers }, + body: data ? JSON.stringify(data) : null, + ...options, + }); + + return this.handleResponse(response); + } + + async delete(endpoint: string, options: RequestInit = {}): Promise { + const response = await fetch(this.getURL(endpoint), { + method: 'DELETE', + headers: { ...this.defaultHeaders, ...options.headers }, + ...options, + }); + + return this.handleResponse(response); + } + + async upload(endpoint: string, formData: FormData, options: RequestInit = {}): Promise { + const headers = { ...this.defaultHeaders }; + delete headers['Content-Type']; + + const response = await fetch(this.getURL(endpoint), { + method: 'POST', + headers: { ...headers, ...options.headers }, + body: formData, + ...options, + }); + + return this.handleResponse(response); + } +} + +export const api = new ApiClient(); diff --git a/frontend/src/lib/auth.ts b/frontend/src/lib/auth.ts new file mode 100644 index 0000000..1d2c0c5 --- /dev/null +++ b/frontend/src/lib/auth.ts @@ -0,0 +1,213 @@ +import { createStore } from 'solid-js/store'; +import { api } from './api'; + +interface User { + id: string; + email: string; + name: string; + avatar?: string | null; + preferences?: { + theme?: string; + notifications?: boolean; + }; +} + +interface AuthState { + user: User | null; + token: string | null; + isMockMode: boolean; +} + +const mockUsers: User[] = [ + { + id: 'demo-user-1', + email: 'demo@taskcraft.dev', + name: 'Demo User', + avatar: null, + preferences: { + theme: 'light', + notifications: true + } + } +]; + +const mockToken = 'mock-jwt-token-' + Math.random().toString(36).substring(7); +const defaultMockUser = mockUsers[0]; + +function isMockEnvironment(): boolean { + const hostname = window.location.hostname; + return hostname === 'localhost' || + hostname === '127.0.0.1' || + hostname.startsWith('192.168.') || + hostname.endsWith('.local'); +} + +function loadAuthState(): AuthState { + const savedToken = localStorage.getItem('auth_token'); + const savedUser = localStorage.getItem('auth_user'); + const isMockMode = localStorage.getItem('auth_mock_mode') === 'true'; + + if (savedToken && savedUser) { + try { + const user = JSON.parse(savedUser); + api.setAuthToken(savedToken); + return { token: savedToken, user, isMockMode }; + } catch (error) { + console.error('Failed to load auth state:', error); + localStorage.removeItem('auth_token'); + localStorage.removeItem('auth_user'); + localStorage.removeItem('auth_mock_mode'); + } + } + + return { user: null, token: null, isMockMode: false }; +} + +const [authState, setAuthState] = createStore(loadAuthState()); + +function saveAuthState() { + if (authState.token && authState.user) { + localStorage.setItem('auth_token', authState.token); + localStorage.setItem('auth_user', JSON.stringify(authState.user)); + } +} + +function clearAuthState() { + setAuthState({ token: null, user: null, isMockMode: false }); + localStorage.removeItem('auth_token'); + localStorage.removeItem('auth_user'); + localStorage.removeItem('auth_mock_mode'); + api.setAuthToken(null); +} + +export const auth = { + get state() { + return authState; + }, + + isAuthenticated(): boolean { + return !!authState.token && !!authState.user; + }, + + getUser(): User | null { + return authState.user; + }, + + getToken(): string | null { + return authState.token; + }, + + async register(userData: any) { + try { + const response = await api.post('/auth/register', userData); + + if (response.token && response.user) { + setAuthState({ token: response.token, user: response.user, isMockMode: false }); + saveAuthState(); + api.setAuthToken(response.token); + } + + return response; + } catch (error) { + console.error('Registration failed:', error); + throw error; + } + }, + + async login(email: string, password: string) { + try { + const response = await api.post('/auth/login', { email, password }); + + if (response.token && response.user) { + setAuthState({ token: response.token, user: response.user, isMockMode: false }); + saveAuthState(); + api.setAuthToken(response.token); + } + + return response; + } catch (error) { + console.error('Login failed:', error); + throw error; + } + }, + + async loginAsMock(mockUser: User | null = null) { + const user = mockUser || defaultMockUser; + + setAuthState({ token: mockToken, user, isMockMode: true }); + localStorage.setItem('auth_mock_mode', 'true'); + saveAuthState(); + api.setAuthToken(mockToken); + + console.warn('[MOCK AUTH] Logged in as:', user.email); + return { token: mockToken, user }; + }, + + async logout() { + try { + if (authState.token) { + await api.post('/auth/logout').catch(() => { + // Ignore errors + }); + } + } finally { + clearAuthState(); + } + }, + + async updateProfile(userData: any) { + try { + const response = await api.patch('/auth/profile', userData); + + if (response.user) { + setAuthState('user', (prev) => ({ ...prev!, ...response.user })); + saveAuthState(); + } + + return response; + } catch (error) { + console.error('Profile update failed:', error); + throw error; + } + }, + + async changePassword(currentPassword: string, newPassword: string) { + try { + const response = await api.post('/auth/change-password', { + current_password: currentPassword, + new_password: newPassword, + }); + + return response; + } catch (error) { + console.error('Password change failed:', error); + throw error; + } + }, + + async requestPasswordReset(email: string) { + try { + const response = await api.post('/auth/password-reset-request', { email }); + return response; + } catch (error) { + console.error('Password reset request failed:', error); + throw error; + } + }, + + async resetPassword(token: string, newPassword: string) { + try { + const response = await api.post('/auth/password-reset', { + token, + new_password: newPassword, + }); + + return response; + } catch (error) { + console.error('Password reset failed:', error); + throw error; + } + }, + + isMockEnvironment, +}; diff --git a/frontend/src/lib/theme.ts b/frontend/src/lib/theme.ts new file mode 100644 index 0000000..5417a98 --- /dev/null +++ b/frontend/src/lib/theme.ts @@ -0,0 +1,96 @@ +import { createSignal, createEffect } from 'solid-js'; + +const THEME_STORAGE_KEY = 'app_theme'; +const DEFAULT_THEME = 'default'; + +interface Theme { + id: string; + name: string; +} + +const THEMES: Record = { + default: 'Golden Hour', + ocean: 'Ocean Blue', + forest: 'Forest Green', + midnight: 'Midnight Purple', + sunset: 'Sunset Orange', + rose: 'Rose Pink', + daylight: 'Daylight', + cloud: 'Cloud', + meadow: 'Meadow', + pastel: 'Pastel', +}; + +function loadTheme(): string { + try { + const saved = localStorage.getItem(THEME_STORAGE_KEY); + if (saved && THEMES[saved]) { + return saved; + } + } catch (error) { + console.error('Failed to load theme:', error); + } + return DEFAULT_THEME; +} + +function saveTheme(themeId: string): void { + try { + localStorage.setItem(THEME_STORAGE_KEY, themeId); + } catch (error) { + console.error('Failed to save theme:', error); + } +} + +function applyTheme(themeId: string): void { + if (themeId === DEFAULT_THEME) { + document.documentElement.removeAttribute('data-theme'); + } else { + document.documentElement.setAttribute('data-theme', themeId); + } +} + +const [currentTheme, setCurrentTheme] = createSignal(loadTheme()); + +createEffect(() => { + const themeId = currentTheme(); + applyTheme(themeId); + saveTheme(themeId); +}); + +export const theme = { + get current() { + return currentTheme(); + }, + + setTheme(themeId: string) { + if (!THEMES[themeId]) { + console.warn(`Theme "${themeId}" not found, falling back to default`); + themeId = DEFAULT_THEME; + } + setCurrentTheme(themeId); + }, + + getAvailableThemes(): Array { + const current = currentTheme(); + return Object.entries(THEMES).map(([id, name]) => ({ + id, + name, + isCurrent: id === current, + })); + }, + + getCurrentTheme(): Theme { + const id = currentTheme(); + return { + id, + name: THEMES[id], + }; + }, + + nextTheme() { + const themeIds = Object.keys(THEMES); + const currentIndex = themeIds.indexOf(currentTheme()); + const nextIndex = (currentIndex + 1) % themeIds.length; + setCurrentTheme(themeIds[nextIndex]); + }, +}; diff --git a/frontend/src/pages/About.tsx b/frontend/src/pages/About.tsx new file mode 100644 index 0000000..210a578 --- /dev/null +++ b/frontend/src/pages/About.tsx @@ -0,0 +1,89 @@ +export default function About() { + return ( +
+
+
+
+ +
+ + + +
+ +

About TaskCraft

+

Organize your tasks and calendar using natural language.

+
+ +
+
+

Our Mission

+

+ TaskCraft makes it easy to capture and organize your thoughts without complexity. By converting natural language into structured tasks and calendar events, we help you keep track of what matters. +

+

+ The platform uses natural language processing to turn your scattered ideas into organized tasks, scheduled events, and timely reminders. +

+
+
+ +
+
+

Core Values

+
+
+
+ speed +
+

Keep It Simple

+

Task management works best when it doesn't get in your way. We focus on clarity and ease of use.

+
+ +
+
+ lock +
+

Privacy First

+

Your data belongs to you. We use industry-standard encryption and never share your information.

+
+ +
+
+ psychology +
+

Continuous Improvement

+

We use your feedback to make the platform more accurate and helpful over time.

+
+
+
+
+ +
+

The Team

+

+ TaskCraft is developed by a distributed team of engineers and designers. +

+
+ More information coming soon +
+
+ +
+

Contact

+

+ Have questions or feedback? We'd like to hear from you. +

+
+ + +
+
+
+ ); +} diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx new file mode 100644 index 0000000..ef28597 --- /dev/null +++ b/frontend/src/pages/Dashboard.tsx @@ -0,0 +1,271 @@ +import { createSignal, Show } from 'solid-js'; +import { api } from '../lib/api'; +import { auth } from '../lib/auth'; + +interface EventDraft { + title: string; + start_at: string | null; + end_at: string | null; + notes: string; + missing_info: string[]; +} + +interface TaskCreateResponse { + raw_text: string; + event: EventDraft; +} + +export default function Dashboard() { + const [taskText, setTaskText] = createSignal(''); + const [isGenerating, setIsGenerating] = createSignal(false); + const [lastEvent, setLastEvent] = createSignal(null); + const [error, setError] = createSignal(null); + + const handleGenerate = async () => { + const text = taskText().trim(); + + if (!text) { + setError('Please enter a task'); + return; + } + + try { + setIsGenerating(true); + setError(null); + setLastEvent(null); + + const user = auth.getUser(); + const payload = { + text, + email: user ? user.email : null, + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || "Asia/Jerusalem" + }; + + const response: TaskCreateResponse = await api.post('/tasks', payload); + console.log('Generated task output:', response); + + setLastEvent(response.event); + setTaskText(''); + } catch (err: any) { + console.error('Failed to generate tasks:', err); + + if (err.status === 503) { + setError('AI service is temporarily unavailable. Please try again later.'); + } else if (err.status >= 400 && err.status < 500) { + setError(err.data?.detail || 'Invalid request. Please check your input.'); + } else { + setError('Failed to generate task. Please try again.'); + } + } finally { + setIsGenerating(false); + } + }; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleGenerate(); + } + }; + + const formatDateTime = (dateStr: string | null) => { + if (!dateStr) return null; + try { + const date = new Date(dateStr); + return date.toLocaleString(undefined, { + dateStyle: 'medium', + timeStyle: 'short' + }); + } catch { + return dateStr; + } + }; + + return ( +
+
+
+

+ Turn Chaos into Calendar. +

+

+ Simply type your next tasks below and we'll handle the rest. +

+
+ + +
+ error +
{error()}
+
+
+ + + {(event) => ( +
+
+ check_circle +
+

+ Event Created: {event().title} +

+ + +
+ +
+ Start: {formatDateTime(event().start_at)} +
+
+ +
+ End: {formatDateTime(event().end_at)} +
+
+
+
+ + +
+ Notes: {event().notes} +
+
+ + 0}> +
+
+ warning + Missing Information +
+
    + {event().missing_info.map((info) => ( +
  • {info}
  • + ))} +
+
+
+
+
+
+ )} +
+ +
+
+