diff --git a/.claude/commands/workflow/test-fix-gen.md b/.claude/commands/workflow/test-fix-gen.md
index c24dc4f7..33c7e207 100644
--- a/.claude/commands/workflow/test-fix-gen.md
+++ b/.claude/commands/workflow/test-fix-gen.md
@@ -7,69 +7,40 @@ allowed-tools: SlashCommand(*), TodoWrite(*), Read(*), Bash(*)
# Workflow Test-Fix Generation Command (/workflow:test-fix-gen)
-## Overview
+## Quick Reference
-### What It Does
+### Command Scope
-This command creates an independent test-fix workflow session for existing code. It orchestrates a 5-phase process to analyze implementation, generate test requirements, and create executable test generation and fix tasks.
+| Aspect | Description |
+|--------|-------------|
+| **Purpose** | Generate test-fix workflow session with task JSON files |
+| **Output** | IMPL-001.json, IMPL-001.3-validation.json, IMPL-001.5-review.json, IMPL-002.json |
+| **Does NOT** | Execute tests, apply fixes, handle test failures |
+| **Next Step** | Must call `/workflow:test-cycle-execute` after this command |
-**CRITICAL - Command Scope**:
-- **This command ONLY generates task JSON files** (IMPL-001.json, IMPL-002.json)
-- **Does NOT execute tests or apply fixes** - all execution happens in separate orchestrator
-- **Must call `/workflow:test-cycle-execute`** after this command to actually run tests and fixes
-- **Test failure handling happens in test-cycle-execute**, not here
+### Task Pipeline
-### Dual-Mode Support
-
-**Automatic mode detection** based on input pattern:
-
-| Mode | Input Pattern | Context Source | Use Case |
-|------|--------------|----------------|----------|
-| **Session Mode** | `WFS-xxx` | Source session summaries | Test validation for completed workflow |
-| **Prompt Mode** | Text or file path | Direct codebase analysis | Test generation from description |
-
-**Detection Logic**:
-```bash
-if [[ "$input" == WFS-* ]]; then
- MODE="session" # Use test-context-gather
-else
- MODE="prompt" # Use context-gather
-fi
```
-
-### Core Principles
-
-- **Dual Input Support**: Accepts session ID (WFS-xxx) or feature description/file path
-- **Session Isolation**: Creates independent `WFS-test-[slug]` session
-- **Context-First**: Gathers implementation context via appropriate method
-- **Format Reuse**: Creates standard `IMPL-*.json` tasks with `meta.type: "test-fix"`
-- **Semantic CLI Selection**: CLI tool usage determined from user's task description
-- **Automatic Detection**: Input pattern determines execution mode
+IMPL-001 (Test Generation) → IMPL-001.3 (Code Validation) → IMPL-001.5 (Test Quality) → IMPL-002 (Test Execution)
+ @code-developer @test-fix-agent @test-fix-agent @test-fix-agent
+```
### Coordinator Role
This command is a **pure planning coordinator**:
-- Does NOT analyze code directly
-- Does NOT generate tests or documentation
-- Does NOT execute tests or apply fixes
-- Does NOT handle test failures or iterations
- ONLY coordinates slash commands to generate task JSON files
-- Parses outputs to pass data between phases
-- Creates independent test workflow session
-- **All execution delegated to `/workflow:test-cycle-execute`**
+- Does NOT analyze code, generate tests, execute tests, or apply fixes
+- All execution delegated to `/workflow:test-cycle-execute`
-**Task Attachment Model**:
-- SlashCommand execute **expands workflow** by attaching sub-tasks to current TodoWrite
-- When executing a sub-command (e.g., `/workflow:tools:test-context-gather`), its internal tasks are attached to the orchestrator's TodoWrite
-- Orchestrator **executes these attached tasks** sequentially
-- After completion, attached tasks are **collapsed** back to high-level phase summary
-- This is **task expansion**, not external delegation
+### Core Principles
-**Auto-Continue Mechanism**:
-- TodoList tracks current phase status and dynamically manages task attachment/collapse
-- When each phase finishes executing, automatically execute next pending phase
-- All phases run autonomously without user interaction
-- **⚠️ CONTINUOUS EXECUTION** - Do not stop until all phases complete
+| Principle | Description |
+|-----------|-------------|
+| **Session Isolation** | Creates independent `WFS-test-[slug]` session |
+| **Context-First** | Gathers implementation context via appropriate method |
+| **Format Reuse** | Creates standard `IMPL-*.json` tasks with `meta.type: "test-fix"` |
+| **Semantic CLI Selection** | CLI tool usage determined from user's task description |
+| **Automatic Detection** | Input pattern determines execution mode |
---
@@ -78,245 +49,156 @@ This command is a **pure planning coordinator**:
### Command Syntax
```bash
-# Basic syntax
/workflow:test-fix-gen
-# Input
- # Session ID, description, or file path
+# INPUT can be:
+# - Session ID: WFS-user-auth-v2
+# - Description: "Test the user authentication API"
+# - File path: ./docs/api-requirements.md
```
-**Note**: CLI tool usage is determined semantically from the task description. To request CLI execution, include it in your description (e.g., "use Codex for automated fixes").
+### Mode Detection
-### Usage Examples
+**Automatic mode detection** based on input pattern:
-#### Session Mode
```bash
-# Test validation for completed implementation
+if [[ "$input" == WFS-* ]]; then
+ MODE="session" # Use test-context-gather
+else
+ MODE="prompt" # Use context-gather
+fi
+```
+
+| Mode | Input Pattern | Context Source | Use Case |
+|------|--------------|----------------|----------|
+| **Session** | `WFS-xxx` | Source session summaries | Test validation for completed workflow |
+| **Prompt** | Text or file path | Direct codebase analysis | Ad-hoc test generation |
+
+### Examples
+
+```bash
+# Session Mode - test validation for completed implementation
/workflow:test-fix-gen WFS-user-auth-v2
-# With semantic CLI request
-/workflow:test-fix-gen WFS-api-endpoints # Add "use Codex" in description for automated fixes
-```
-
-#### Prompt Mode - Text Description
-```bash
-# Generate tests from feature description
+# Prompt Mode - text description
/workflow:test-fix-gen "Test the user authentication API endpoints in src/auth/api.ts"
-# With CLI execution (semantic)
-/workflow:test-fix-gen "Test user registration and login flows, use Codex for automated fixes"
-```
-
-#### Prompt Mode - File Reference
-```bash
-# Generate tests from requirements file
+# Prompt Mode - file reference
/workflow:test-fix-gen ./docs/api-requirements.md
+
+# With CLI tool preference (semantic detection)
+/workflow:test-fix-gen "Test user registration, use Codex for automated fixes"
```
-### Mode Comparison
-
-| Aspect | Session Mode | Prompt Mode |
-|--------|-------------|-------------|
-| **Phase 1** | Create `WFS-test-[source]` with `source_session_id` | Create `WFS-test-[slug]` without `source_session_id` |
-| **Phase 2** | `/workflow:tools:test-context-gather` | `/workflow:tools:context-gather` |
-| **Phase 3-5** | Identical | Identical |
-| **Context** | Source session summaries + artifacts | Direct codebase analysis |
-
---
-## Execution Flow
+## Execution Phases
-### Core Execution Rules
+### Execution Rules
-1. **Start Immediately**: First action is TodoWrite, second is execute Phase 1 session creation
+1. **Start Immediately**: First action is TodoWrite, second is Phase 1 execution
2. **No Preliminary Analysis**: Do not read files before Phase 1
3. **Parse Every Output**: Extract required data from each phase for next phase
4. **Sequential Execution**: Each phase depends on previous phase's output
5. **Complete All Phases**: Do not return until Phase 5 completes
-6. **Track Progress**: Update TodoWrite dynamically with task attachment/collapse pattern
-7. **Automatic Detection**: Mode auto-detected from input pattern
-8. **Semantic CLI Detection**: CLI tool usage determined from user's task description for Phase 4
-9. **Task Attachment Model**: SlashCommand execute **attaches** sub-tasks to current workflow. Orchestrator **executes** these attached tasks itself, then **collapses** them after completion
-10. **⚠️ CRITICAL: DO NOT STOP**: Continuous multi-phase workflow. After executing all attached tasks, immediately collapse them and execute next phase
+6. **⚠️ CONTINUOUS EXECUTION**: Do not stop between phases
-### 5-Phase Execution
-
-#### Phase 1: Create Test Session
-
-**Step 1.0: Load Source Session Intent (Session Mode Only)** - Preserve user's original task description for semantic CLI selection
+### Phase 1: Create Test Session
+**Execute**:
```javascript
-// Session Mode: Read source session metadata to get original task description
+// Session Mode - preserve original task description
Read(".workflow/active/[sourceSessionId]/workflow-session.json")
-// OR if context-package exists:
-Read(".workflow/active/[sourceSessionId]/.process/context-package.json")
+SlashCommand("/workflow:session:start --type test --new \"Test validation for [sourceSessionId]: [originalTaskDescription]\"")
-// Extract: metadata.task_description or project/description field
-// This preserves user's CLI tool preferences (e.g., "use Codex for fixes")
+// Prompt Mode - use user's description directly
+SlashCommand("/workflow:session:start --type test --new \"Test generation for: [description]\"")
```
-**Step 1.1: Execute** - Create test workflow session with preserved intent
-
-```javascript
-// Session Mode - Include original task description to enable semantic CLI selection
-SlashCommand(command="/workflow:session:start --type test --new \"Test validation for [sourceSessionId]: [originalTaskDescription]\"")
-
-// Prompt Mode - User's description already contains their intent
-SlashCommand(command="/workflow:session:start --type test --new \"Test generation for: [description]\"")
-```
-
-**Input**: User argument (session ID, description, or file path)
-
-**Expected Behavior**:
-- Creates new session: `WFS-test-[slug]`
-- Writes `workflow-session.json` metadata with `type: "test"`
- - **Session Mode**: Additionally includes `source_session_id: "[sourceId]"`, description with original user intent
- - **Prompt Mode**: Uses user's description (already contains intent)
-- Returns new session ID
-
-**Parse Output**:
-- Extract: `testSessionId` (pattern: `WFS-test-[slug]`)
+**Output**: `testSessionId` (pattern: `WFS-test-[slug]`)
**Validation**:
-- **Session Mode**: Source session exists with completed IMPL tasks
-- **Both Modes**: New test session directory created with metadata
-
-**TodoWrite**: Mark phase 1 completed, phase 2 in_progress
+- Session Mode: Source session exists with completed IMPL tasks
+- Both Modes: New test session directory created with metadata
---
-#### Phase 2: Gather Test Context
-
-**Step 2.1: Execute** - Gather test context via appropriate method
+### Phase 2: Gather Test Context
+**Execute**:
```javascript
// Session Mode
-SlashCommand(command="/workflow:tools:test-context-gather --session [testSessionId]")
+SlashCommand("/workflow:tools:test-context-gather --session [testSessionId]")
// Prompt Mode
-SlashCommand(command="/workflow:tools:context-gather --session [testSessionId] \"[task_description]\"")
+SlashCommand("/workflow:tools:context-gather --session [testSessionId] \"[task_description]\"")
```
-**Input**: `testSessionId` from Phase 1
-
**Expected Behavior**:
-- **Session Mode**:
- - Load source session implementation context and summaries
- - Analyze test coverage using MCP tools
- - Identify files requiring tests
-- **Prompt Mode**:
- - Analyze codebase based on description
- - Identify relevant files and dependencies
-- Detect test framework and conventions
-- Generate context package JSON
+- **Session Mode**: Load source session summaries, analyze test coverage
+- **Prompt Mode**: Analyze codebase from description
+- Both: Detect test framework, generate context package
-**Parse Output**:
-- Extract: `contextPath` (pattern: `.workflow/[testSessionId]/.process/[test-]context-package.json`)
-
-**Validation**:
-- Context package created with coverage analysis
-- Test framework detected
-- Test conventions documented
-
-**TodoWrite**: Mark phase 2 completed, phase 3 in_progress
+**Output**: `contextPath` (pattern: `.workflow/[testSessionId]/.process/[test-]context-package.json`)
---
-#### Phase 3: Test Generation Analysis
-
-**Step 3.1: Execute** - Generate test requirements using Gemini
+### Phase 3: Test Generation Analysis
+**Execute**:
```javascript
-SlashCommand(command="/workflow:tools:test-concept-enhanced --session [testSessionId] --context [contextPath]")
+SlashCommand("/workflow:tools:test-concept-enhanced --session [testSessionId] --context [contextPath]")
```
-**Input**:
-- `testSessionId` from Phase 1
-- `contextPath` from Phase 2
-
**Expected Behavior**:
-- Use Gemini to analyze coverage gaps and implementation
-- Study existing test patterns and conventions
-- Generate **multi-layered test requirements** (L0: Static Analysis, L1: Unit, L2: Integration, L3: E2E)
-- Design test generation strategy with quality assurance criteria
-- Generate `TEST_ANALYSIS_RESULTS.md` with structured test layers
+- Use Gemini to analyze coverage gaps
+- Generate **multi-layered test requirements**:
+ - L0: Static Analysis (linting, type checking, anti-pattern detection)
+ - L1: Unit Tests (happy path, negative path, edge cases: null/undefined/empty)
+ - L2: Integration Tests (component interactions, failure scenarios: timeout/unavailable)
+ - L3: E2E Tests (user journeys, if applicable)
+- Generate `TEST_ANALYSIS_RESULTS.md`
-**Enhanced Test Requirements**:
-For each targeted file/function, Gemini MUST generate:
-1. **L0: Static Analysis Requirements**:
- - Linting rules to enforce (ESLint, Prettier)
- - Type checking requirements (TypeScript)
- - Anti-pattern detection rules
-2. **L1: Unit Test Requirements**:
- - Happy path scenarios (valid inputs → expected outputs)
- - Negative path scenarios (invalid inputs → error handling)
- - Edge cases (null, undefined, 0, empty strings/arrays)
-3. **L2: Integration Test Requirements**:
- - Successful component interactions
- - Failure handling scenarios (service unavailable, timeout)
-4. **L3: E2E Test Requirements** (if applicable):
- - Key user journeys from start to finish
+**Output**: `.workflow/[testSessionId]/.process/TEST_ANALYSIS_RESULTS.md`
-**Parse Output**:
-- Verify `.workflow/[testSessionId]/.process/TEST_ANALYSIS_RESULTS.md` created
-
-**Validation**:
-- TEST_ANALYSIS_RESULTS.md exists with complete sections:
- - Coverage Assessment
- - Test Framework & Conventions
- - **Multi-Layered Test Plan** (NEW):
- - L0: Static Analysis Plan
- - L1: Unit Test Plan
- - L2: Integration Test Plan
- - L3: E2E Test Plan (if applicable)
- - Test Requirements by File (with layer annotations)
- - Test Generation Strategy
- - Implementation Targets
- - Quality Assurance Criteria (NEW):
- - Minimum coverage thresholds
- - Required test types per function
- - Acceptance criteria for test quality
- - Success Criteria
-
-**TodoWrite**: Mark phase 3 completed, phase 4 in_progress
+**Validation** - TEST_ANALYSIS_RESULTS.md must include:
+- Coverage Assessment
+- Test Framework & Conventions
+- Multi-Layered Test Plan (L0-L3)
+- Test Requirements by File (with layer annotations)
+- Test Generation Strategy
+- Implementation Targets
+- Quality Assurance Criteria:
+ - Minimum coverage thresholds
+ - Required test types per function
+ - Acceptance criteria for test quality
+- Success Criteria
---
-#### Phase 4: Generate Test Tasks
-
-**Step 4.1: Execute** - Generate test task JSONs
+### Phase 4: Generate Test Tasks
+**Execute**:
```javascript
-SlashCommand(command="/workflow:tools:test-task-generate --session [testSessionId]")
+SlashCommand("/workflow:tools:test-task-generate --session [testSessionId]")
```
-**Input**:
-- `testSessionId` from Phase 1
-
-**Note**: CLI tool usage is determined semantically from user's task description.
-
**Expected Behavior**:
-- Parse TEST_ANALYSIS_RESULTS.md from Phase 3 (multi-layered test plan)
-- Generate **minimum 3 task JSON files** (expandable based on complexity):
- - **IMPL-001.json**: Test Understanding & Generation (`@code-developer`)
- - **IMPL-001.5-review.json**: Test Quality Gate (`@test-fix-agent`) ← **NEW**
- - **IMPL-002.json**: Test Execution & Fix Cycle (`@test-fix-agent`)
- - **IMPL-003+**: Additional tasks if needed for complex projects
-- Generate `IMPL_PLAN.md` with multi-layered test strategy
-- Generate `TODO_LIST.md` with task checklist
+- Parse TEST_ANALYSIS_RESULTS.md
+- Generate **minimum 4 task JSON files**:
+ - IMPL-001.json (Test Generation)
+ - IMPL-001.3-validation.json (Code Validation Gate)
+ - IMPL-001.5-review.json (Test Quality Gate)
+ - IMPL-002.json (Test Execution & Fix)
+- Generate IMPL_PLAN.md and TODO_LIST.md
-**Parse Output**:
-- Verify `.workflow/[testSessionId]/.task/IMPL-001.json` exists
-- Verify `.workflow/[testSessionId]/.task/IMPL-001.5-review.json` exists ← **NEW**
-- Verify `.workflow/[testSessionId]/.task/IMPL-002.json` exists
-- Verify additional `.task/IMPL-*.json` if applicable
+**Output Validation**:
+- Verify all `.task/IMPL-*.json` files exist
- Verify `IMPL_PLAN.md` and `TODO_LIST.md` created
-**TodoWrite**: Mark phase 4 completed, phase 5 in_progress
-
---
-#### Phase 5: Return Summary
+### Phase 5: Return Summary
**Return to User**:
```
@@ -328,47 +210,195 @@ Test Session: [testSessionId]
Tasks Created:
- IMPL-001: Test Understanding & Generation (@code-developer)
-- IMPL-001.5: Test Quality Gate - Static Analysis & Coverage (@test-fix-agent) ← NEW
+- IMPL-001.3: Code Validation Gate - AI Error Detection (@test-fix-agent)
+- IMPL-001.5: Test Quality Gate - Static Analysis & Coverage (@test-fix-agent)
- IMPL-002: Test Execution & Fix Cycle (@test-fix-agent)
-[- IMPL-003+: Additional tasks if applicable]
-Test Strategy: Multi-Layered (L0: Static, L1: Unit, L2: Integration, L3: E2E)
-Test Framework: [detected framework]
-Test Files to Generate: [count]
Quality Thresholds:
+- Code Validation: Zero compilation/import/variable errors
- Minimum Coverage: 80%
- Static Analysis: Zero critical issues
-Max Fix Iterations: 5
-Fix Mode: [Manual|Codex Automated]
+- Max Fix Iterations: 5
Review artifacts:
- Test plan: .workflow/[testSessionId]/IMPL_PLAN.md
- Task list: .workflow/[testSessionId]/TODO_LIST.md
+- Validation config: ~/.claude/workflows/test-quality-config.json
CRITICAL - Next Steps:
-1. Review IMPL_PLAN.md (now includes multi-layered test strategy)
+1. Review IMPL_PLAN.md
2. **MUST execute: /workflow:test-cycle-execute**
- - This command only generated task JSON files
- - Test execution and fix iterations happen in test-cycle-execute
- - Do NOT attempt to run tests or fixes in main workflow
-3. IMPL-001.5 will validate test quality before fix cycle begins
```
-**TodoWrite**: Mark phase 5 completed
-
-**BOUNDARY NOTE**:
-- Command completes here - only task JSON files generated
-- All test execution, failure detection, CLI analysis, fix generation happens in `/workflow:test-cycle-execute`
-- This command does NOT handle test failures or apply fixes
-
---
+## Task Specifications
+
+Generates minimum 4 tasks (expandable for complex projects):
+
+### IMPL-001: Test Understanding & Generation
+
+| Field | Value |
+|-------|-------|
+| **Agent** | `@code-developer` |
+| **Type** | `test-gen` |
+| **Depends On** | None |
+
+**Purpose**: Understand source implementation and generate test files following multi-layered test strategy
+
+**Execution Flow**:
+1. **Understand**: Load TEST_ANALYSIS_RESULTS.md, analyze requirements (L0-L3)
+2. **Generate**: Create test files (unit, integration, E2E as applicable)
+3. **Verify**: Check test completeness, meaningful assertions, no anti-patterns
+
+---
+
+### IMPL-001.3: Code Validation Gate
+
+| Field | Value |
+|-------|-------|
+| **Agent** | `@test-fix-agent` |
+| **Type** | `code-validation` |
+| **Depends On** | `["IMPL-001"]` |
+| **Config** | `~/.claude/workflows/test-quality-config.json` |
+
+**Purpose**: Validate AI-generated code for common errors before test execution
+
+**Validation Phases**:
+| Phase | Checks |
+|-------|--------|
+| L0.1 Compilation | `tsc --noEmit` - syntax errors, module resolution |
+| L0.2 Imports | Unresolved/hallucinated packages, circular deps, duplicates |
+| L0.3 Variables | Redeclaration, scope conflicts, undefined/unused vars |
+| L0.4 Types | Type mismatches, missing definitions, `any` abuse |
+| L0.5 AI-Specific | Placeholder code, mock in production, naming inconsistency |
+
+**Gate Decision**:
+| Decision | Condition | Action |
+|----------|-----------|--------|
+| **PASS** | critical=0, error≤3, warning≤10 | Proceed to IMPL-001.5 |
+| **SOFT_FAIL** | Fixable issues | Auto-fix and retry (max 2) |
+| **HARD_FAIL** | critical>0 OR max retries | Block with report |
+
+**Acceptance Criteria**:
+- Zero compilation errors
+- All imports resolvable
+- No variable redeclarations
+- No undefined variable usage
+
+**Output**: `.process/code-validation-report.md`, `.process/code-validation-report.json`
+
+---
+
+### IMPL-001.5: Test Quality Gate
+
+| Field | Value |
+|-------|-------|
+| **Agent** | `@test-fix-agent` |
+| **Type** | `test-quality-review` |
+| **Depends On** | `["IMPL-001", "IMPL-001.3"]` |
+| **Config** | `~/.claude/workflows/test-quality-config.json` |
+
+**Purpose**: Validate test quality before entering fix cycle
+
+**Execution Flow**:
+1. **Static Analysis**: Lint test files, check anti-patterns (empty tests, missing assertions)
+2. **Coverage Analysis**: Calculate coverage percentage, identify gaps
+3. **Quality Metrics**: Verify thresholds, negative test coverage
+4. **Gate Decision**: PASS (proceed) or FAIL (loop back to IMPL-001)
+
+**Acceptance Criteria**:
+- Coverage ≥ 80%
+- Zero critical anti-patterns
+- All targeted functions have unit tests
+- Each public API has error handling test
+
+**Failure Handling**:
+If quality gate fails:
+1. Generate detailed feedback report (`.process/test-quality-report.md`)
+2. Update IMPL-001 task with specific improvement requirements
+3. Trigger IMPL-001 re-execution with enhanced context
+4. Maximum 2 quality gate retries before escalating to user
+
+**Output**: `.process/test-quality-report.md`
+
+---
+
+### IMPL-002: Test Execution & Fix Cycle
+
+| Field | Value |
+|-------|-------|
+| **Agent** | `@test-fix-agent` |
+| **Type** | `test-fix` |
+| **Depends On** | `["IMPL-001", "IMPL-001.3", "IMPL-001.5"]` |
+
+**Purpose**: Execute tests and trigger orchestrator-managed fix cycles
+
+**Note**: The agent executes tests and reports results. The `test-cycle-execute` orchestrator manages all fix iterations.
+
+**Cycle Pattern** (orchestrator-managed):
+```
+test → gemini_diagnose → fix (agent or CLI) → retest
+```
+
+**Tools Configuration** (orchestrator-controlled):
+- Gemini for analysis with bug-fix template → surgical fix suggestions
+- Agent fix application (default) OR CLI if `command` field present in implementation_approach
+
+**Exit Conditions**:
+- Success: All tests pass
+- Failure: Max iterations reached (5)
+
+---
+
+### IMPL-003+: Additional Tasks (Optional)
+
+**Scenarios**:
+- Large projects requiring per-module test generation
+- Separate integration vs unit test tasks
+- Specialized test types (performance, security)
+
+---
+
+## Output Artifacts
+
+### Directory Structure
+
+```
+.workflow/active/WFS-test-[session]/
+├── workflow-session.json # Session metadata
+├── IMPL_PLAN.md # Test generation and execution strategy
+├── TODO_LIST.md # Task checklist
+├── .task/
+│ ├── IMPL-001.json # Test understanding & generation
+│ ├── IMPL-001.3-validation.json # Code validation gate
+│ ├── IMPL-001.5-review.json # Test quality gate
+│ ├── IMPL-002.json # Test execution & fix cycle
+│ └── IMPL-*.json # Additional tasks (if applicable)
+└── .process/
+ ├── [test-]context-package.json # Context and coverage analysis
+ ├── TEST_ANALYSIS_RESULTS.md # Test requirements and strategy
+ ├── code-validation-report.md # Code validation findings
+ ├── code-validation-report.json # Machine-readable findings
+ └── test-quality-report.md # Test quality gate findings
+```
+
+### Session Metadata
+
+**File**: `workflow-session.json`
+
+| Mode | Fields |
+|------|--------|
+| **Session** | `type: "test"`, `source_session_id: "[sourceId]"` |
+| **Prompt** | `type: "test"` (no source_session_id) |
+
+---
+
+## Orchestration Patterns
+
### TodoWrite Pattern
-**Core Concept**: Dynamic task attachment and collapse for test-fix-gen workflow with dual-mode support (Session Mode and Prompt Mode).
-
-#### Initial TodoWrite Structure
-
+**Initial Structure**:
```json
[
{"content": "Phase 1: Create Test Session", "status": "in_progress", "activeForm": "Creating test session"},
@@ -379,269 +409,33 @@ CRITICAL - Next Steps:
]
```
-#### Key Principles
+### Task Attachment Model
-1. **Task Attachment** (when SlashCommand executed):
- - Sub-command's internal tasks are **attached** to orchestrator's TodoWrite
- - Example - Phase 2 with sub-tasks:
- ```json
- [
- {"content": "Phase 1: Create Test Session", "status": "completed", "activeForm": "Creating test session"},
- {"content": "Phase 2: Gather Test Context", "status": "in_progress", "activeForm": "Gathering test context"},
- {"content": " → Load context and analyze coverage", "status": "in_progress", "activeForm": "Loading context"},
- {"content": " → Detect test framework and conventions", "status": "pending", "activeForm": "Detecting framework"},
- {"content": " → Generate context package", "status": "pending", "activeForm": "Generating context"},
- {"content": "Phase 3: Test Generation Analysis", "status": "pending", "activeForm": "Analyzing test generation"},
- {"content": "Phase 4: Generate Test Tasks", "status": "pending", "activeForm": "Generating test tasks"},
- {"content": "Phase 5: Return Summary", "status": "pending", "activeForm": "Completing"}
- ]
- ```
+SlashCommand execution follows **attach → execute → collapse** pattern:
-2. **Task Collapse** (after sub-tasks complete):
- - Remove detailed sub-tasks from TodoWrite
- - **Collapse** to high-level phase summary
- - Example - Phase 2 completed:
- ```json
- [
- {"content": "Phase 1: Create Test Session", "status": "completed", "activeForm": "Creating test session"},
- {"content": "Phase 2: Gather Test Context", "status": "completed", "activeForm": "Gathering test context"},
- {"content": "Phase 3: Test Generation Analysis", "status": "in_progress", "activeForm": "Analyzing test generation"},
- {"content": "Phase 4: Generate Test Tasks", "status": "pending", "activeForm": "Generating test tasks"},
- {"content": "Phase 5: Return Summary", "status": "pending", "activeForm": "Completing"}
- ]
- ```
+1. **Attach**: Sub-command's tasks are attached to orchestrator's TodoWrite
+2. **Execute**: Orchestrator executes attached tasks sequentially
+3. **Collapse**: After completion, sub-tasks collapse to phase summary
-3. **Continuous Execution**:
- - After collapse, automatically proceed to next pending phase
- - No user intervention required between phases
- - TodoWrite dynamically reflects current execution state
-
-**Lifecycle Summary**: Initial pending tasks → Phase executed (tasks ATTACHED with mode-specific context gathering) → Sub-tasks executed sequentially → Phase completed (tasks COLLAPSED to summary) → Next phase begins → Repeat until all phases complete.
-
-#### Test-Fix-Gen Specific Features
-
-- **Dual-Mode Support**: Automatic mode detection based on input pattern
- - **Session Mode**: Input pattern `WFS-*` → uses `test-context-gather` for cross-session context
- - **Prompt Mode**: Text or file path → uses `context-gather` for direct codebase analysis
-- **Phase 2**: Mode-specific context gathering (session summaries vs codebase analysis)
-- **Phase 3**: Multi-layered test requirements analysis (L0: Static, L1: Unit, L2: Integration, L3: E2E)
-- **Phase 4**: Multi-task generation with quality gate (IMPL-001, IMPL-001.5-review, IMPL-002)
-- **Fix Mode Configuration**: CLI tool usage determined semantically from user's task description
-
-
----
-
-## Task Specifications
-
-Generates minimum 3 tasks (expandable for complex projects):
-
-### IMPL-001: Test Understanding & Generation
-
-**Agent**: `@code-developer`
-
-**Purpose**: Understand source implementation and generate test files following multi-layered test strategy
-
-**Task Configuration**:
-- Task ID: `IMPL-001`
-- `meta.type: "test-gen"`
-- `meta.agent: "@code-developer"`
-- `context.requirements`: Understand source implementation and generate tests across all layers (L0-L3)
-- `flow_control.target_files`: Test files to create from TEST_ANALYSIS_RESULTS.md section 5
-
-**Execution Flow**:
-1. **Understand Phase**:
- - Load TEST_ANALYSIS_RESULTS.md and test context
- - Understand source code implementation patterns
- - Analyze multi-layered test requirements (L0: Static, L1: Unit, L2: Integration, L3: E2E)
- - Identify test scenarios, edge cases, and error paths
-2. **Generation Phase**:
- - Generate L1 unit test files following existing patterns
- - Generate L2 integration test files (if applicable)
- - Generate L3 E2E test files (if applicable)
- - Ensure test coverage aligns with multi-layered requirements
- - Include both positive and negative test cases
-3. **Verification Phase**:
- - Verify test completeness and correctness
- - Ensure each test has meaningful assertions
- - Check for test anti-patterns (tests without assertions, overly broad mocks)
-
-### IMPL-001.5: Test Quality Gate ← **NEW**
-
-**Agent**: `@test-fix-agent`
-
-**Purpose**: Validate test quality before entering fix cycle - prevent "hollow tests" from becoming the source of truth
-
-**Task Configuration**:
-- Task ID: `IMPL-001.5-review`
-- `meta.type: "test-quality-review"`
-- `meta.agent: "@test-fix-agent"`
-- `context.depends_on: ["IMPL-001"]`
-- `context.requirements`: Validate generated tests meet quality standards
-- `context.quality_config`: Load from `.claude/workflows/test-quality-config.json`
-
-**Execution Flow**:
-1. **L0: Static Analysis**:
- - Run linting on test files (ESLint, Prettier)
- - Check for test anti-patterns:
- - Tests without assertions (`expect()` missing)
- - Empty test bodies (`it('should...', () => {})`)
- - Disabled tests without justification (`it.skip`, `xit`)
- - Verify TypeScript type safety (if applicable)
-2. **Coverage Analysis**:
- - Run coverage analysis on generated tests
- - Calculate coverage percentage for target source files
- - Identify uncovered branches and edge cases
-3. **Test Quality Metrics**:
- - Verify minimum coverage threshold met (default: 80%)
- - Verify all critical functions have negative test cases
- - Verify integration tests cover key component interactions
-4. **Quality Gate Decision**:
- - **PASS**: Coverage ≥ 80%, zero critical anti-patterns → Proceed to IMPL-002
- - **FAIL**: Coverage < 80% OR critical anti-patterns found → Loop back to IMPL-001 with feedback
-
-**Acceptance Criteria**:
-- Static analysis: Zero critical issues
-- Test coverage: ≥ 80% for target files
-- Test completeness: All targeted functions have unit tests
-- Negative test coverage: Each public API has at least one error handling test
-- Integration coverage: Key component interactions have integration tests (if applicable)
-
-**Failure Handling**:
-If quality gate fails:
-1. Generate detailed feedback report (`.process/test-quality-report.md`)
-2. Update IMPL-001 task with specific improvement requirements
-3. Trigger IMPL-001 re-execution with enhanced context
-4. Maximum 2 quality gate retries before escalating to user
-
-### IMPL-002: Test Execution & Fix Cycle
-
-**Agent**: `@test-fix-agent`
-
-**Purpose**: Execute initial tests and trigger orchestrator-managed fix cycles
-
-**Note**: This task executes tests and reports results. The test-cycle-execute orchestrator manages all fix iterations, CLI analysis, and fix task generation.
-
-**Task Configuration**:
-- Task ID: `IMPL-002`
-- `meta.type: "test-fix"`
-- `meta.agent: "@test-fix-agent"`
-- `context.depends_on: ["IMPL-001"]`
-- `context.requirements`: Execute and fix tests
-
-**Test-Fix Cycle Specification**:
-**Note**: This specification describes what test-cycle-execute orchestrator will do. The agent only executes single tasks.
-- **Cycle Pattern** (orchestrator-managed): test → gemini_diagnose → fix (agent or CLI) → retest
-- **Tools Configuration** (orchestrator-controlled):
- - Gemini for analysis with bug-fix template → surgical fix suggestions
- - Agent fix application (default) OR CLI if `command` field present in implementation_approach
-- **Exit Conditions** (orchestrator-enforced):
- - Success: All tests pass
- - Failure: Max iterations reached (5)
-
-**Execution Flow**:
-1. **Phase 1**: Initial test execution
-2. **Phase 2**: Iterative Gemini diagnosis + manual/Codex fixes
-3. **Phase 3**: Final validation and certification
-
-### IMPL-003+: Additional Tasks (Optional)
-
-**Scenarios for Multiple Tasks**:
-- Large projects requiring per-module test generation
-- Separate integration vs unit test tasks
-- Specialized test types (performance, security, etc.)
-
-**Agent**: `@code-developer` or specialized agents based on requirements
-
----
-
-## Artifacts & Output
-
-### Output Files Structure
-
-Created in `.workflow/active/WFS-test-[session]/`:
-
-```
-WFS-test-[session]/
-├── workflow-session.json # Session metadata
-├── IMPL_PLAN.md # Test generation and execution strategy
-├── TODO_LIST.md # Task checklist
-├── .task/
-│ ├── IMPL-001.json # Test understanding & generation
-│ ├── IMPL-002.json # Test execution & fix cycle
-│ └── IMPL-*.json # Additional tasks (if applicable)
-└── .process/
- ├── [test-]context-package.json # Context and coverage analysis
- └── TEST_ANALYSIS_RESULTS.md # Test requirements and strategy
+**Example - Phase 2 Expanded**:
+```json
+[
+ {"content": "Phase 1: Create Test Session", "status": "completed"},
+ {"content": "Phase 2: Gather Test Context", "status": "in_progress"},
+ {"content": " → Load context and analyze coverage", "status": "in_progress"},
+ {"content": " → Detect test framework and conventions", "status": "pending"},
+ {"content": " → Generate context package", "status": "pending"},
+ {"content": "Phase 3: Test Generation Analysis", "status": "pending"},
+ ...
+]
```
-### Session Metadata
+### Auto-Continue Mechanism
-**File**: `workflow-session.json`
-
-**Session Mode** includes:
-- `type: "test"` (set by session:start --type test)
-- `source_session_id: "[sourceSessionId]"` (enables automatic cross-session context)
-
-**Prompt Mode** includes:
-- `type: "test"` (set by session:start --type test)
-- No `source_session_id` field
-
-### Execution Flow Diagram
-
-```
-Test-Fix-Gen Workflow Orchestrator (Dual-Mode Support)
-│
-├─ Phase 1: Create Test Session
-│ ├─ Session Mode: /workflow:session:start --new (with source_session_id)
-│ └─ Prompt Mode: /workflow:session:start --new (without source_session_id)
-│ └─ Returns: testSessionId (WFS-test-[slug])
-│
-├─ Phase 2: Gather Context ← ATTACHED (3 tasks)
-│ ├─ Session Mode: /workflow:tools:test-context-gather
-│ │ └─ Load source session summaries + analyze coverage
-│ └─ Prompt Mode: /workflow:tools:context-gather
-│ └─ Analyze codebase from description
-│ ├─ Phase 2.1: Load context and analyze coverage
-│ ├─ Phase 2.2: Detect test framework and conventions
-│ └─ Phase 2.3: Generate context package
-│ └─ Returns: [test-]context-package.json ← COLLAPSED
-│
-├─ Phase 3: Test Generation Analysis ← ATTACHED (3 tasks)
-│ └─ /workflow:tools:test-concept-enhanced
-│ ├─ Phase 3.1: Analyze coverage gaps with Gemini
-│ ├─ Phase 3.2: Study existing test patterns
-│ └─ Phase 3.3: Generate test generation strategy
-│ └─ Returns: TEST_ANALYSIS_RESULTS.md ← COLLAPSED
-│
-├─ Phase 4: Generate Test Tasks ← ATTACHED (3 tasks)
-│ └─ /workflow:tools:test-task-generate
-│ ├─ Phase 4.1: Parse TEST_ANALYSIS_RESULTS.md
-│ ├─ Phase 4.2: Generate task JSONs (IMPL-001, IMPL-002)
-│ └─ Phase 4.3: Generate IMPL_PLAN.md and TODO_LIST.md
-│ └─ Returns: Task JSONs and plans ← COLLAPSED
-│
-└─ Phase 5: Return Summary
- └─ Command ends, control returns to user
-
-Artifacts Created:
-├── .workflow/active/WFS-test-[session]/
-│ ├── workflow-session.json
-│ ├── IMPL_PLAN.md
-│ ├── TODO_LIST.md
-│ ├── .task/
-│ │ ├── IMPL-001.json (test understanding & generation)
-│ │ ├── IMPL-002.json (test execution & fix cycle)
-│ │ └── IMPL-003.json (optional: test review & certification)
-│ └── .process/
-│ ├── [test-]context-package.json
-│ └── TEST_ANALYSIS_RESULTS.md
-
-Key Points:
-• ← ATTACHED: SlashCommand attaches sub-tasks to orchestrator TodoWrite
-• ← COLLAPSED: Sub-tasks executed and collapsed to phase summary
-• Dual-Mode: Session Mode and Prompt Mode share same attachment pattern
-• Command Boundary: Execution delegated to /workflow:test-cycle-execute
-```
+- TodoList tracks current phase status
+- When phase completes, automatically execute next pending phase
+- All phases run autonomously without user interaction
+- **⚠️ Do not stop until all phases complete**
---
@@ -651,49 +445,50 @@ Key Points:
| Phase | Error Condition | Action |
|-------|----------------|--------|
-| 1 | Source session not found (session mode) | Return error with source session ID |
-| 1 | No completed IMPL tasks (session mode) | Return error, source incomplete |
+| 1 | Source session not found | Return error with session ID |
+| 1 | No completed IMPL tasks | Return error, source incomplete |
| 2 | Context gathering failed | Return error, check source artifacts |
| 3 | Gemini analysis failed | Return error, check context package |
-| 4 | Task generation failed | Retry once, then return error with details |
+| 4 | Task generation failed | Retry once, then return error |
### Best Practices
-1. **Before Running**:
- - Ensure implementation is complete (session mode: check summaries exist)
- - Commit all implementation changes
- - Review source code quality
+**Before Running**:
+- Ensure implementation is complete (session mode: check summaries exist)
+- Commit all implementation changes
-2. **After Running**:
- - Review generated `IMPL_PLAN.md` before execution
- - Check `TEST_ANALYSIS_RESULTS.md` for completeness
- - Verify task dependencies in `TODO_LIST.md`
+**After Running**:
+- Review `IMPL_PLAN.md` before execution
+- Check `TEST_ANALYSIS_RESULTS.md` for completeness
+- Verify task dependencies in `TODO_LIST.md`
-3. **During Execution**:
- - Monitor iteration logs in `.process/fix-iteration-*`
- - Track progress with `/workflow:status`
- - Review Gemini diagnostic outputs
+**During Execution** (in test-cycle-execute):
+- Monitor iteration logs in `.process/fix-iteration-*`
+- Track progress with `/workflow:status`
+- Review Gemini diagnostic outputs
-4. **Mode Selection**:
- - Use **Session Mode** for completed workflow validation
- - Use **Prompt Mode** for ad-hoc test generation
- - Include "use Codex" in description for autonomous fix application
+**Mode Selection**:
+- **Session Mode**: For completed workflow validation
+- **Prompt Mode**: For ad-hoc test generation
+- Include "use Codex" in description for autonomous fix application
-## Related Commands
+### Related Commands
-**Prerequisite Commands**:
-- `/workflow:plan` or `/workflow:execute` - Complete implementation session (for Session Mode)
-- None for Prompt Mode (ad-hoc test generation)
+**Prerequisites**:
+- `/workflow:plan` or `/workflow:execute` - Complete implementation (Session Mode)
+- None for Prompt Mode
-**Called by This Command** (5 phases):
-- `/workflow:session:start` - Phase 1: Create independent test workflow session
-- `/workflow:tools:test-context-gather` - Phase 2 (Session Mode): Gather source session context
-- `/workflow:tools:context-gather` - Phase 2 (Prompt Mode): Analyze codebase directly
-- `/workflow:tools:test-concept-enhanced` - Phase 3: Generate test requirements using Gemini
-- `/workflow:tools:test-task-generate` - Phase 4: Generate test task JSONs (CLI tool usage determined semantically)
+**Called by This Command**:
+- `/workflow:session:start` - Phase 1
+- `/workflow:tools:test-context-gather` - Phase 2 (Session Mode)
+- `/workflow:tools:context-gather` - Phase 2 (Prompt Mode)
+- `/workflow:tools:test-concept-enhanced` - Phase 3
+- `/workflow:tools:test-task-generate` - Phase 4
+
+**Validation Commands** (invoked during test-cycle-execute):
+- `/workflow:tools:code-validation-gate` - IMPL-001.3
**Follow-up Commands**:
-- `/workflow:status` - Review generated test tasks
-- `/workflow:test-cycle-execute` - Execute test generation and iterative fix cycles
-- `/workflow:execute` - Standard execution of generated test tasks
-
+- `/workflow:status` - Review generated tasks
+- `/workflow:test-cycle-execute` - Execute test workflow
+- `/workflow:execute` - Standard task execution
diff --git a/.claude/commands/workflow/tools/code-validation-gate.md b/.claude/commands/workflow/tools/code-validation-gate.md
new file mode 100644
index 00000000..7a7a21a7
--- /dev/null
+++ b/.claude/commands/workflow/tools/code-validation-gate.md
@@ -0,0 +1,391 @@
+---
+name: code-validation-gate
+description: Validate AI-generated code for common errors (imports, variables, types) before test execution
+argument-hint: "--session WFS-test-session-id [--fix] [--strict]"
+examples:
+ - /workflow:tools:code-validation-gate --session WFS-test-auth
+ - /workflow:tools:code-validation-gate --session WFS-test-auth --fix
+ - /workflow:tools:code-validation-gate --session WFS-test-auth --strict
+---
+
+# Code Validation Gate Command
+
+## Overview
+
+Pre-test validation gate that checks AI-generated code for common errors before test execution. This prevents wasted test cycles on code with fundamental issues like import errors, variable conflicts, and type mismatches.
+
+## Core Philosophy
+
+- **Fail Fast**: Catch fundamental errors before expensive test execution
+- **AI-Aware**: Specifically targets common AI code generation mistakes
+- **Auto-Remediation**: Attempt safe fixes before failing
+- **Clear Feedback**: Provide actionable fix suggestions for manual intervention
+
+## Target Error Categories
+
+### L0.1: Compilation Errors
+- TypeScript compilation failures
+- Syntax errors
+- Module resolution failures
+
+### L0.2: Import Errors
+- Unresolved module imports (hallucinated packages)
+- Circular dependencies
+- Duplicate imports
+- Unused imports
+
+### L0.3: Variable Errors
+- Variable redeclaration
+- Scope conflicts (shadowing)
+- Undefined variable usage
+- Unused variables
+
+### L0.4: Type Errors (TypeScript)
+- Type mismatches
+- Missing type definitions
+- Excessive `any` usage
+- Implicit `any` types
+
+### L0.5: AI-Specific Patterns
+- Placeholder code (`// TODO: implement`)
+- Hallucinated package imports
+- Mock code in production files
+- Inconsistent naming patterns
+
+## Execution Process
+
+```
+Input Parsing:
+ ├─ Parse flags: --session (required), --fix, --strict
+ └─ Load test-quality-config.json
+
+Phase 1: Context Loading
+ ├─ Load session metadata
+ ├─ Identify target files (from IMPL-001 output or context-package)
+ └─ Detect project configuration (tsconfig, eslint, etc.)
+
+Phase 2: Validation Execution
+ ├─ L0.1: Run TypeScript compilation check
+ ├─ L0.2: Run import validation
+ ├─ L0.3: Run variable validation
+ ├─ L0.4: Run type validation
+ └─ L0.5: Run AI-specific checks
+
+Phase 3: Result Analysis
+ ├─ Aggregate all findings by severity
+ ├─ Calculate pass/fail status
+ └─ Generate fix suggestions
+
+Phase 4: Auto-Fix (if --fix enabled)
+ ├─ Apply safe auto-fixes (imports, formatting)
+ ├─ Re-run validation
+ └─ Report remaining issues
+
+Phase 5: Gate Decision
+ ├─ PASS: Proceed to IMPL-001.5
+ ├─ SOFT_FAIL: Auto-fix applied, needs re-validation
+ └─ HARD_FAIL: Block with detailed report
+```
+
+## Execution Lifecycle
+
+### Phase 1: Context Loading
+
+**Load session and identify validation targets.**
+
+```javascript
+// Load session metadata
+Read(".workflow/active/{session_id}/workflow-session.json")
+
+// Load context package for target files
+Read(".workflow/active/{session_id}/.process/context-package.json")
+// OR
+Read(".workflow/active/{session_id}/.process/test-context-package.json")
+
+// Identify files to validate:
+// 1. Source files from context.implementation_files
+// 2. Test files from IMPL-001 output (if exists)
+// 3. All modified files since session start
+```
+
+**Target File Discovery**:
+- Source files: `context.focus_paths` from context-package
+- Generated tests: `.workflow/active/{session_id}/.task/IMPL-001-output/`
+- All TypeScript/JavaScript in target directories
+
+### Phase 2: Validation Execution
+
+**Execute validation checks in order of dependency.**
+
+#### L0.1: TypeScript Compilation
+
+```bash
+# Primary check - catches most fundamental errors
+npx tsc --noEmit --skipLibCheck --project tsconfig.json 2>&1
+
+# Parse output for errors
+# Critical: Any compilation error blocks further validation
+```
+
+**Error Patterns**:
+```
+error TS2307: Cannot find module 'xxx'
+error TS2451: Cannot redeclare block-scoped variable 'xxx'
+error TS2322: Type 'xxx' is not assignable to type 'yyy'
+```
+
+#### L0.2: Import Validation
+
+```bash
+# Check for circular dependencies
+npx madge --circular --extensions ts,tsx,js,jsx {target_dirs}
+
+# ESLint import rules
+npx eslint --rule 'import/no-duplicates: error' --rule 'import/no-unresolved: error' {files}
+```
+
+**Hallucinated Package Check**:
+```javascript
+// Extract all imports from files
+// Verify each package exists in package.json or node_modules
+// Flag any unresolvable imports as "hallucinated"
+```
+
+#### L0.3: Variable Validation
+
+```bash
+# ESLint variable rules
+npx eslint --rule 'no-shadow: error' --rule 'no-undef: error' --rule 'no-redeclare: error' {files}
+```
+
+#### L0.4: Type Validation
+
+```bash
+# TypeScript strict checks
+npx tsc --noEmit --strict {files}
+
+# Check for any abuse
+npx eslint --rule '@typescript-eslint/no-explicit-any: warn' {files}
+```
+
+#### L0.5: AI-Specific Checks
+
+```bash
+# Check for placeholder code
+grep -rn "// TODO: implement\|// Add your code here\|throw new Error.*Not implemented" {files}
+
+# Check for mock code in production files
+grep -rn "jest\.mock\|sinon\.\|vi\.mock" {source_files_only}
+```
+
+### Phase 3: Result Analysis
+
+**Aggregate and categorize findings.**
+
+```javascript
+const findings = {
+ critical: [], // Blocks all progress
+ error: [], // Blocks with threshold
+ warning: [] // Advisory only
+};
+
+// Apply thresholds from config
+const config = loadConfig("test-quality-config.json");
+const thresholds = config.code_validation.severity_thresholds;
+
+// Gate decision
+if (findings.critical.length > thresholds.critical) {
+ decision = "HARD_FAIL";
+} else if (findings.error.length > thresholds.error) {
+ decision = "SOFT_FAIL"; // Try auto-fix
+} else {
+ decision = "PASS";
+}
+```
+
+### Phase 4: Auto-Fix (Optional)
+
+**Apply safe automatic fixes when --fix flag provided.**
+
+```bash
+# Safe fixes only
+npx eslint --fix --rule 'import/no-duplicates: error' --rule 'unused-imports/no-unused-imports: error' {files}
+
+# Re-run validation after fixes
+# Report what was fixed vs what remains
+```
+
+**Safe Fix Categories**:
+- Remove unused imports
+- Remove duplicate imports
+- Fix import ordering
+- Remove unused variables (with caution)
+- Formatting fixes
+
+**Unsafe (Manual Only)**:
+- Missing imports (need to determine correct package)
+- Type errors (need to understand intent)
+- Variable shadowing (need to understand scope intent)
+
+### Phase 5: Gate Decision
+
+**Determine next action based on results.**
+
+| Decision | Condition | Action |
+|----------|-----------|--------|
+| **PASS** | critical=0, error<=3, warning<=10 | Proceed to IMPL-001.5 |
+| **SOFT_FAIL** | critical=0, error>3 OR fixable issues | Auto-fix and retry (max 2) |
+| **HARD_FAIL** | critical>0 OR max retries exceeded | Block with report |
+
+## Output Artifacts
+
+### Validation Report
+
+**File**: `.workflow/active/{session_id}/.process/code-validation-report.md`
+
+```markdown
+# Code Validation Report
+
+**Session**: {session_id}
+**Timestamp**: {timestamp}
+**Status**: PASS | SOFT_FAIL | HARD_FAIL
+
+## Summary
+- Files Validated: {count}
+- Critical Issues: {count}
+- Errors: {count}
+- Warnings: {count}
+
+## Critical Issues (Must Fix)
+### Import Errors
+- `src/auth/service.ts:5` - Cannot find module 'non-existent-package'
+ - **Suggestion**: Check if package exists, may be hallucinated by AI
+
+### Variable Conflicts
+- `src/utils/helper.ts:12` - Cannot redeclare block-scoped variable 'config'
+ - **Suggestion**: Rename one of the variables or merge declarations
+
+## Errors (Should Fix)
+...
+
+## Warnings (Consider Fixing)
+...
+
+## Auto-Fix Applied
+- Removed 3 unused imports in `src/auth/service.ts`
+- Fixed import ordering in `src/utils/index.ts`
+
+## Remaining Issues Requiring Manual Fix
+...
+
+## Next Steps
+- [ ] Fix critical issues before proceeding
+- [ ] Review error suggestions
+- [ ] Re-run validation: `/workflow:tools:code-validation-gate --session {session_id}`
+```
+
+### JSON Report (Machine-Readable)
+
+**File**: `.workflow/active/{session_id}/.process/code-validation-report.json`
+
+```json
+{
+ "session_id": "WFS-test-xxx",
+ "timestamp": "2025-01-30T10:00:00Z",
+ "status": "HARD_FAIL",
+ "summary": {
+ "files_validated": 15,
+ "critical": 2,
+ "error": 5,
+ "warning": 8
+ },
+ "findings": {
+ "critical": [
+ {
+ "category": "import",
+ "file": "src/auth/service.ts",
+ "line": 5,
+ "message": "Cannot find module 'non-existent-package'",
+ "suggestion": "Check if package exists in package.json",
+ "auto_fixable": false
+ }
+ ],
+ "error": [...],
+ "warning": [...]
+ },
+ "auto_fixes_applied": [...],
+ "gate_decision": "HARD_FAIL",
+ "retry_count": 0,
+ "max_retries": 2
+}
+```
+
+## Command Options
+
+| Option | Description | Default |
+|--------|-------------|---------|
+| `--session` | Test session ID (required) | - |
+| `--fix` | Enable auto-fix for safe issues | false |
+| `--strict` | Use strict thresholds (0 errors allowed) | false |
+| `--files` | Specific files to validate (comma-separated) | All target files |
+| `--skip-types` | Skip TypeScript type checks | false |
+
+## Integration
+
+### Command Chain
+
+- **Called By**: `/workflow:test-fix-gen` (after IMPL-001)
+- **Requires**: IMPL-001 output OR context-package.json
+- **Followed By**: IMPL-001.5 (Test Quality Gate) on PASS
+
+### Task JSON Integration
+
+When used in test-fix workflow, generates task:
+
+```json
+{
+ "id": "IMPL-001.3-validation",
+ "meta": {
+ "type": "code-validation",
+ "agent": "@test-fix-agent"
+ },
+ "context": {
+ "depends_on": ["IMPL-001"],
+ "requirements": "Validate generated code for AI common errors"
+ },
+ "flow_control": {
+ "validation_config": "~/.claude/workflows/test-quality-config.json",
+ "max_retries": 2,
+ "auto_fix_enabled": true
+ },
+ "acceptance_criteria": [
+ "Zero critical issues",
+ "Maximum 3 error issues",
+ "All imports resolvable",
+ "No variable redeclarations"
+ ]
+}
+```
+
+## Error Handling
+
+| Error | Resolution |
+|-------|------------|
+| tsconfig.json not found | Use default compiler options |
+| ESLint not installed | Skip ESLint checks, use tsc only |
+| madge not installed | Skip circular dependency check |
+| No files to validate | Return PASS (nothing to check) |
+
+## Best Practices
+
+1. **Run Early**: Execute validation immediately after code generation
+2. **Use --fix First**: Let auto-fix resolve trivial issues
+3. **Review Suggestions**: AI fix suggestions may need human judgment
+4. **Don't Skip Critical**: Never proceed with critical errors
+5. **Track Patterns**: Common failures indicate prompt improvement opportunities
+
+## Related Commands
+
+- `/workflow:test-fix-gen` - Parent workflow that invokes this command
+- `/workflow:tools:test-quality-gate` - Next phase (IMPL-001.5) for test quality
+- `/workflow:test-cycle-execute` - Execute tests after validation passes
diff --git a/.claude/commands/workflow/tools/test-task-generate.md b/.claude/commands/workflow/tools/test-task-generate.md
index 15d54409..ddf4c8e0 100644
--- a/.claude/commands/workflow/tools/test-task-generate.md
+++ b/.claude/commands/workflow/tools/test-task-generate.md
@@ -143,7 +143,7 @@ Determine CLI tool usage per-step based on user's task description:
(Detailed specifications in your agent definition)
### Task Structure Requirements
-- Minimum 2 tasks: IMPL-001 (test generation) + IMPL-002 (test execution & fix)
+- Minimum 4 tasks: IMPL-001 (test generation) + IMPL-001.3 (code validation) + IMPL-001.5 (test quality) + IMPL-002 (test execution & fix)
- Expandable for complex projects: Add IMPL-003+ (per-module, integration, E2E tests)
Task Configuration:
@@ -154,9 +154,29 @@ Task Configuration:
- flow_control: Test generation strategy from TEST_ANALYSIS_RESULTS.md
- CLI execution: Add `command` field when user requests (determined semantically)
+ IMPL-001.3 (Code Validation Gate) ← NEW:
+ - meta.type: "code-validation"
+ - meta.agent: "@test-fix-agent"
+ - context.depends_on: ["IMPL-001"]
+ - context.validation_config: "~/.claude/workflows/test-quality-config.json"
+ - flow_control.validation_phases: ["compilation", "imports", "variables", "types", "ai_specific"]
+ - flow_control.auto_fix_enabled: true
+ - flow_control.max_retries: 2
+ - flow_control.severity_thresholds: { critical: 0, error: 3, warning: 10 }
+ - acceptance_criteria: Zero compilation errors, all imports resolvable, no variable redeclarations
+
+ IMPL-001.5 (Test Quality Gate):
+ - meta.type: "test-quality-review"
+ - meta.agent: "@test-fix-agent"
+ - context.depends_on: ["IMPL-001", "IMPL-001.3"]
+ - context.quality_config: "~/.claude/workflows/test-quality-config.json"
+ - flow_control: Static analysis, coverage analysis, anti-pattern detection
+ - acceptance_criteria: Coverage ≥ 80%, zero critical anti-patterns
+
IMPL-002+ (Test Execution & Fix):
- meta.type: "test-fix"
- meta.agent: "@test-fix-agent"
+ - context.depends_on: ["IMPL-001", "IMPL-001.3", "IMPL-001.5"]
- flow_control: Test-fix cycle with iteration limits and diagnosis configuration
- CLI execution: Add `command` field when user requests (determined semantically)
@@ -190,10 +210,17 @@ PRIMARY requirements source - extract and map to task JSONs:
- Implementation targets → context.files_to_test (absolute paths)
## EXPECTED DELIVERABLES
-1. Test Task JSON Files (.task/IMPL-*.json)
+1. Test Task JSON Files (.task/IMPL-*.json) - Minimum 4 required:
+ - IMPL-001.json: Test generation task
+ - IMPL-001.3-validation.json: Code validation gate (AI error detection) ← NEW
+ - IMPL-001.5-review.json: Test quality gate
+ - IMPL-002.json: Test execution & fix cycle
+
+ Each task includes:
- 6-field schema with quantified requirements from TEST_ANALYSIS_RESULTS.md
- Test-specific metadata: type, agent, test_framework, coverage_target
- flow_control includes: reusable_test_tools, test_commands (from project config)
+ - Validation config reference for IMPL-001.3: ~/.claude/workflows/test-quality-config.json
- CLI execution via `command` field when user requests (determined semantically)
- Artifact references from test-context-package.json
- Absolute paths in context.files_to_test
@@ -211,7 +238,7 @@ PRIMARY requirements source - extract and map to task JSONs:
## QUALITY STANDARDS
Hard Constraints:
- - Task count: minimum 2, maximum 18
+ - Task count: minimum 4, maximum 18 (IMPL-001, IMPL-001.3, IMPL-001.5, IMPL-002 required)
- All requirements quantified from TEST_ANALYSIS_RESULTS.md
- Test framework matches existing project framework
- flow_control includes reusable_test_tools and test_commands from project
@@ -249,7 +276,11 @@ CLI tool usage is determined semantically from user's task description:
- Default: Agent execution (no `command` field)
### Output
-- Test task JSON files in `.task/` directory (minimum 2)
-- IMPL_PLAN.md with test strategy and fix cycle specification
+- Test task JSON files in `.task/` directory (minimum 4):
+ - IMPL-001.json (test generation)
+ - IMPL-001.3-validation.json (code validation gate)
+ - IMPL-001.5-review.json (test quality gate)
+ - IMPL-002.json (test execution & fix)
+- IMPL_PLAN.md with test strategy, validation gates, and fix cycle specification
- TODO_LIST.md with test phase indicators
- Session ready for test execution
diff --git a/.claude/workflows/test-quality-config.json b/.claude/workflows/test-quality-config.json
new file mode 100644
index 00000000..f0c1c104
--- /dev/null
+++ b/.claude/workflows/test-quality-config.json
@@ -0,0 +1,251 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "version": "1.0.0",
+ "description": "Test quality and code validation configuration for AI-generated code",
+
+ "code_validation": {
+ "description": "Pre-test validation for AI-generated code common errors",
+ "enabled": true,
+ "phases": {
+ "L0_compilation": {
+ "description": "TypeScript/JavaScript compilation check",
+ "enabled": true,
+ "commands": {
+ "typescript": "npx tsc --noEmit --skipLibCheck",
+ "javascript": "node --check"
+ },
+ "critical": true,
+ "failure_blocks_tests": true
+ },
+ "L0_imports": {
+ "description": "Import statement validation",
+ "enabled": true,
+ "checks": [
+ {
+ "id": "unresolved_imports",
+ "description": "Check for unresolved module imports",
+ "pattern": "Cannot find module|Module not found|Unable to resolve",
+ "severity": "critical"
+ },
+ {
+ "id": "circular_imports",
+ "description": "Check for circular dependencies",
+ "tool": "madge",
+ "command": "npx madge --circular --extensions ts,tsx,js,jsx",
+ "severity": "warning"
+ },
+ {
+ "id": "duplicate_imports",
+ "description": "Check for duplicate imports",
+ "eslint_rule": "import/no-duplicates",
+ "severity": "error"
+ },
+ {
+ "id": "unused_imports",
+ "description": "Check for unused imports",
+ "eslint_rule": "unused-imports/no-unused-imports",
+ "severity": "warning"
+ }
+ ]
+ },
+ "L0_variables": {
+ "description": "Variable declaration validation",
+ "enabled": true,
+ "checks": [
+ {
+ "id": "redeclaration",
+ "description": "Check for variable redeclaration",
+ "pattern": "Cannot redeclare|Duplicate identifier|has already been declared",
+ "severity": "critical"
+ },
+ {
+ "id": "scope_conflict",
+ "description": "Check for scope conflicts",
+ "eslint_rule": "no-shadow",
+ "severity": "error"
+ },
+ {
+ "id": "undefined_vars",
+ "description": "Check for undefined variables",
+ "eslint_rule": "no-undef",
+ "severity": "critical"
+ },
+ {
+ "id": "unused_vars",
+ "description": "Check for unused variables",
+ "eslint_rule": "@typescript-eslint/no-unused-vars",
+ "severity": "warning"
+ }
+ ]
+ },
+ "L0_types": {
+ "description": "TypeScript type validation",
+ "enabled": true,
+ "checks": [
+ {
+ "id": "type_mismatch",
+ "description": "Check for type mismatches",
+ "pattern": "Type .* is not assignable to type",
+ "severity": "critical"
+ },
+ {
+ "id": "missing_types",
+ "description": "Check for missing type definitions",
+ "pattern": "Could not find a declaration file",
+ "severity": "warning"
+ },
+ {
+ "id": "any_abuse",
+ "description": "Check for excessive any type usage",
+ "eslint_rule": "@typescript-eslint/no-explicit-any",
+ "severity": "warning",
+ "max_occurrences": 5
+ },
+ {
+ "id": "implicit_any",
+ "description": "Check for implicit any",
+ "pattern": "implicitly has an 'any' type",
+ "severity": "error"
+ }
+ ]
+ }
+ },
+ "severity_thresholds": {
+ "critical": 0,
+ "error": 3,
+ "warning": 10
+ },
+ "max_retries": 2,
+ "auto_fix": {
+ "enabled": true,
+ "safe_fixes_only": true,
+ "fixable_categories": ["imports", "formatting", "unused_vars"]
+ }
+ },
+
+ "test_quality": {
+ "description": "Test file quality validation (IMPL-001.5)",
+ "enabled": true,
+ "coverage": {
+ "minimum_threshold": 80,
+ "branch_threshold": 70,
+ "function_threshold": 80,
+ "line_threshold": 80
+ },
+ "anti_patterns": {
+ "empty_test_body": {
+ "pattern": "it\\(['\"].*['\"],\\s*\\(\\)\\s*=>\\s*\\{\\s*\\}\\)",
+ "severity": "critical",
+ "description": "Test with empty body"
+ },
+ "missing_assertion": {
+ "pattern": "it\\(['\"].*['\"],.*\\{[^}]*\\}\\)(?![\\s\\S]*expect)",
+ "severity": "critical",
+ "description": "Test without expect() assertion"
+ },
+ "skipped_without_reason": {
+ "pattern": "(it|describe)\\.skip\\(['\"][^'\"]*['\"](?!.*\\/\\/ )",
+ "severity": "error",
+ "description": "Skipped test without comment explaining why"
+ },
+ "todo_test": {
+ "pattern": "(it|test)\\.todo\\(",
+ "severity": "warning",
+ "description": "TODO test placeholder"
+ },
+ "only_test": {
+ "pattern": "(it|describe)\\.only\\(",
+ "severity": "critical",
+ "description": "Focused test (will skip other tests)"
+ }
+ },
+ "required_test_types": {
+ "unit": {
+ "min_per_function": 1,
+ "must_include": ["happy_path"]
+ },
+ "negative": {
+ "min_per_public_api": 1,
+ "description": "Error handling tests for public APIs"
+ },
+ "edge_case": {
+ "required_scenarios": ["null", "undefined", "empty_string", "empty_array", "boundary_values"]
+ }
+ }
+ },
+
+ "ai_specific_checks": {
+ "description": "Checks specifically for AI-generated code patterns",
+ "enabled": true,
+ "checks": [
+ {
+ "id": "hallucinated_imports",
+ "description": "Check for imports of non-existent packages",
+ "validation": "npm_package_exists",
+ "severity": "critical"
+ },
+ {
+ "id": "inconsistent_naming",
+ "description": "Check for naming inconsistencies within file",
+ "pattern": "function (\\w+).*\\1(?!\\()",
+ "severity": "warning"
+ },
+ {
+ "id": "placeholder_code",
+ "description": "Check for AI placeholder comments",
+ "patterns": [
+ "// TODO: implement",
+ "// Add your code here",
+ "// Implementation pending",
+ "throw new Error\\(['\"]Not implemented['\"]\\)"
+ ],
+ "severity": "error"
+ },
+ {
+ "id": "mock_in_production",
+ "description": "Check for mock/stub code in production files",
+ "patterns": [
+ "jest\\.mock\\(",
+ "sinon\\.",
+ "vi\\.mock\\("
+ ],
+ "exclude_paths": ["**/*.test.*", "**/*.spec.*", "**/test/**", "**/__tests__/**"],
+ "severity": "critical"
+ }
+ ]
+ },
+
+ "validation_commands": {
+ "typescript_check": {
+ "command": "npx tsc --noEmit --skipLibCheck",
+ "timeout": 60000,
+ "parse_errors": true
+ },
+ "eslint_check": {
+ "command": "npx eslint --format json",
+ "timeout": 60000,
+ "auto_fix_command": "npx eslint --fix"
+ },
+ "circular_deps_check": {
+ "command": "npx madge --circular --extensions ts,tsx,js,jsx",
+ "timeout": 30000
+ },
+ "package_validation": {
+ "command": "npm ls --json",
+ "timeout": 30000
+ }
+ },
+
+ "gate_decisions": {
+ "pass_criteria": {
+ "critical_issues": 0,
+ "error_issues": "<=3",
+ "warning_issues": "<=10"
+ },
+ "actions": {
+ "pass": "Proceed to IMPL-001.5 (Test Quality Gate)",
+ "soft_fail": "Auto-fix and retry (max 2 attempts)",
+ "hard_fail": "Block and report to user with fix suggestions"
+ }
+ }
+}
diff --git a/ccw/frontend/index.html b/ccw/frontend/index.html
new file mode 100644
index 00000000..49a9fe02
--- /dev/null
+++ b/ccw/frontend/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ CCW Dashboard
+
+
+
+
+
+
diff --git a/ccw/frontend/package-lock.json b/ccw/frontend/package-lock.json
new file mode 100644
index 00000000..fc197c43
--- /dev/null
+++ b/ccw/frontend/package-lock.json
@@ -0,0 +1,4100 @@
+{
+ "name": "ccw-frontend",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "ccw-frontend",
+ "version": "0.1.0",
+ "dependencies": {
+ "@radix-ui/react-dialog": "^1.1.0",
+ "@radix-ui/react-dropdown-menu": "^2.1.0",
+ "@radix-ui/react-select": "^2.1.0",
+ "@radix-ui/react-tabs": "^1.1.0",
+ "@radix-ui/react-toast": "^1.2.0",
+ "@radix-ui/react-tooltip": "^1.1.0",
+ "@tanstack/react-query": "^5.60.0",
+ "@xyflow/react": "^12.3.0",
+ "class-variance-authority": "^0.7.0",
+ "clsx": "^2.1.0",
+ "lucide-react": "^0.460.0",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-router-dom": "^6.28.0",
+ "tailwind-merge": "^2.5.0",
+ "zod": "^3.23.8",
+ "zustand": "^5.0.0"
+ },
+ "devDependencies": {
+ "@types/react": "^18.3.0",
+ "@types/react-dom": "^18.3.0",
+ "@vitejs/plugin-react": "^4.3.0",
+ "autoprefixer": "^10.4.20",
+ "postcss": "^8.4.40",
+ "tailwindcss": "^3.4.0",
+ "tailwindcss-animate": "^1.0.7",
+ "typescript": "^5.6.0",
+ "vite": "^6.0.0"
+ }
+ },
+ "node_modules/@alloc/quick-lru": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+ "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "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-transform-react-jsx-self": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.28.6",
+ "@babel/parser": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.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.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@floating-ui/core": {
+ "version": "1.7.4",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz",
+ "integrity": "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.10"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.7.5",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.5.tgz",
+ "integrity": "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/core": "^1.7.4",
+ "@floating-ui/utils": "^0.2.10"
+ }
+ },
+ "node_modules/@floating-ui/react-dom": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.7.tgz",
+ "integrity": "sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/dom": "^1.7.5"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.10",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
+ "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
+ "license": "MIT"
+ },
+ "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/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@radix-ui/number": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
+ "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==",
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/primitive": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
+ "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/react-arrow": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
+ "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-collection": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
+ "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-compose-refs": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
+ "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz",
+ "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-direction": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
+ "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dismissable-layer": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
+ "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-escape-keydown": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dropdown-menu": {
+ "version": "2.1.16",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz",
+ "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-menu": "2.1.16",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-guards": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz",
+ "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-scope": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
+ "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-id": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
+ "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-menu": {
+ "version": "2.1.16",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz",
+ "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popper": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz",
+ "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/react-dom": "^2.0.0",
+ "@radix-ui/react-arrow": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-use-rect": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1",
+ "@radix-ui/rect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-portal": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
+ "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-presence": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
+ "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-roving-focus": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz",
+ "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-select": {
+ "version": "2.2.6",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz",
+ "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.1",
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-visually-hidden": "1.2.3",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-tabs": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz",
+ "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-toast": {
+ "version": "1.2.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz",
+ "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-visually-hidden": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-tooltip": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz",
+ "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-visually-hidden": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-callback-ref": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
+ "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-controllable-state": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
+ "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-effect-event": "0.0.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-effect-event": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
+ "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-escape-keydown": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
+ "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-callback-ref": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-layout-effect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
+ "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-previous": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz",
+ "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-rect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz",
+ "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/rect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-size": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
+ "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-visually-hidden": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz",
+ "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/rect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
+ "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
+ "license": "MIT"
+ },
+ "node_modules/@remix-run/router": {
+ "version": "1.23.2",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
+ "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.27",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.0.tgz",
+ "integrity": "sha512-tPgXB6cDTndIe1ah7u6amCI1T0SsnlOuKgg10Xh3uizJk4e5M1JGaUMk7J4ciuAUcFpbOiNhm2XIjP9ON0dUqA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.0.tgz",
+ "integrity": "sha512-sa4LyseLLXr1onr97StkU1Nb7fWcg6niokTwEVNOO7awaKaoRObQ54+V/hrF/BP1noMEaaAW6Fg2d/CfLiq3Mg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.0.tgz",
+ "integrity": "sha512-/NNIj9A7yLjKdmkx5dC2XQ9DmjIECpGpwHoGmA5E1AhU0fuICSqSWScPhN1yLCkEdkCwJIDu2xIeLPs60MNIVg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.0.tgz",
+ "integrity": "sha512-xoh8abqgPrPYPr7pTYipqnUi1V3em56JzE/HgDgitTqZBZ3yKCWI+7KUkceM6tNweyUKYru1UMi7FC060RyKwA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.0.tgz",
+ "integrity": "sha512-PCkMh7fNahWSbA0OTUQ2OpYHpjZZr0hPr8lId8twD7a7SeWrvT3xJVyza+dQwXSSq4yEQTMoXgNOfMCsn8584g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.0.tgz",
+ "integrity": "sha512-1j3stGx+qbhXql4OCDZhnK7b01s6rBKNybfsX+TNrEe9JNq4DLi1yGiR1xW+nL+FNVvI4D02PUnl6gJ/2y6WJA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.0.tgz",
+ "integrity": "sha512-eyrr5W08Ms9uM0mLcKfM/Uzx7hjhz2bcjv8P2uynfj0yU8GGPdz8iYrBPhiLOZqahoAMB8ZiolRZPbbU2MAi6Q==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.0.tgz",
+ "integrity": "sha512-Xds90ITXJCNyX9pDhqf85MKWUI4lqjiPAipJ8OLp8xqI2Ehk+TCVhF9rvOoN8xTbcafow3QOThkNnrM33uCFQA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.0.tgz",
+ "integrity": "sha512-Xws2KA4CLvZmXjy46SQaXSejuKPhwVdaNinldoYfqruZBaJHqVo6hnRa8SDo9z7PBW5x84SH64+izmldCgbezw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.0.tgz",
+ "integrity": "sha512-hrKXKbX5FdaRJj7lTMusmvKbhMJSGWJ+w++4KmjiDhpTgNlhYobMvKfDoIWecy4O60K6yA4SnztGuNTQF+Lplw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.0.tgz",
+ "integrity": "sha512-6A+nccfSDGKsPm00d3xKcrsBcbqzCTAukjwWK6rbuAnB2bHaL3r9720HBVZ/no7+FhZLz/U3GwwZZEh6tOSI8Q==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.0.tgz",
+ "integrity": "sha512-4P1VyYUe6XAJtQH1Hh99THxr0GKMMwIXsRNOceLrJnaHTDgk1FTcTimDgneRJPvB3LqDQxUmroBclQ1S0cIJwQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.0.tgz",
+ "integrity": "sha512-8Vv6pLuIZCMcgXre6c3nOPhE0gjz1+nZP6T+hwWjr7sVH8k0jRkH+XnfjjOTglyMBdSKBPPz54/y1gToSKwrSQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.0.tgz",
+ "integrity": "sha512-r1te1M0Sm2TBVD/RxBPC6RZVwNqUTwJTA7w+C/IW5v9Ssu6xmxWEi+iJQlpBhtUiT1raJ5b48pI8tBvEjEFnFA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.0.tgz",
+ "integrity": "sha512-say0uMU/RaPm3CDQLxUUTF2oNWL8ysvHkAjcCzV2znxBr23kFfaxocS9qJm+NdkRhF8wtdEEAJuYcLPhSPbjuQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.0.tgz",
+ "integrity": "sha512-/MU7/HizQGsnBREtRpcSbSV1zfkoxSTR7wLsRmBPQ8FwUj5sykrP1MyJTvsxP5KBq9SyE6kH8UQQQwa0ASeoQQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.0.tgz",
+ "integrity": "sha512-Q9eh+gUGILIHEaJf66aF6a414jQbDnn29zeu0eX3dHMuysnhTvsUvZTCAyZ6tJhUjnvzBKE4FtuaYxutxRZpOg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.0.tgz",
+ "integrity": "sha512-OR5p5yG5OKSxHReWmwvM0P+VTPMwoBS45PXTMYaskKQqybkS3Kmugq1W+YbNWArF8/s7jQScgzXUhArzEQ7x0A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.0.tgz",
+ "integrity": "sha512-XeatKzo4lHDsVEbm1XDHZlhYZZSQYym6dg2X/Ko0kSFgio+KXLsxwJQprnR48GvdIKDOpqWqssC3iBCjoMcMpw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.0.tgz",
+ "integrity": "sha512-Lu71y78F5qOfYmubYLHPcJm74GZLU6UJ4THkf/a1K7Tz2ycwC2VUbsqbJAXaR6Bx70SRdlVrt2+n5l7F0agTUw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.0.tgz",
+ "integrity": "sha512-v5xwKDWcu7qhAEcsUubiav7r+48Uk/ENWdr82MBZZRIm7zThSxCIVDfb3ZeRRq9yqk+oIzMdDo6fCcA5DHfMyA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.0.tgz",
+ "integrity": "sha512-XnaaaSMGSI6Wk8F4KK3QP7GfuuhjGchElsVerCplUuxRIzdvZ7hRBpLR0omCmw+kI2RFJB80nenhOoGXlJ5TfQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.0.tgz",
+ "integrity": "sha512-3K1lP+3BXY4t4VihLw5MEg6IZD3ojSYzqzBG571W3kNQe4G4CcFpSUQVgurYgib5d+YaCjeFow8QivWp8vuSvA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.0.tgz",
+ "integrity": "sha512-MDk610P/vJGc5L5ImE4k5s+GZT3en0KoK1MKPXCRgzmksAMk79j4h3k1IerxTNqwDLxsGxStEZVBqG0gIqZqoA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.0.tgz",
+ "integrity": "sha512-Zv7v6q6aV+VslnpwzqKAmrk5JdVkLUzok2208ZXGipjb+msxBr/fJPZyeEXiFgH7k62Ak0SLIfxQRZQvTuf7rQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@tanstack/query-core": {
+ "version": "5.90.20",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz",
+ "integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/react-query": {
+ "version": "5.90.20",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.20.tgz",
+ "integrity": "sha512-vXBxa+qeyveVO7OA0jX1z+DeyCA4JKnThKv411jd5SORpBKgkcVnYKCiBgECvADvniBX7tobwBmg01qq9JmMJw==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/query-core": "5.90.20"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": "^18 || ^19"
+ }
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/d3-color": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
+ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-drag": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
+ "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-interpolate": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-color": "*"
+ }
+ },
+ "node_modules/@types/d3-selection": {
+ "version": "3.0.11",
+ "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
+ "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-transition": {
+ "version": "3.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
+ "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-zoom": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
+ "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-interpolate": "*",
+ "@types/d3-selection": "*"
+ }
+ },
+ "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/prop-types": {
+ "version": "15.7.15",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "18.3.27",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz",
+ "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/prop-types": "*",
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "18.3.7",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
+ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^18.0.0"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.28.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.27",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.17.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+ }
+ },
+ "node_modules/@xyflow/react": {
+ "version": "12.10.0",
+ "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.10.0.tgz",
+ "integrity": "sha512-eOtz3whDMWrB4KWVatIBrKuxECHqip6PfA8fTpaS2RUGVpiEAe+nqDKsLqkViVWxDGreq0lWX71Xth/SPAzXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "@xyflow/system": "0.0.74",
+ "classcat": "^5.0.3",
+ "zustand": "^4.4.0"
+ },
+ "peerDependencies": {
+ "react": ">=17",
+ "react-dom": ">=17"
+ }
+ },
+ "node_modules/@xyflow/react/node_modules/zustand": {
+ "version": "4.5.7",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
+ "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
+ "license": "MIT",
+ "dependencies": {
+ "use-sync-external-store": "^1.2.2"
+ },
+ "engines": {
+ "node": ">=12.7.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.8",
+ "immer": ">=9.0.6",
+ "react": ">=16.8"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@xyflow/system": {
+ "version": "0.0.74",
+ "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.74.tgz",
+ "integrity": "sha512-7v7B/PkiVrkdZzSbL+inGAo6tkR/WQHHG0/jhSvLQToCsfa8YubOGmBYd1s08tpKpihdHDZFwzQZeR69QSBb4Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-drag": "^3.0.7",
+ "@types/d3-interpolate": "^3.0.4",
+ "@types/d3-selection": "^3.0.10",
+ "@types/d3-transition": "^3.0.8",
+ "@types/d3-zoom": "^3.0.8",
+ "d3-drag": "^3.0.0",
+ "d3-interpolate": "^3.0.1",
+ "d3-selection": "^3.0.0",
+ "d3-zoom": "^3.0.0"
+ }
+ },
+ "node_modules/any-promise": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
+ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/arg": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
+ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/aria-hidden": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
+ "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/autoprefixer": {
+ "version": "10.4.23",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz",
+ "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.28.1",
+ "caniuse-lite": "^1.0.30001760",
+ "fraction.js": "^5.3.4",
+ "picocolors": "^1.1.1",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "bin": {
+ "autoprefixer": "bin/autoprefixer"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.9.19",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz",
+ "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.js"
+ }
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "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/camelcase-css": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
+ "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001766",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz",
+ "integrity": "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==",
+ "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/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/chokidar/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/class-variance-authority": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
+ "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "clsx": "^2.1.1"
+ },
+ "funding": {
+ "url": "https://polar.sh/cva"
+ }
+ },
+ "node_modules/classcat": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz",
+ "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==",
+ "license": "MIT"
+ },
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/commander": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
+ "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "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/cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "cssesc": "bin/cssesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dispatch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
+ "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-drag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
+ "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-selection": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-selection": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
+ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-transition": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
+ "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-dispatch": "1 - 3",
+ "d3-ease": "1 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "d3-selection": "2 - 3"
+ }
+ },
+ "node_modules/d3-zoom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
+ "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "2 - 3",
+ "d3-transition": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "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/detect-node-es": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
+ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
+ "license": "MIT"
+ },
+ "node_modules/didyoumean": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
+ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/dlv": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
+ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.282",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.282.tgz",
+ "integrity": "sha512-FCPkJtpst28UmFzd903iU7PdeVTfY0KAeJy+Lk0GLZRwgwYHn/irRcaCbQQOmr5Vytc/7rcavsYLvTM8RiHYhQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.12",
+ "@esbuild/android-arm": "0.25.12",
+ "@esbuild/android-arm64": "0.25.12",
+ "@esbuild/android-x64": "0.25.12",
+ "@esbuild/darwin-arm64": "0.25.12",
+ "@esbuild/darwin-x64": "0.25.12",
+ "@esbuild/freebsd-arm64": "0.25.12",
+ "@esbuild/freebsd-x64": "0.25.12",
+ "@esbuild/linux-arm": "0.25.12",
+ "@esbuild/linux-arm64": "0.25.12",
+ "@esbuild/linux-ia32": "0.25.12",
+ "@esbuild/linux-loong64": "0.25.12",
+ "@esbuild/linux-mips64el": "0.25.12",
+ "@esbuild/linux-ppc64": "0.25.12",
+ "@esbuild/linux-riscv64": "0.25.12",
+ "@esbuild/linux-s390x": "0.25.12",
+ "@esbuild/linux-x64": "0.25.12",
+ "@esbuild/netbsd-arm64": "0.25.12",
+ "@esbuild/netbsd-x64": "0.25.12",
+ "@esbuild/openbsd-arm64": "0.25.12",
+ "@esbuild/openbsd-x64": "0.25.12",
+ "@esbuild/openharmony-arm64": "0.25.12",
+ "@esbuild/sunos-x64": "0.25.12",
+ "@esbuild/win32-arm64": "0.25.12",
+ "@esbuild/win32-ia32": "0.25.12",
+ "@esbuild/win32-x64": "0.25.12"
+ }
+ },
+ "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/fast-glob": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.8"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-glob/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fastq": {
+ "version": "1.20.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
+ "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/fraction.js": {
+ "version": "5.3.4",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
+ "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/rawify"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-nonce": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
+ "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/jiti": {
+ "version": "1.21.7",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
+ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jiti": "bin/jiti.js"
+ }
+ },
+ "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==",
+ "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/lilconfig": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
+ "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antonk52"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/lucide-react": {
+ "version": "0.460.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.460.0.tgz",
+ "integrity": "sha512-BVtq/DykVeIvRTJvRAgCsOwaGL8Un3Bxh8MbDxMhEWlZay3T4IpEKDEpwt5KZ0KJMHzgm6jrltxlT5eXOWXDHg==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "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/mz": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
+ "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0",
+ "object-assign": "^4.0.1",
+ "thenify-all": "^1.0.0"
+ }
+ },
+ "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/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-hash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
+ "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "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": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/pirates": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
+ "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "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/postcss-import": {
+ "version": "15.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
+ "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "postcss-value-parser": "^4.0.0",
+ "read-cache": "^1.0.0",
+ "resolve": "^1.1.7"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.0.0"
+ }
+ },
+ "node_modules/postcss-js": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
+ "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "camelcase-css": "^2.0.1"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >= 16"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4.21"
+ }
+ },
+ "node_modules/postcss-load-config": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
+ "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "lilconfig": "^3.1.1"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "peerDependencies": {
+ "jiti": ">=1.21.0",
+ "postcss": ">=8.0.9",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ },
+ "postcss": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/postcss-nested": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
+ "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "postcss-selector-parser": "^6.1.1"
+ },
+ "engines": {
+ "node": ">=12.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.14"
+ }
+ },
+ "node_modules/postcss-selector-parser": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+ "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/react": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
+ },
+ "peerDependencies": {
+ "react": "^18.3.1"
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-remove-scroll": {
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz",
+ "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==",
+ "license": "MIT",
+ "dependencies": {
+ "react-remove-scroll-bar": "^2.3.7",
+ "react-style-singleton": "^2.2.3",
+ "tslib": "^2.1.0",
+ "use-callback-ref": "^1.3.3",
+ "use-sidecar": "^1.1.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-remove-scroll-bar": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
+ "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
+ "license": "MIT",
+ "dependencies": {
+ "react-style-singleton": "^2.2.2",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router": {
+ "version": "6.30.3",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz",
+ "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==",
+ "license": "MIT",
+ "dependencies": {
+ "@remix-run/router": "1.23.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8"
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "6.30.3",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz",
+ "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==",
+ "license": "MIT",
+ "dependencies": {
+ "@remix-run/router": "1.23.2",
+ "react-router": "6.30.3"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
+ }
+ },
+ "node_modules/react-style-singleton": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
+ "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
+ "license": "MIT",
+ "dependencies": {
+ "get-nonce": "^1.0.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/read-cache": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
+ "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pify": "^2.3.0"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.11",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
+ "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-core-module": "^2.16.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.0.tgz",
+ "integrity": "sha512-e5lPJi/aui4TO1LpAXIRLySmwXSE8k3b9zoGfd42p67wzxog4WHjiZF3M2uheQih4DGyc25QEV4yRBbpueNiUA==",
+ "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.57.0",
+ "@rollup/rollup-android-arm64": "4.57.0",
+ "@rollup/rollup-darwin-arm64": "4.57.0",
+ "@rollup/rollup-darwin-x64": "4.57.0",
+ "@rollup/rollup-freebsd-arm64": "4.57.0",
+ "@rollup/rollup-freebsd-x64": "4.57.0",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.57.0",
+ "@rollup/rollup-linux-arm-musleabihf": "4.57.0",
+ "@rollup/rollup-linux-arm64-gnu": "4.57.0",
+ "@rollup/rollup-linux-arm64-musl": "4.57.0",
+ "@rollup/rollup-linux-loong64-gnu": "4.57.0",
+ "@rollup/rollup-linux-loong64-musl": "4.57.0",
+ "@rollup/rollup-linux-ppc64-gnu": "4.57.0",
+ "@rollup/rollup-linux-ppc64-musl": "4.57.0",
+ "@rollup/rollup-linux-riscv64-gnu": "4.57.0",
+ "@rollup/rollup-linux-riscv64-musl": "4.57.0",
+ "@rollup/rollup-linux-s390x-gnu": "4.57.0",
+ "@rollup/rollup-linux-x64-gnu": "4.57.0",
+ "@rollup/rollup-linux-x64-musl": "4.57.0",
+ "@rollup/rollup-openbsd-x64": "4.57.0",
+ "@rollup/rollup-openharmony-arm64": "4.57.0",
+ "@rollup/rollup-win32-arm64-msvc": "4.57.0",
+ "@rollup/rollup-win32-ia32-msvc": "4.57.0",
+ "@rollup/rollup-win32-x64-gnu": "4.57.0",
+ "@rollup/rollup-win32-x64-msvc": "4.57.0",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "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/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/sucrase": {
+ "version": "3.35.1",
+ "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
+ "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.2",
+ "commander": "^4.0.0",
+ "lines-and-columns": "^1.1.6",
+ "mz": "^2.7.0",
+ "pirates": "^4.0.1",
+ "tinyglobby": "^0.2.11",
+ "ts-interface-checker": "^0.1.9"
+ },
+ "bin": {
+ "sucrase": "bin/sucrase",
+ "sucrase-node": "bin/sucrase-node"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/tailwind-merge": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz",
+ "integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/dcastil"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "3.4.19",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
+ "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@alloc/quick-lru": "^5.2.0",
+ "arg": "^5.0.2",
+ "chokidar": "^3.6.0",
+ "didyoumean": "^1.2.2",
+ "dlv": "^1.1.3",
+ "fast-glob": "^3.3.2",
+ "glob-parent": "^6.0.2",
+ "is-glob": "^4.0.3",
+ "jiti": "^1.21.7",
+ "lilconfig": "^3.1.3",
+ "micromatch": "^4.0.8",
+ "normalize-path": "^3.0.0",
+ "object-hash": "^3.0.0",
+ "picocolors": "^1.1.1",
+ "postcss": "^8.4.47",
+ "postcss-import": "^15.1.0",
+ "postcss-js": "^4.0.1",
+ "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0",
+ "postcss-nested": "^6.2.0",
+ "postcss-selector-parser": "^6.1.2",
+ "resolve": "^1.22.8",
+ "sucrase": "^3.35.0"
+ },
+ "bin": {
+ "tailwind": "lib/cli.js",
+ "tailwindcss": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tailwindcss-animate": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz",
+ "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "tailwindcss": ">=3.0.0 || insiders"
+ }
+ },
+ "node_modules/thenify": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
+ "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0"
+ }
+ },
+ "node_modules/thenify-all": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
+ "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "thenify": ">= 3.1.0 < 4"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "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/tinyglobby/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/tinyglobby/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/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/ts-interface-checker": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
+ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
+ "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/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/use-callback-ref": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
+ "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/use-sidecar": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
+ "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "detect-node-es": "^1.1.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/use-sync-external-store": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vite": {
+ "version": "6.4.1",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
+ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2",
+ "postcss": "^8.5.3",
+ "rollup": "^4.34.9",
+ "tinyglobby": "^0.2.13"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "jiti": ">=1.21.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "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/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/vite/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/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/zod": {
+ "version": "3.25.76",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/zustand": {
+ "version": "5.0.10",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.10.tgz",
+ "integrity": "sha512-U1AiltS1O9hSy3rul+Ub82ut2fqIAefiSuwECWt6jlMVUGejvf+5omLcRBSzqbRagSM3hQZbtzdeRc6QVScXTg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18.0.0",
+ "immer": ">=9.0.6",
+ "react": ">=18.0.0",
+ "use-sync-external-store": ">=1.2.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "use-sync-external-store": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/ccw/frontend/package.json b/ccw/frontend/package.json
new file mode 100644
index 00000000..61805b72
--- /dev/null
+++ b/ccw/frontend/package.json
@@ -0,0 +1,42 @@
+{
+ "name": "ccw-frontend",
+ "private": true,
+ "version": "0.1.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview",
+ "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
+ },
+ "dependencies": {
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-router-dom": "^6.28.0",
+ "zustand": "^5.0.0",
+ "@tanstack/react-query": "^5.60.0",
+ "@xyflow/react": "^12.3.0",
+ "@radix-ui/react-dialog": "^1.1.0",
+ "@radix-ui/react-dropdown-menu": "^2.1.0",
+ "@radix-ui/react-tabs": "^1.1.0",
+ "@radix-ui/react-tooltip": "^1.1.0",
+ "@radix-ui/react-select": "^2.1.0",
+ "@radix-ui/react-toast": "^1.2.0",
+ "clsx": "^2.1.0",
+ "tailwind-merge": "^2.5.0",
+ "class-variance-authority": "^0.7.0",
+ "lucide-react": "^0.460.0",
+ "zod": "^3.23.8"
+ },
+ "devDependencies": {
+ "@types/react": "^18.3.0",
+ "tailwindcss-animate": "^1.0.7",
+ "@types/react-dom": "^18.3.0",
+ "@vitejs/plugin-react": "^4.3.0",
+ "autoprefixer": "^10.4.20",
+ "postcss": "^8.4.40",
+ "tailwindcss": "^3.4.0",
+ "typescript": "^5.6.0",
+ "vite": "^6.0.0"
+ }
+}
diff --git a/ccw/frontend/postcss.config.js b/ccw/frontend/postcss.config.js
new file mode 100644
index 00000000..2e7af2b7
--- /dev/null
+++ b/ccw/frontend/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/ccw/frontend/src/App.tsx b/ccw/frontend/src/App.tsx
new file mode 100644
index 00000000..fb6d08ae
--- /dev/null
+++ b/ccw/frontend/src/App.tsx
@@ -0,0 +1,17 @@
+// ========================================
+// App Component
+// ========================================
+// Root application component with Router provider
+
+import { RouterProvider } from 'react-router-dom';
+import { router } from './router';
+
+/**
+ * Root App component
+ * Provides routing and global providers
+ */
+function App() {
+ return ;
+}
+
+export default App;
diff --git a/ccw/frontend/src/components/layout/AppShell.tsx b/ccw/frontend/src/components/layout/AppShell.tsx
new file mode 100644
index 00000000..3df8aed1
--- /dev/null
+++ b/ccw/frontend/src/components/layout/AppShell.tsx
@@ -0,0 +1,111 @@
+// ========================================
+// AppShell Component
+// ========================================
+// Root layout component combining Header, Sidebar, and MainContent
+
+import { useState, useCallback, useEffect } from 'react';
+import { cn } from '@/lib/utils';
+import { Header } from './Header';
+import { Sidebar } from './Sidebar';
+import { MainContent } from './MainContent';
+
+export interface AppShellProps {
+ /** Initial sidebar collapsed state */
+ defaultCollapsed?: boolean;
+ /** Current project path to display in header */
+ projectPath?: string;
+ /** Callback for refresh action */
+ onRefresh?: () => void;
+ /** Whether refresh is in progress */
+ isRefreshing?: boolean;
+ /** Children to render in main content area */
+ children?: React.ReactNode;
+}
+
+// Local storage key for sidebar state
+const SIDEBAR_COLLAPSED_KEY = 'ccw-sidebar-collapsed';
+
+export function AppShell({
+ defaultCollapsed = false,
+ projectPath = '',
+ onRefresh,
+ isRefreshing = false,
+ children,
+}: AppShellProps) {
+ // Sidebar collapse state (persisted)
+ const [sidebarCollapsed, setSidebarCollapsed] = useState(() => {
+ if (typeof window !== 'undefined') {
+ const stored = localStorage.getItem(SIDEBAR_COLLAPSED_KEY);
+ return stored ? JSON.parse(stored) : defaultCollapsed;
+ }
+ return defaultCollapsed;
+ });
+
+ // Mobile sidebar open state
+ const [mobileOpen, setMobileOpen] = useState(false);
+
+ // Persist sidebar state
+ useEffect(() => {
+ localStorage.setItem(SIDEBAR_COLLAPSED_KEY, JSON.stringify(sidebarCollapsed));
+ }, [sidebarCollapsed]);
+
+ // Close mobile sidebar on route change or resize
+ useEffect(() => {
+ const handleResize = () => {
+ if (window.innerWidth >= 768) {
+ setMobileOpen(false);
+ }
+ };
+
+ window.addEventListener('resize', handleResize);
+ return () => window.removeEventListener('resize', handleResize);
+ }, []);
+
+ const handleMenuClick = useCallback(() => {
+ setMobileOpen((prev) => !prev);
+ }, []);
+
+ const handleMobileClose = useCallback(() => {
+ setMobileOpen(false);
+ }, []);
+
+ const handleCollapsedChange = useCallback((collapsed: boolean) => {
+ setSidebarCollapsed(collapsed);
+ }, []);
+
+ return (
+
+ {/* Header - fixed at top */}
+
+
+ {/* Main layout - sidebar + content */}
+
+ {/* Sidebar */}
+
+
+ {/* Main content area */}
+
+ {children}
+
+
+
+ );
+}
+
+export default AppShell;
diff --git a/ccw/frontend/src/components/layout/Header.tsx b/ccw/frontend/src/components/layout/Header.tsx
new file mode 100644
index 00000000..dbb21678
--- /dev/null
+++ b/ccw/frontend/src/components/layout/Header.tsx
@@ -0,0 +1,164 @@
+// ========================================
+// Header Component
+// ========================================
+// Top navigation bar with theme toggle and user menu
+
+import { useCallback } from 'react';
+import { Link } from 'react-router-dom';
+import {
+ Workflow,
+ Menu,
+ Moon,
+ Sun,
+ RefreshCw,
+ Settings,
+ User,
+ LogOut,
+} from 'lucide-react';
+import { cn } from '@/lib/utils';
+import { Button } from '@/components/ui/Button';
+import { useTheme } from '@/hooks';
+
+export interface HeaderProps {
+ /** Callback to toggle mobile sidebar */
+ onMenuClick?: () => void;
+ /** Current project path */
+ projectPath?: string;
+ /** Callback for refresh action */
+ onRefresh?: () => void;
+ /** Whether refresh is in progress */
+ isRefreshing?: boolean;
+}
+
+export function Header({
+ onMenuClick,
+ projectPath = '',
+ onRefresh,
+ isRefreshing = false,
+}: HeaderProps) {
+ const { isDark, toggleTheme } = useTheme();
+
+ const handleRefresh = useCallback(() => {
+ if (onRefresh && !isRefreshing) {
+ onRefresh();
+ }
+ }, [onRefresh, isRefreshing]);
+
+ // Get display path (truncate if too long)
+ const displayPath = projectPath.length > 40
+ ? '...' + projectPath.slice(-37)
+ : projectPath || 'No project selected';
+
+ return (
+
+ {/* Left side - Menu button (mobile) and Logo */}
+
+ {/* Mobile menu toggle */}
+
+
+ {/* Logo / Brand */}
+
+
+ Claude Code Workflow
+ CCW
+
+
+
+ {/* Right side - Actions */}
+
+ {/* Project path indicator */}
+ {projectPath && (
+
+
+ {displayPath}
+
+
+ )}
+
+ {/* Refresh button */}
+ {onRefresh && (
+
+ )}
+
+ {/* Theme toggle */}
+
+
+ {/* User menu dropdown - simplified version */}
+
+
+
+ {/* Dropdown menu */}
+
+
+
+
+ Settings
+
+
+
+
+
+
+
+
+ );
+}
+
+export default Header;
diff --git a/ccw/frontend/src/components/layout/MainContent.tsx b/ccw/frontend/src/components/layout/MainContent.tsx
new file mode 100644
index 00000000..05d288d0
--- /dev/null
+++ b/ccw/frontend/src/components/layout/MainContent.tsx
@@ -0,0 +1,31 @@
+// ========================================
+// MainContent Component
+// ========================================
+// Main content area with scrollable container and Outlet for routes
+
+import { Outlet } from 'react-router-dom';
+import { cn } from '@/lib/utils';
+
+export interface MainContentProps {
+ /** Additional class names */
+ className?: string;
+ /** Children to render instead of Outlet */
+ children?: React.ReactNode;
+}
+
+export function MainContent({ className, children }: MainContentProps) {
+ return (
+
+ {children ?? }
+
+ );
+}
+
+export default MainContent;
diff --git a/ccw/frontend/src/components/layout/Sidebar.tsx b/ccw/frontend/src/components/layout/Sidebar.tsx
new file mode 100644
index 00000000..5631b0d3
--- /dev/null
+++ b/ccw/frontend/src/components/layout/Sidebar.tsx
@@ -0,0 +1,184 @@
+// ========================================
+// Sidebar Component
+// ========================================
+// Collapsible navigation sidebar with route links
+
+import { useState, useCallback } from 'react';
+import { NavLink, useLocation } from 'react-router-dom';
+import {
+ Home,
+ FolderKanban,
+ Workflow,
+ RefreshCw,
+ AlertCircle,
+ Sparkles,
+ Terminal,
+ Brain,
+ Settings,
+ HelpCircle,
+ PanelLeftClose,
+ PanelLeftOpen,
+} from 'lucide-react';
+import { cn } from '@/lib/utils';
+import { Button } from '@/components/ui/Button';
+
+export interface SidebarProps {
+ /** Whether sidebar is collapsed */
+ collapsed?: boolean;
+ /** Callback when collapse state changes */
+ onCollapsedChange?: (collapsed: boolean) => void;
+ /** Whether sidebar is open on mobile */
+ mobileOpen?: boolean;
+ /** Callback to close mobile sidebar */
+ onMobileClose?: () => void;
+}
+
+interface NavItem {
+ path: string;
+ label: string;
+ icon: React.ElementType;
+ badge?: number | string;
+ badgeVariant?: 'default' | 'success' | 'warning' | 'info';
+}
+
+const navItems: NavItem[] = [
+ { path: '/', label: 'Home', icon: Home },
+ { path: '/sessions', label: 'Sessions', icon: FolderKanban },
+ { path: '/orchestrator', label: 'Orchestrator', icon: Workflow },
+ { path: '/loops', label: 'Loop Monitor', icon: RefreshCw },
+ { path: '/issues', label: 'Issues', icon: AlertCircle },
+ { path: '/skills', label: 'Skills', icon: Sparkles },
+ { path: '/commands', label: 'Commands', icon: Terminal },
+ { path: '/memory', label: 'Memory', icon: Brain },
+ { path: '/settings', label: 'Settings', icon: Settings },
+ { path: '/help', label: 'Help', icon: HelpCircle },
+];
+
+export function Sidebar({
+ collapsed = false,
+ onCollapsedChange,
+ mobileOpen = false,
+ onMobileClose,
+}: SidebarProps) {
+ const location = useLocation();
+ const [internalCollapsed, setInternalCollapsed] = useState(collapsed);
+
+ const isCollapsed = onCollapsedChange ? collapsed : internalCollapsed;
+
+ const handleToggleCollapse = useCallback(() => {
+ if (onCollapsedChange) {
+ onCollapsedChange(!collapsed);
+ } else {
+ setInternalCollapsed(!internalCollapsed);
+ }
+ }, [collapsed, internalCollapsed, onCollapsedChange]);
+
+ const handleNavClick = useCallback(() => {
+ // Close mobile sidebar when navigating
+ if (onMobileClose) {
+ onMobileClose();
+ }
+ }, [onMobileClose]);
+
+ return (
+ <>
+ {/* Mobile overlay */}
+ {mobileOpen && (
+
+ )}
+
+ {/* Sidebar */}
+
+ >
+ );
+}
+
+export default Sidebar;
diff --git a/ccw/frontend/src/components/layout/index.ts b/ccw/frontend/src/components/layout/index.ts
new file mode 100644
index 00000000..9f816291
--- /dev/null
+++ b/ccw/frontend/src/components/layout/index.ts
@@ -0,0 +1,16 @@
+// ========================================
+// Layout Components Barrel Export
+// ========================================
+// Re-export all layout components for convenient imports
+
+export { AppShell } from './AppShell';
+export type { AppShellProps } from './AppShell';
+
+export { Header } from './Header';
+export type { HeaderProps } from './Header';
+
+export { Sidebar } from './Sidebar';
+export type { SidebarProps } from './Sidebar';
+
+export { MainContent } from './MainContent';
+export type { MainContentProps } from './MainContent';
diff --git a/ccw/frontend/src/components/shared/IssueCard.tsx b/ccw/frontend/src/components/shared/IssueCard.tsx
new file mode 100644
index 00000000..cfd4c005
--- /dev/null
+++ b/ccw/frontend/src/components/shared/IssueCard.tsx
@@ -0,0 +1,238 @@
+// ========================================
+// IssueCard Component
+// ========================================
+// Card component for displaying issues with actions
+
+import { useState } from 'react';
+import {
+ AlertCircle,
+ AlertTriangle,
+ Info,
+ MoreVertical,
+ Edit,
+ Trash2,
+ ExternalLink,
+ CheckCircle,
+ Clock,
+ XCircle,
+} from 'lucide-react';
+import { cn } from '@/lib/utils';
+import { Card } from '@/components/ui/Card';
+import { Badge } from '@/components/ui/Badge';
+import { Button } from '@/components/ui/Button';
+import { Dropdown, DropdownTrigger, DropdownContent, DropdownItem } from '@/components/ui/Dropdown';
+import type { Issue } from '@/lib/api';
+
+// ========== Types ==========
+
+export interface IssueCardProps {
+ issue: Issue;
+ onEdit?: (issue: Issue) => void;
+ onDelete?: (issue: Issue) => void;
+ onClick?: (issue: Issue) => void;
+ onStatusChange?: (issue: Issue, status: Issue['status']) => void;
+ className?: string;
+ compact?: boolean;
+ showActions?: boolean;
+ draggableProps?: Record;
+ dragHandleProps?: Record;
+ innerRef?: React.Ref;
+}
+
+// ========== Priority Helpers ==========
+
+const priorityConfig: Record = {
+ critical: { icon: AlertCircle, color: 'destructive', label: 'Critical' },
+ high: { icon: AlertTriangle, color: 'warning', label: 'High' },
+ medium: { icon: Info, color: 'info', label: 'Medium' },
+ low: { icon: Info, color: 'secondary', label: 'Low' },
+};
+
+const statusConfig: Record = {
+ open: { icon: AlertCircle, color: 'info', label: 'Open' },
+ in_progress: { icon: Clock, color: 'warning', label: 'In Progress' },
+ resolved: { icon: CheckCircle, color: 'success', label: 'Resolved' },
+ closed: { icon: XCircle, color: 'muted', label: 'Closed' },
+ completed: { icon: CheckCircle, color: 'success', label: 'Completed' },
+};
+
+// ========== Priority Badge ==========
+
+export function PriorityBadge({ priority }: { priority: Issue['priority'] }) {
+ const config = priorityConfig[priority];
+ const Icon = config.icon;
+
+ return (
+
+
+ {config.label}
+
+ );
+}
+
+// ========== Status Badge ==========
+
+export function StatusBadge({ status }: { status: Issue['status'] }) {
+ const config = statusConfig[status];
+ const Icon = config.icon;
+
+ return (
+
+
+ {config.label}
+
+ );
+}
+
+// ========== Main IssueCard Component ==========
+
+export function IssueCard({
+ issue,
+ onEdit,
+ onDelete,
+ onClick,
+ onStatusChange,
+ className,
+ compact = false,
+ showActions = true,
+ draggableProps,
+ dragHandleProps,
+ innerRef,
+}: IssueCardProps) {
+ const [isMenuOpen, setIsMenuOpen] = useState(false);
+
+ const handleClick = () => {
+ if (!isMenuOpen) {
+ onClick?.(issue);
+ }
+ };
+
+ const handleEdit = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ setIsMenuOpen(false);
+ onEdit?.(issue);
+ };
+
+ const handleDelete = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ setIsMenuOpen(false);
+ onDelete?.(issue);
+ };
+
+ if (compact) {
+ return (
+
+
+
+
{issue.title}
+
#{issue.id}
+
+
+
+
+ );
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+ {issue.title}
+
+
#{issue.id}
+
+ {showActions && (
+
+
+
+
+
+
+
+ Edit
+
+ onStatusChange?.(issue, 'in_progress')}>
+
+ Start Progress
+
+ onStatusChange?.(issue, 'resolved')}>
+
+ Mark Resolved
+
+
+
+ Delete
+
+
+
+ )}
+
+
+ {/* Context Preview */}
+ {issue.context && (
+
+ {issue.context}
+
+ )}
+
+ {/* Labels */}
+ {issue.labels && issue.labels.length > 0 && (
+
+ {issue.labels.slice(0, 3).map((label) => (
+
+ {label}
+
+ ))}
+ {issue.labels.length > 3 && (
+
+ +{issue.labels.length - 3}
+
+ )}
+
+ )}
+
+ {/* Footer */}
+
+
+ {/* Solutions Count */}
+ {issue.solutions && issue.solutions.length > 0 && (
+
+
+ {issue.solutions.length} solution{issue.solutions.length !== 1 ? 's' : ''}
+
+ )}
+
+ );
+}
+
+export default IssueCard;
diff --git a/ccw/frontend/src/components/shared/KanbanBoard.tsx b/ccw/frontend/src/components/shared/KanbanBoard.tsx
new file mode 100644
index 00000000..986713d3
--- /dev/null
+++ b/ccw/frontend/src/components/shared/KanbanBoard.tsx
@@ -0,0 +1,265 @@
+// ========================================
+// KanbanBoard Component
+// ========================================
+// Drag-and-drop kanban board for loops and tasks
+
+import { useState, useCallback } from 'react';
+import {
+ DragDropContext,
+ Droppable,
+ Draggable,
+ type DropResult,
+ type DraggableProvided,
+ type DroppableProvided,
+} from '@hello-pangea/dnd';
+import { cn } from '@/lib/utils';
+import { Card } from '@/components/ui/Card';
+import { Badge } from '@/components/ui/Badge';
+
+// ========== Types ==========
+
+export interface KanbanItem {
+ id: string;
+ title?: string;
+ status: string;
+ [key: string]: unknown;
+}
+
+export interface KanbanColumn {
+ id: string;
+ title: string;
+ items: T[];
+ color?: string;
+ icon?: React.ReactNode;
+}
+
+export interface KanbanBoardProps {
+ columns: KanbanColumn[];
+ onDragEnd?: (result: DropResult, sourceColumn: string, destColumn: string) => void;
+ onItemClick?: (item: T) => void;
+ renderItem?: (item: T, provided: DraggableProvided) => React.ReactNode;
+ className?: string;
+ columnClassName?: string;
+ itemClassName?: string;
+ emptyColumnMessage?: string;
+ isLoading?: boolean;
+}
+
+// ========== Default Item Renderer ==========
+
+function DefaultItemRenderer({
+ item,
+ provided,
+ onClick,
+ className,
+}: {
+ item: T;
+ provided: DraggableProvided;
+ onClick?: () => void;
+ className?: string;
+}) {
+ return (
+
+
+ {item.title || item.id}
+
+
+ );
+}
+
+// ========== Column Component ==========
+
+function KanbanColumnComponent({
+ column,
+ onItemClick,
+ renderItem,
+ itemClassName,
+ emptyMessage,
+}: {
+ column: KanbanColumn;
+ onItemClick?: (item: T) => void;
+ renderItem?: (item: T, provided: DraggableProvided) => React.ReactNode;
+ itemClassName?: string;
+ emptyMessage?: string;
+}) {
+ return (
+
+ {(provided: DroppableProvided, snapshot) => (
+
+ {column.items.length === 0 ? (
+
+ {emptyMessage || 'No items'}
+
+ ) : (
+ column.items.map((item, index) => (
+
+ {(dragProvided: DraggableProvided) =>
+ renderItem ? (
+ renderItem(item, dragProvided)
+ ) : (
+ onItemClick?.(item)}
+ className={itemClassName}
+ />
+ )
+ }
+
+ ))
+ )}
+ {provided.placeholder}
+
+ )}
+
+ );
+}
+
+// ========== Main Kanban Board Component ==========
+
+export function KanbanBoard({
+ columns,
+ onDragEnd,
+ onItemClick,
+ renderItem,
+ className,
+ columnClassName,
+ itemClassName,
+ emptyColumnMessage,
+ isLoading = false,
+}: KanbanBoardProps) {
+ const handleDragEnd = useCallback(
+ (result: DropResult) => {
+ if (!result.destination) return;
+
+ const { source, destination } = result;
+ if (
+ source.droppableId === destination.droppableId &&
+ source.index === destination.index
+ ) {
+ return;
+ }
+
+ onDragEnd?.(result, source.droppableId, destination.droppableId);
+ },
+ [onDragEnd]
+ );
+
+ if (isLoading) {
+ return (
+
+ {columns.map((column) => (
+
+
+
+ {[1, 2, 3].map((i) => (
+
+ ))}
+
+
+ ))}
+
+ );
+ }
+
+ return (
+
+
+ {columns.map((column) => (
+
+ {/* Column Header */}
+
+
+ {column.icon}
+
{column.title}
+
+
+ {column.items.length}
+
+
+
+ {/* Column Content */}
+
+
+ ))}
+
+
+ );
+}
+
+// ========== Loop-specific Kanban ==========
+
+export interface LoopKanbanItem extends KanbanItem {
+ status: 'created' | 'running' | 'paused' | 'completed' | 'failed';
+ currentStep?: number;
+ totalSteps?: number;
+ prompt?: string;
+ tool?: string;
+}
+
+export function useLoopKanbanColumns(loopsByStatus: Record): KanbanColumn[] {
+ return [
+ {
+ id: 'created',
+ title: 'Pending',
+ items: loopsByStatus.created || [],
+ color: 'muted',
+ },
+ {
+ id: 'running',
+ title: 'Running',
+ items: loopsByStatus.running || [],
+ color: 'primary',
+ },
+ {
+ id: 'paused',
+ title: 'Paused',
+ items: loopsByStatus.paused || [],
+ color: 'warning',
+ },
+ {
+ id: 'completed',
+ title: 'Completed',
+ items: loopsByStatus.completed || [],
+ color: 'success',
+ },
+ {
+ id: 'failed',
+ title: 'Failed',
+ items: loopsByStatus.failed || [],
+ color: 'destructive',
+ },
+ ];
+}
+
+export default KanbanBoard;
diff --git a/ccw/frontend/src/components/shared/SessionCard.tsx b/ccw/frontend/src/components/shared/SessionCard.tsx
new file mode 100644
index 00000000..1851e30e
--- /dev/null
+++ b/ccw/frontend/src/components/shared/SessionCard.tsx
@@ -0,0 +1,287 @@
+// ========================================
+// SessionCard Component
+// ========================================
+// Session card with status badge and action menu
+
+import * as React from 'react';
+import { cn } from '@/lib/utils';
+import { Card, CardContent } from '@/components/ui/Card';
+import { Badge } from '@/components/ui/Badge';
+import { Button } from '@/components/ui/Button';
+import {
+ DropdownMenu,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+} from '@/components/ui/Dropdown';
+import {
+ Calendar,
+ ListChecks,
+ MoreVertical,
+ Eye,
+ Archive,
+ Trash2,
+ Play,
+ Pause,
+} from 'lucide-react';
+import type { SessionMetadata } from '@/types/store';
+
+export interface SessionCardProps {
+ /** Session data */
+ session: SessionMetadata;
+ /** Called when view action is triggered */
+ onView?: (sessionId: string) => void;
+ /** Called when archive action is triggered */
+ onArchive?: (sessionId: string) => void;
+ /** Called when delete action is triggered */
+ onDelete?: (sessionId: string) => void;
+ /** Called when card is clicked */
+ onClick?: (sessionId: string) => void;
+ /** Optional className */
+ className?: string;
+ /** Show actions dropdown */
+ showActions?: boolean;
+ /** Disabled state for actions */
+ actionsDisabled?: boolean;
+}
+
+// Status badge configuration
+const statusConfig: Record<
+ SessionMetadata['status'],
+ { label: string; variant: 'default' | 'secondary' | 'destructive' | 'success' | 'warning' | 'info' }
+> = {
+ planning: { label: 'Planning', variant: 'info' },
+ in_progress: { label: 'In Progress', variant: 'warning' },
+ completed: { label: 'Completed', variant: 'success' },
+ archived: { label: 'Archived', variant: 'secondary' },
+ paused: { label: 'Paused', variant: 'default' },
+};
+
+/**
+ * Format date to localized string
+ */
+function formatDate(dateString: string | undefined): string {
+ if (!dateString) return 'Unknown';
+
+ try {
+ const date = new Date(dateString);
+ return date.toLocaleDateString(undefined, {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ });
+ } catch {
+ return 'Invalid date';
+ }
+}
+
+/**
+ * Calculate progress percentage from tasks
+ */
+function calculateProgress(tasks: SessionMetadata['tasks']): {
+ completed: number;
+ total: number;
+ percentage: number;
+} {
+ if (!tasks || tasks.length === 0) {
+ return { completed: 0, total: 0, percentage: 0 };
+ }
+
+ const completed = tasks.filter((t) => t.status === 'completed').length;
+ const total = tasks.length;
+ const percentage = Math.round((completed / total) * 100);
+
+ return { completed, total, percentage };
+}
+
+/**
+ * SessionCard component for displaying session information
+ *
+ * @example
+ * ```tsx
+ * navigate(`/sessions/${id}`)}
+ * onArchive={(id) => archiveSession(id)}
+ * onDelete={(id) => deleteSession(id)}
+ * />
+ * ```
+ */
+export function SessionCard({
+ session,
+ onView,
+ onArchive,
+ onDelete,
+ onClick,
+ className,
+ showActions = true,
+ actionsDisabled = false,
+}: SessionCardProps) {
+ const { label: statusLabel, variant: statusVariant } = statusConfig[session.status] || {
+ label: 'Unknown',
+ variant: 'default' as const,
+ };
+
+ const progress = calculateProgress(session.tasks);
+ const isPlanning = session.status === 'planning';
+ const isArchived = session.status === 'archived' || session.location === 'archived';
+
+ const handleCardClick = (e: React.MouseEvent) => {
+ // Don't trigger if clicking on dropdown
+ if ((e.target as HTMLElement).closest('[data-radix-popper-content-wrapper]')) {
+ return;
+ }
+ onClick?.(session.session_id);
+ };
+
+ const handleAction = (
+ e: React.MouseEvent,
+ action: 'view' | 'archive' | 'delete'
+ ) => {
+ e.stopPropagation();
+ switch (action) {
+ case 'view':
+ onView?.(session.session_id);
+ break;
+ case 'archive':
+ onArchive?.(session.session_id);
+ break;
+ case 'delete':
+ onDelete?.(session.session_id);
+ break;
+ }
+ };
+
+ return (
+
+
+ {/* Header */}
+
+
+
+ {session.title || session.session_id}
+
+ {session.title && session.title !== session.session_id && (
+
+ {session.session_id}
+
+ )}
+
+
+
{statusLabel}
+ {showActions && (
+
+
+
+
+
+ handleAction(e, 'view')}>
+
+ View Details
+
+ {!isArchived && (
+ <>
+
+ handleAction(e, 'archive')}>
+
+ Archive
+
+ >
+ )}
+
+ handleAction(e, 'delete')}
+ className="text-destructive focus:text-destructive"
+ >
+
+ Delete
+
+
+
+ )}
+
+
+
+ {/* Meta info */}
+
+
+
+ {formatDate(session.created_at)}
+
+
+
+ {progress.total} tasks
+
+
+
+ {/* Progress bar (only show if not planning and has tasks) */}
+ {progress.total > 0 && !isPlanning && (
+
+
+ Progress
+
+ {progress.completed}/{progress.total} ({progress.percentage}%)
+
+
+
+
+ )}
+
+ {/* Description (if exists) */}
+ {session.description && (
+
+ {session.description}
+
+ )}
+
+
+ );
+}
+
+/**
+ * Skeleton loader for SessionCard
+ */
+export function SessionCardSkeleton({ className }: { className?: string }) {
+ return (
+
+
+
+
+
+
+
+ );
+}
diff --git a/ccw/frontend/src/components/shared/SkillCard.tsx b/ccw/frontend/src/components/shared/SkillCard.tsx
new file mode 100644
index 00000000..a72c605b
--- /dev/null
+++ b/ccw/frontend/src/components/shared/SkillCard.tsx
@@ -0,0 +1,257 @@
+// ========================================
+// SkillCard Component
+// ========================================
+// Card component for displaying skills with enable/disable toggle
+
+import { useState } from 'react';
+import {
+ Sparkles,
+ MoreVertical,
+ Info,
+ Settings,
+ Power,
+ PowerOff,
+ Tag,
+ User,
+} from 'lucide-react';
+import { cn } from '@/lib/utils';
+import { Card } from '@/components/ui/Card';
+import { Badge } from '@/components/ui/Badge';
+import { Button } from '@/components/ui/Button';
+import { Dropdown, DropdownTrigger, DropdownContent, DropdownItem } from '@/components/ui/Dropdown';
+import type { Skill } from '@/lib/api';
+
+// ========== Types ==========
+
+export interface SkillCardProps {
+ skill: Skill;
+ onToggle?: (skill: Skill, enabled: boolean) => void;
+ onClick?: (skill: Skill) => void;
+ onConfigure?: (skill: Skill) => void;
+ className?: string;
+ compact?: boolean;
+ showActions?: boolean;
+ isToggling?: boolean;
+}
+
+// ========== Source Badge ==========
+
+const sourceConfig: Record, { color: string; label: string }> = {
+ builtin: { color: 'default', label: 'Built-in' },
+ custom: { color: 'secondary', label: 'Custom' },
+ community: { color: 'outline', label: 'Community' },
+};
+
+export function SourceBadge({ source }: { source?: Skill['source'] }) {
+ const config = sourceConfig[source ?? 'builtin'];
+ return (
+
+ {config.label}
+
+ );
+}
+
+// ========== Main SkillCard Component ==========
+
+export function SkillCard({
+ skill,
+ onToggle,
+ onClick,
+ onConfigure,
+ className,
+ compact = false,
+ showActions = true,
+ isToggling = false,
+}: SkillCardProps) {
+ const [isMenuOpen, setIsMenuOpen] = useState(false);
+
+ const handleClick = () => {
+ if (!isMenuOpen) {
+ onClick?.(skill);
+ }
+ };
+
+ const handleToggle = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ onToggle?.(skill, !skill.enabled);
+ };
+
+ const handleConfigure = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ setIsMenuOpen(false);
+ onConfigure?.(skill);
+ };
+
+ if (compact) {
+ return (
+
+
+
+
+ {skill.name}
+
+
+
+
+ );
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
+
+
{skill.name}
+ {skill.version && (
+
v{skill.version}
+ )}
+
+
+ {showActions && (
+
+
+
+
+
+ onClick?.(skill)}>
+
+ View Details
+
+
+
+ Configure
+
+
+ {skill.enabled ? (
+ <>
+
+ Disable
+ >
+ ) : (
+ <>
+
+ Enable
+ >
+ )}
+
+
+
+ )}
+
+
+ {/* Description */}
+
+ {skill.description}
+
+
+ {/* Triggers */}
+ {skill.triggers && skill.triggers.length > 0 && (
+
+
+
+ Triggers
+
+
+ {skill.triggers.slice(0, 4).map((trigger) => (
+
+ {trigger}
+
+ ))}
+ {skill.triggers.length > 4 && (
+
+ +{skill.triggers.length - 4}
+
+ )}
+
+
+ )}
+
+ {/* Footer */}
+
+
+
+ {skill.category && (
+
+ {skill.category}
+
+ )}
+
+
+
+
+ {/* Author */}
+ {skill.author && (
+
+
+ {skill.author}
+
+ )}
+
+ );
+}
+
+export default SkillCard;
diff --git a/ccw/frontend/src/components/shared/StatCard.tsx b/ccw/frontend/src/components/shared/StatCard.tsx
new file mode 100644
index 00000000..3ec762b1
--- /dev/null
+++ b/ccw/frontend/src/components/shared/StatCard.tsx
@@ -0,0 +1,161 @@
+// ========================================
+// StatCard Component
+// ========================================
+// Reusable stat card for dashboard metrics
+
+import * as React from 'react';
+import { cva, type VariantProps } from 'class-variance-authority';
+import { cn } from '@/lib/utils';
+import { Card, CardContent } from '@/components/ui/Card';
+import { TrendingUp, TrendingDown, Minus, type LucideIcon } from 'lucide-react';
+
+const statCardVariants = cva(
+ 'transition-all duration-200 hover:shadow-md',
+ {
+ variants: {
+ variant: {
+ default: 'border-border',
+ primary: 'border-primary/30 bg-primary/5',
+ success: 'border-success/30 bg-success/5',
+ warning: 'border-warning/30 bg-warning/5',
+ danger: 'border-destructive/30 bg-destructive/5',
+ info: 'border-info/30 bg-info/5',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ },
+ }
+);
+
+const iconContainerVariants = cva(
+ 'flex h-10 w-10 items-center justify-center rounded-lg',
+ {
+ variants: {
+ variant: {
+ default: 'bg-muted text-muted-foreground',
+ primary: 'bg-primary/10 text-primary',
+ success: 'bg-success/10 text-success',
+ warning: 'bg-warning/10 text-warning',
+ danger: 'bg-destructive/10 text-destructive',
+ info: 'bg-info/10 text-info',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ },
+ }
+);
+
+export interface StatCardProps
+ extends React.HTMLAttributes,
+ VariantProps {
+ /** Card title */
+ title: string;
+ /** Stat value to display */
+ value: number | string;
+ /** Optional icon component */
+ icon?: LucideIcon;
+ /** Optional trend direction */
+ trend?: 'up' | 'down' | 'neutral';
+ /** Optional trend value (e.g., "+12%") */
+ trendValue?: string;
+ /** Loading state */
+ isLoading?: boolean;
+ /** Optional description */
+ description?: string;
+}
+
+/**
+ * StatCard component for displaying dashboard metrics
+ *
+ * @example
+ * ```tsx
+ *
+ * ```
+ */
+export function StatCard({
+ className,
+ variant,
+ title,
+ value,
+ icon: Icon,
+ trend,
+ trendValue,
+ isLoading = false,
+ description,
+ ...props
+}: StatCardProps) {
+ const TrendIcon = trend === 'up' ? TrendingUp : trend === 'down' ? TrendingDown : Minus;
+ const trendColor =
+ trend === 'up'
+ ? 'text-success'
+ : trend === 'down'
+ ? 'text-destructive'
+ : 'text-muted-foreground';
+
+ return (
+
+
+
+
+
+ {title}
+
+
+ {isLoading ? (
+
+ ) : (
+
+ {typeof value === 'number' ? value.toLocaleString() : value}
+
+ )}
+ {trend && trendValue && !isLoading && (
+
+
+ {trendValue}
+
+ )}
+
+ {description && (
+
+ {description}
+
+ )}
+
+ {Icon && (
+
+
+
+ )}
+
+
+
+ );
+}
+
+/**
+ * Skeleton loader for StatCard
+ */
+export function StatCardSkeleton({ className }: { className?: string }) {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/ccw/frontend/src/components/ui/Badge.tsx b/ccw/frontend/src/components/ui/Badge.tsx
new file mode 100644
index 00000000..e74d67d4
--- /dev/null
+++ b/ccw/frontend/src/components/ui/Badge.tsx
@@ -0,0 +1,42 @@
+import * as React from "react";
+import { cva, type VariantProps } from "class-variance-authority";
+import { cn } from "@/lib/utils";
+
+const badgeVariants = cva(
+ "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
+ {
+ variants: {
+ variant: {
+ default:
+ "border-transparent bg-primary text-primary-foreground",
+ secondary:
+ "border-transparent bg-secondary text-secondary-foreground",
+ destructive:
+ "border-transparent bg-destructive text-destructive-foreground",
+ outline:
+ "text-foreground",
+ success:
+ "border-transparent bg-success text-white",
+ warning:
+ "border-transparent bg-warning text-white",
+ info:
+ "border-transparent bg-info text-white",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+);
+
+export interface BadgeProps
+ extends React.HTMLAttributes,
+ VariantProps {}
+
+function Badge({ className, variant, ...props }: BadgeProps) {
+ return (
+
+ );
+}
+
+export { Badge, badgeVariants };
diff --git a/ccw/frontend/src/components/ui/Button.tsx b/ccw/frontend/src/components/ui/Button.tsx
new file mode 100644
index 00000000..274ce73a
--- /dev/null
+++ b/ccw/frontend/src/components/ui/Button.tsx
@@ -0,0 +1,56 @@
+import * as React from "react";
+import { cva, type VariantProps } from "class-variance-authority";
+import { cn } from "@/lib/utils";
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
+ {
+ variants: {
+ variant: {
+ default:
+ "bg-primary text-primary-foreground hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
+ outline:
+ "border border-border bg-background hover:bg-accent hover:text-accent-foreground",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost:
+ "hover:bg-accent hover:text-accent-foreground",
+ link:
+ "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-10 px-4 py-2",
+ sm: "h-9 rounded-md px-3",
+ lg: "h-11 rounded-md px-8",
+ icon: "h-10 w-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+);
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean;
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ return (
+
+ );
+ }
+);
+Button.displayName = "Button";
+
+export { Button, buttonVariants };
diff --git a/ccw/frontend/src/components/ui/Card.tsx b/ccw/frontend/src/components/ui/Card.tsx
new file mode 100644
index 00000000..532612cf
--- /dev/null
+++ b/ccw/frontend/src/components/ui/Card.tsx
@@ -0,0 +1,85 @@
+import * as React from "react";
+import { cn } from "@/lib/utils";
+
+const Card = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+Card.displayName = "Card";
+
+const CardHeader = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardHeader.displayName = "CardHeader";
+
+const CardTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardTitle.displayName = "CardTitle";
+
+const CardDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardDescription.displayName = "CardDescription";
+
+const CardContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardContent.displayName = "CardContent";
+
+const CardFooter = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardFooter.displayName = "CardFooter";
+
+export {
+ Card,
+ CardHeader,
+ CardFooter,
+ CardTitle,
+ CardDescription,
+ CardContent,
+};
diff --git a/ccw/frontend/src/components/ui/Dialog.tsx b/ccw/frontend/src/components/ui/Dialog.tsx
new file mode 100644
index 00000000..d0b130da
--- /dev/null
+++ b/ccw/frontend/src/components/ui/Dialog.tsx
@@ -0,0 +1,119 @@
+import * as React from "react";
+import * as DialogPrimitive from "@radix-ui/react-dialog";
+import { X } from "lucide-react";
+import { cn } from "@/lib/utils";
+
+const Dialog = DialogPrimitive.Root;
+
+const DialogTrigger = DialogPrimitive.Trigger;
+
+const DialogPortal = DialogPrimitive.Portal;
+
+const DialogClose = DialogPrimitive.Close;
+
+const DialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
+
+const DialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+ {children}
+
+
+ Close
+
+
+
+));
+DialogContent.displayName = DialogPrimitive.Content.displayName;
+
+const DialogHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+);
+DialogHeader.displayName = "DialogHeader";
+
+const DialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+);
+DialogFooter.displayName = "DialogFooter";
+
+const DialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DialogTitle.displayName = DialogPrimitive.Title.displayName;
+
+const DialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DialogDescription.displayName = DialogPrimitive.Description.displayName;
+
+export {
+ Dialog,
+ DialogPortal,
+ DialogOverlay,
+ DialogClose,
+ DialogTrigger,
+ DialogContent,
+ DialogHeader,
+ DialogFooter,
+ DialogTitle,
+ DialogDescription,
+};
diff --git a/ccw/frontend/src/components/ui/Dropdown.tsx b/ccw/frontend/src/components/ui/Dropdown.tsx
new file mode 100644
index 00000000..4fcd1c07
--- /dev/null
+++ b/ccw/frontend/src/components/ui/Dropdown.tsx
@@ -0,0 +1,197 @@
+import * as React from "react";
+import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
+import { Check, ChevronRight, Circle } from "lucide-react";
+import { cn } from "@/lib/utils";
+
+const DropdownMenu = DropdownMenuPrimitive.Root;
+
+const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
+
+const DropdownMenuGroup = DropdownMenuPrimitive.Group;
+
+const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
+
+const DropdownMenuSub = DropdownMenuPrimitive.Sub;
+
+const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
+
+const DropdownMenuSubTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean;
+ }
+>(({ className, inset, children, ...props }, ref) => (
+
+ {children}
+
+
+));
+DropdownMenuSubTrigger.displayName =
+ DropdownMenuPrimitive.SubTrigger.displayName;
+
+const DropdownMenuSubContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DropdownMenuSubContent.displayName =
+ DropdownMenuPrimitive.SubContent.displayName;
+
+const DropdownMenuContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, sideOffset = 4, ...props }, ref) => (
+
+
+
+));
+DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
+
+const DropdownMenuItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean;
+ }
+>(({ className, inset, ...props }, ref) => (
+
+));
+DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
+
+const DropdownMenuCheckboxItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, checked, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+));
+DropdownMenuCheckboxItem.displayName =
+ DropdownMenuPrimitive.CheckboxItem.displayName;
+
+const DropdownMenuRadioItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+));
+DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
+
+const DropdownMenuLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean;
+ }
+>(({ className, inset, ...props }, ref) => (
+
+));
+DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
+
+const DropdownMenuSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
+
+const DropdownMenuShortcut = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => {
+ return (
+
+ );
+};
+DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
+
+export {
+ DropdownMenu,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuCheckboxItem,
+ DropdownMenuRadioItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuGroup,
+ DropdownMenuPortal,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
+ DropdownMenuRadioGroup,
+};
diff --git a/ccw/frontend/src/components/ui/Input.tsx b/ccw/frontend/src/components/ui/Input.tsx
new file mode 100644
index 00000000..0c2fcd29
--- /dev/null
+++ b/ccw/frontend/src/components/ui/Input.tsx
@@ -0,0 +1,27 @@
+import * as React from "react";
+import { cn } from "@/lib/utils";
+
+export interface InputProps
+ extends React.InputHTMLAttributes {
+ error?: boolean;
+}
+
+const Input = React.forwardRef(
+ ({ className, type, error, ...props }, ref) => {
+ return (
+
+ );
+ }
+);
+Input.displayName = "Input";
+
+export { Input };
diff --git a/ccw/frontend/src/components/ui/Select.tsx b/ccw/frontend/src/components/ui/Select.tsx
new file mode 100644
index 00000000..32d07c6e
--- /dev/null
+++ b/ccw/frontend/src/components/ui/Select.tsx
@@ -0,0 +1,156 @@
+import * as React from "react";
+import * as SelectPrimitive from "@radix-ui/react-select";
+import { Check, ChevronDown, ChevronUp } from "lucide-react";
+import { cn } from "@/lib/utils";
+
+const Select = SelectPrimitive.Root;
+
+const SelectGroup = SelectPrimitive.Group;
+
+const SelectValue = SelectPrimitive.Value;
+
+const SelectTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+ span]:line-clamp-1",
+ className
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+
+));
+SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
+
+const SelectScrollUpButton = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+));
+SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
+
+const SelectScrollDownButton = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+));
+SelectScrollDownButton.displayName =
+ SelectPrimitive.ScrollDownButton.displayName;
+
+const SelectContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, position = "popper", ...props }, ref) => (
+
+
+
+
+ {children}
+
+
+
+
+));
+SelectContent.displayName = SelectPrimitive.Content.displayName;
+
+const SelectLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+SelectLabel.displayName = SelectPrimitive.Label.displayName;
+
+const SelectItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+));
+SelectItem.displayName = SelectPrimitive.Item.displayName;
+
+const SelectSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
+
+export {
+ Select,
+ SelectGroup,
+ SelectValue,
+ SelectTrigger,
+ SelectContent,
+ SelectLabel,
+ SelectItem,
+ SelectSeparator,
+ SelectScrollUpButton,
+ SelectScrollDownButton,
+};
diff --git a/ccw/frontend/src/components/ui/Tabs.tsx b/ccw/frontend/src/components/ui/Tabs.tsx
new file mode 100644
index 00000000..cc2b747b
--- /dev/null
+++ b/ccw/frontend/src/components/ui/Tabs.tsx
@@ -0,0 +1,52 @@
+import * as React from "react";
+import * as TabsPrimitive from "@radix-ui/react-tabs";
+import { cn } from "@/lib/utils";
+
+const Tabs = TabsPrimitive.Root;
+
+const TabsList = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+TabsList.displayName = TabsPrimitive.List.displayName;
+
+const TabsTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
+
+const TabsContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+TabsContent.displayName = TabsPrimitive.Content.displayName;
+
+export { Tabs, TabsList, TabsTrigger, TabsContent };
diff --git a/ccw/frontend/src/components/ui/Toast.tsx b/ccw/frontend/src/components/ui/Toast.tsx
new file mode 100644
index 00000000..e4aa43a8
--- /dev/null
+++ b/ccw/frontend/src/components/ui/Toast.tsx
@@ -0,0 +1,128 @@
+import * as React from "react";
+import * as ToastPrimitives from "@radix-ui/react-toast";
+import { cva, type VariantProps } from "class-variance-authority";
+import { X } from "lucide-react";
+import { cn } from "@/lib/utils";
+
+const ToastProvider = ToastPrimitives.Provider;
+
+const ToastViewport = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
+
+const toastVariants = cva(
+ "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
+ {
+ variants: {
+ variant: {
+ default: "border-border bg-card text-card-foreground",
+ success: "border-success bg-success text-white",
+ warning: "border-warning bg-warning text-white",
+ error: "border-destructive bg-destructive text-destructive-foreground",
+ info: "border-info bg-info text-white",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+);
+
+const Toast = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef &
+ VariantProps
+>(({ className, variant, ...props }, ref) => {
+ return (
+
+ );
+});
+Toast.displayName = ToastPrimitives.Root.displayName;
+
+const ToastAction = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+ToastAction.displayName = ToastPrimitives.Action.displayName;
+
+const ToastClose = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+));
+ToastClose.displayName = ToastPrimitives.Close.displayName;
+
+const ToastTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+ToastTitle.displayName = ToastPrimitives.Title.displayName;
+
+const ToastDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+ToastDescription.displayName = ToastPrimitives.Description.displayName;
+
+type ToastProps = React.ComponentPropsWithoutRef;
+
+type ToastActionElement = React.ReactElement;
+
+export {
+ type ToastProps,
+ type ToastActionElement,
+ ToastProvider,
+ ToastViewport,
+ Toast,
+ ToastTitle,
+ ToastDescription,
+ ToastClose,
+ ToastAction,
+};
diff --git a/ccw/frontend/src/components/ui/index.ts b/ccw/frontend/src/components/ui/index.ts
new file mode 100644
index 00000000..28c7354e
--- /dev/null
+++ b/ccw/frontend/src/components/ui/index.ts
@@ -0,0 +1,87 @@
+// UI Component Library - Barrel Export
+// All components follow shadcn/ui patterns with Radix UI primitives and Tailwind CSS
+
+// Button
+export { Button, buttonVariants } from "./Button";
+export type { ButtonProps } from "./Button";
+
+// Input
+export { Input } from "./Input";
+export type { InputProps } from "./Input";
+
+// Select (Radix)
+export {
+ Select,
+ SelectGroup,
+ SelectValue,
+ SelectTrigger,
+ SelectContent,
+ SelectLabel,
+ SelectItem,
+ SelectSeparator,
+ SelectScrollUpButton,
+ SelectScrollDownButton,
+} from "./Select";
+
+// Dialog (Radix)
+export {
+ Dialog,
+ DialogPortal,
+ DialogOverlay,
+ DialogClose,
+ DialogTrigger,
+ DialogContent,
+ DialogHeader,
+ DialogFooter,
+ DialogTitle,
+ DialogDescription,
+} from "./Dialog";
+
+// Dropdown (Radix)
+export {
+ DropdownMenu,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuCheckboxItem,
+ DropdownMenuRadioItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuGroup,
+ DropdownMenuPortal,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
+ DropdownMenuRadioGroup,
+} from "./Dropdown";
+
+// Tabs (Radix)
+export { Tabs, TabsList, TabsTrigger, TabsContent } from "./Tabs";
+
+// Card
+export {
+ Card,
+ CardHeader,
+ CardFooter,
+ CardTitle,
+ CardDescription,
+ CardContent,
+} from "./Card";
+
+// Badge
+export { Badge, badgeVariants } from "./Badge";
+export type { BadgeProps } from "./Badge";
+
+// Toast (Radix)
+export {
+ type ToastProps,
+ type ToastActionElement,
+ ToastProvider,
+ ToastViewport,
+ Toast,
+ ToastTitle,
+ ToastDescription,
+ ToastClose,
+ ToastAction,
+} from "./Toast";
diff --git a/ccw/frontend/src/hooks/index.ts b/ccw/frontend/src/hooks/index.ts
new file mode 100644
index 00000000..4dbeb077
--- /dev/null
+++ b/ccw/frontend/src/hooks/index.ts
@@ -0,0 +1,121 @@
+// ========================================
+// Hooks Barrel Export
+// ========================================
+// Re-export all custom hooks for convenient imports
+
+export { useTheme } from './useTheme';
+export type { UseThemeReturn } from './useTheme';
+
+export { useSession } from './useSession';
+export type { UseSessionReturn } from './useSession';
+
+export { useConfig } from './useConfig';
+export type { UseConfigReturn } from './useConfig';
+
+export { useNotifications } from './useNotifications';
+export type { UseNotificationsReturn, ToastOptions } from './useNotifications';
+
+export { useDashboardStats, usePrefetchDashboardStats, dashboardStatsKeys } from './useDashboardStats';
+export type { UseDashboardStatsOptions, UseDashboardStatsReturn } from './useDashboardStats';
+
+export {
+ useSessions,
+ useCreateSession,
+ useUpdateSession,
+ useArchiveSession,
+ useDeleteSession,
+ useSessionMutations,
+ usePrefetchSessions,
+ sessionsKeys,
+} from './useSessions';
+export type {
+ SessionsFilter,
+ UseSessionsOptions,
+ UseSessionsReturn,
+ UseCreateSessionReturn,
+ UseUpdateSessionReturn,
+ UseArchiveSessionReturn,
+ UseDeleteSessionReturn,
+} from './useSessions';
+
+// ========== Loops ==========
+export {
+ useLoops,
+ useLoop,
+ useCreateLoop,
+ useUpdateLoopStatus,
+ useDeleteLoop,
+ useLoopMutations,
+ loopsKeys,
+} from './useLoops';
+export type {
+ LoopsFilter,
+ UseLoopsOptions,
+ UseLoopsReturn,
+ UseCreateLoopReturn,
+ UseUpdateLoopStatusReturn,
+ UseDeleteLoopReturn,
+} from './useLoops';
+
+// ========== Issues ==========
+export {
+ useIssues,
+ useIssueQueue,
+ useCreateIssue,
+ useUpdateIssue,
+ useDeleteIssue,
+ useIssueMutations,
+ issuesKeys,
+} from './useIssues';
+export type {
+ IssuesFilter,
+ UseIssuesOptions,
+ UseIssuesReturn,
+ UseCreateIssueReturn,
+ UseUpdateIssueReturn,
+ UseDeleteIssueReturn,
+} from './useIssues';
+
+// ========== Skills ==========
+export {
+ useSkills,
+ useToggleSkill,
+ useSkillMutations,
+ skillsKeys,
+} from './useSkills';
+export type {
+ SkillsFilter,
+ UseSkillsOptions,
+ UseSkillsReturn,
+ UseToggleSkillReturn,
+} from './useSkills';
+
+// ========== Commands ==========
+export {
+ useCommands,
+ useCommandSearch,
+ commandsKeys,
+} from './useCommands';
+export type {
+ CommandsFilter,
+ UseCommandsOptions,
+ UseCommandsReturn,
+} from './useCommands';
+
+// ========== Memory ==========
+export {
+ useMemory,
+ useCreateMemory,
+ useUpdateMemory,
+ useDeleteMemory,
+ useMemoryMutations,
+ memoryKeys,
+} from './useMemory';
+export type {
+ MemoryFilter,
+ UseMemoryOptions,
+ UseMemoryReturn,
+ UseCreateMemoryReturn,
+ UseUpdateMemoryReturn,
+ UseDeleteMemoryReturn,
+} from './useMemory';
diff --git a/ccw/frontend/src/hooks/useCommands.ts b/ccw/frontend/src/hooks/useCommands.ts
new file mode 100644
index 00000000..3fe82ca2
--- /dev/null
+++ b/ccw/frontend/src/hooks/useCommands.ts
@@ -0,0 +1,128 @@
+// ========================================
+// useCommands Hook
+// ========================================
+// TanStack Query hooks for commands management
+
+import { useQuery, useQueryClient } from '@tanstack/react-query';
+import {
+ fetchCommands,
+ type Command,
+} from '../lib/api';
+
+// Query key factory
+export const commandsKeys = {
+ all: ['commands'] as const,
+ lists: () => [...commandsKeys.all, 'list'] as const,
+ list: (filters?: CommandsFilter) => [...commandsKeys.lists(), filters] as const,
+};
+
+// Default stale time: 10 minutes (commands are static)
+const STALE_TIME = 10 * 60 * 1000;
+
+export interface CommandsFilter {
+ search?: string;
+ category?: string;
+ source?: Command['source'];
+}
+
+export interface UseCommandsOptions {
+ filter?: CommandsFilter;
+ staleTime?: number;
+ enabled?: boolean;
+}
+
+export interface UseCommandsReturn {
+ commands: Command[];
+ categories: string[];
+ commandsByCategory: Record;
+ totalCount: number;
+ isLoading: boolean;
+ isFetching: boolean;
+ error: Error | null;
+ refetch: () => Promise;
+ invalidate: () => Promise;
+}
+
+/**
+ * Hook for fetching and filtering commands
+ */
+export function useCommands(options: UseCommandsOptions = {}): UseCommandsReturn {
+ const { filter, staleTime = STALE_TIME, enabled = true } = options;
+ const queryClient = useQueryClient();
+
+ const query = useQuery({
+ queryKey: commandsKeys.list(filter),
+ queryFn: fetchCommands,
+ staleTime,
+ enabled,
+ retry: 2,
+ });
+
+ const allCommands = query.data?.commands ?? [];
+
+ // Apply filters
+ const filteredCommands = (() => {
+ let commands = allCommands;
+
+ if (filter?.search) {
+ const searchLower = filter.search.toLowerCase();
+ commands = commands.filter(
+ (c) =>
+ c.name.toLowerCase().includes(searchLower) ||
+ c.description.toLowerCase().includes(searchLower) ||
+ c.aliases?.some((a) => a.toLowerCase().includes(searchLower))
+ );
+ }
+
+ if (filter?.category) {
+ commands = commands.filter((c) => c.category === filter.category);
+ }
+
+ if (filter?.source) {
+ commands = commands.filter((c) => c.source === filter.source);
+ }
+
+ return commands;
+ })();
+
+ // Group by category
+ const commandsByCategory: Record = {};
+ const categories = new Set();
+
+ for (const command of allCommands) {
+ const category = command.category || 'Uncategorized';
+ categories.add(category);
+ if (!commandsByCategory[category]) {
+ commandsByCategory[category] = [];
+ }
+ commandsByCategory[category].push(command);
+ }
+
+ const refetch = async () => {
+ await query.refetch();
+ };
+
+ const invalidate = async () => {
+ await queryClient.invalidateQueries({ queryKey: commandsKeys.all });
+ };
+
+ return {
+ commands: filteredCommands,
+ categories: Array.from(categories).sort(),
+ commandsByCategory,
+ totalCount: allCommands.length,
+ isLoading: query.isLoading,
+ isFetching: query.isFetching,
+ error: query.error,
+ refetch,
+ invalidate,
+ };
+}
+
+/**
+ * Hook to search commands by name or alias
+ */
+export function useCommandSearch(searchTerm: string) {
+ const { commands } = useCommands({ filter: { search: searchTerm } });
+ return commands;
+}
diff --git a/ccw/frontend/src/hooks/useConfig.ts b/ccw/frontend/src/hooks/useConfig.ts
new file mode 100644
index 00000000..9eedf7e4
--- /dev/null
+++ b/ccw/frontend/src/hooks/useConfig.ts
@@ -0,0 +1,143 @@
+// ========================================
+// useConfig Hook
+// ========================================
+// Convenient hook for configuration management
+
+import { useCallback } from 'react';
+import {
+ useConfigStore,
+ selectCliTools,
+ selectDefaultCliTool,
+ selectApiEndpoints,
+ selectUserPreferences,
+ selectFeatureFlags,
+ getFirstEnabledCliTool,
+} from '../stores/configStore';
+import type { CliToolConfig, ApiEndpoints, UserPreferences, ConfigState } from '../types/store';
+
+export interface UseConfigReturn {
+ /** CLI tools configuration */
+ cliTools: Record;
+ /** Default CLI tool ID */
+ defaultCliTool: string;
+ /** First enabled CLI tool (fallback) */
+ firstEnabledTool: string;
+ /** API endpoints */
+ apiEndpoints: ApiEndpoints;
+ /** User preferences */
+ userPreferences: UserPreferences;
+ /** Feature flags */
+ featureFlags: Record;
+ /** Update CLI tool config */
+ updateCliTool: (toolId: string, updates: Partial) => void;
+ /** Set default CLI tool */
+ setDefaultCliTool: (toolId: string) => void;
+ /** Update user preferences */
+ setUserPreferences: (prefs: Partial) => void;
+ /** Reset user preferences to defaults */
+ resetUserPreferences: () => void;
+ /** Set a feature flag */
+ setFeatureFlag: (flag: string, enabled: boolean) => void;
+ /** Check if a feature is enabled */
+ isFeatureEnabled: (flag: string) => boolean;
+ /** Load full config */
+ loadConfig: (config: Partial) => void;
+}
+
+/**
+ * Hook for managing configuration state
+ * @returns Config state and actions
+ *
+ * @example
+ * ```tsx
+ * const { cliTools, defaultCliTool, userPreferences, setUserPreferences } = useConfig();
+ *
+ * return (
+ *
+ * );
+ * ```
+ */
+export function useConfig(): UseConfigReturn {
+ const cliTools = useConfigStore(selectCliTools);
+ const defaultCliTool = useConfigStore(selectDefaultCliTool);
+ const apiEndpoints = useConfigStore(selectApiEndpoints);
+ const userPreferences = useConfigStore(selectUserPreferences);
+ const featureFlags = useConfigStore(selectFeatureFlags);
+
+ // Actions
+ const updateCliToolAction = useConfigStore((state) => state.updateCliTool);
+ const setDefaultCliToolAction = useConfigStore((state) => state.setDefaultCliTool);
+ const setUserPreferencesAction = useConfigStore((state) => state.setUserPreferences);
+ const resetUserPreferencesAction = useConfigStore((state) => state.resetUserPreferences);
+ const setFeatureFlagAction = useConfigStore((state) => state.setFeatureFlag);
+ const loadConfigAction = useConfigStore((state) => state.loadConfig);
+
+ // Computed values
+ const firstEnabledTool = getFirstEnabledCliTool(cliTools);
+
+ // Callbacks
+ const updateCliTool = useCallback(
+ (toolId: string, updates: Partial) => {
+ updateCliToolAction(toolId, updates);
+ },
+ [updateCliToolAction]
+ );
+
+ const setDefaultCliTool = useCallback(
+ (toolId: string) => {
+ setDefaultCliToolAction(toolId);
+ },
+ [setDefaultCliToolAction]
+ );
+
+ const setUserPreferences = useCallback(
+ (prefs: Partial) => {
+ setUserPreferencesAction(prefs);
+ },
+ [setUserPreferencesAction]
+ );
+
+ const resetUserPreferences = useCallback(() => {
+ resetUserPreferencesAction();
+ }, [resetUserPreferencesAction]);
+
+ const setFeatureFlag = useCallback(
+ (flag: string, enabled: boolean) => {
+ setFeatureFlagAction(flag, enabled);
+ },
+ [setFeatureFlagAction]
+ );
+
+ const isFeatureEnabled = useCallback(
+ (flag: string): boolean => {
+ return featureFlags[flag] ?? false;
+ },
+ [featureFlags]
+ );
+
+ const loadConfig = useCallback(
+ (config: Partial) => {
+ loadConfigAction(config);
+ },
+ [loadConfigAction]
+ );
+
+ return {
+ cliTools,
+ defaultCliTool,
+ firstEnabledTool,
+ apiEndpoints,
+ userPreferences,
+ featureFlags,
+ updateCliTool,
+ setDefaultCliTool,
+ setUserPreferences,
+ resetUserPreferences,
+ setFeatureFlag,
+ isFeatureEnabled,
+ loadConfig,
+ };
+}
diff --git a/ccw/frontend/src/hooks/useDashboardStats.ts b/ccw/frontend/src/hooks/useDashboardStats.ts
new file mode 100644
index 00000000..f3ef5367
--- /dev/null
+++ b/ccw/frontend/src/hooks/useDashboardStats.ts
@@ -0,0 +1,111 @@
+// ========================================
+// useDashboardStats Hook
+// ========================================
+// TanStack Query hook for dashboard statistics
+
+import { useQuery, useQueryClient } from '@tanstack/react-query';
+import { fetchDashboardStats, type DashboardStats } from '../lib/api';
+
+// Query key factory
+export const dashboardStatsKeys = {
+ all: ['dashboardStats'] as const,
+ detail: () => [...dashboardStatsKeys.all, 'detail'] as const,
+};
+
+// Default stale time: 30 seconds
+const STALE_TIME = 30 * 1000;
+
+export interface UseDashboardStatsOptions {
+ /** Override default stale time (ms) */
+ staleTime?: number;
+ /** Enable/disable the query */
+ enabled?: boolean;
+ /** Refetch interval (ms), 0 to disable */
+ refetchInterval?: number;
+}
+
+export interface UseDashboardStatsReturn {
+ /** Dashboard statistics data */
+ stats: DashboardStats | undefined;
+ /** Loading state for initial fetch */
+ isLoading: boolean;
+ /** Fetching state (initial or refetch) */
+ isFetching: boolean;
+ /** Error object if query failed */
+ error: Error | null;
+ /** Whether data is stale */
+ isStale: boolean;
+ /** Manually refetch data */
+ refetch: () => Promise;
+ /** Invalidate and refetch stats */
+ invalidate: () => Promise;
+}
+
+/**
+ * Hook for fetching and managing dashboard statistics
+ *
+ * @example
+ * ```tsx
+ * const { stats, isLoading, error } = useDashboardStats();
+ *
+ * if (isLoading) return ;
+ * if (error) return ;
+ *
+ * return (
+ *
+ *
+ *
+ *
+ * );
+ * ```
+ */
+export function useDashboardStats(
+ options: UseDashboardStatsOptions = {}
+): UseDashboardStatsReturn {
+ const { staleTime = STALE_TIME, enabled = true, refetchInterval = 0 } = options;
+ const queryClient = useQueryClient();
+
+ const query = useQuery({
+ queryKey: dashboardStatsKeys.detail(),
+ queryFn: fetchDashboardStats,
+ staleTime,
+ enabled,
+ refetchInterval: refetchInterval > 0 ? refetchInterval : false,
+ retry: 2,
+ retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000),
+ });
+
+ const refetch = async () => {
+ await query.refetch();
+ };
+
+ const invalidate = async () => {
+ await queryClient.invalidateQueries({ queryKey: dashboardStatsKeys.all });
+ };
+
+ return {
+ stats: query.data,
+ isLoading: query.isLoading,
+ isFetching: query.isFetching,
+ error: query.error,
+ isStale: query.isStale,
+ refetch,
+ invalidate,
+ };
+}
+
+/**
+ * Hook to prefetch dashboard stats
+ * Use this to prefetch data before navigating to home page
+ */
+export function usePrefetchDashboardStats() {
+ const queryClient = useQueryClient();
+
+ return () => {
+ queryClient.prefetchQuery({
+ queryKey: dashboardStatsKeys.detail(),
+ queryFn: fetchDashboardStats,
+ staleTime: STALE_TIME,
+ });
+ };
+}
diff --git a/ccw/frontend/src/hooks/useFlows.ts b/ccw/frontend/src/hooks/useFlows.ts
new file mode 100644
index 00000000..ff46556c
--- /dev/null
+++ b/ccw/frontend/src/hooks/useFlows.ts
@@ -0,0 +1,295 @@
+// ========================================
+// useFlows Hook
+// ========================================
+// TanStack Query hooks for flow API operations
+
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import type { Flow } from '../types/flow';
+
+// API base URL
+const API_BASE = '/api/orchestrator';
+
+// Query keys
+export const flowKeys = {
+ all: ['flows'] as const,
+ lists: () => [...flowKeys.all, 'list'] as const,
+ list: (filters?: Record) => [...flowKeys.lists(), filters] as const,
+ details: () => [...flowKeys.all, 'detail'] as const,
+ detail: (id: string) => [...flowKeys.details(), id] as const,
+};
+
+// API response types
+interface FlowsListResponse {
+ flows: Flow[];
+ total: number;
+}
+
+interface ExecutionStartResponse {
+ execId: string;
+ flowId: string;
+ status: 'running';
+ startedAt: string;
+}
+
+interface ExecutionControlResponse {
+ execId: string;
+ status: 'paused' | 'running' | 'stopped';
+ message: string;
+}
+
+// ========== Fetch Functions ==========
+
+async function fetchFlows(): Promise {
+ const response = await fetch(`${API_BASE}/flows`);
+ if (!response.ok) {
+ throw new Error(`Failed to fetch flows: ${response.statusText}`);
+ }
+ return response.json();
+}
+
+async function fetchFlow(id: string): Promise {
+ const response = await fetch(`${API_BASE}/flows/${id}`);
+ if (!response.ok) {
+ throw new Error(`Failed to fetch flow: ${response.statusText}`);
+ }
+ return response.json();
+}
+
+async function createFlow(flow: Omit): Promise {
+ const response = await fetch(`${API_BASE}/flows`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(flow),
+ });
+ if (!response.ok) {
+ throw new Error(`Failed to create flow: ${response.statusText}`);
+ }
+ return response.json();
+}
+
+async function updateFlow(id: string, flow: Partial): Promise {
+ const response = await fetch(`${API_BASE}/flows/${id}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(flow),
+ });
+ if (!response.ok) {
+ throw new Error(`Failed to update flow: ${response.statusText}`);
+ }
+ return response.json();
+}
+
+async function deleteFlow(id: string): Promise {
+ const response = await fetch(`${API_BASE}/flows/${id}`, {
+ method: 'DELETE',
+ });
+ if (!response.ok) {
+ throw new Error(`Failed to delete flow: ${response.statusText}`);
+ }
+}
+
+async function duplicateFlow(id: string): Promise {
+ const response = await fetch(`${API_BASE}/flows/${id}/duplicate`, {
+ method: 'POST',
+ });
+ if (!response.ok) {
+ throw new Error(`Failed to duplicate flow: ${response.statusText}`);
+ }
+ return response.json();
+}
+
+// ========== Execution Functions ==========
+
+async function executeFlow(flowId: string): Promise {
+ const response = await fetch(`${API_BASE}/flows/${flowId}/execute`, {
+ method: 'POST',
+ });
+ if (!response.ok) {
+ throw new Error(`Failed to execute flow: ${response.statusText}`);
+ }
+ return response.json();
+}
+
+async function pauseExecution(execId: string): Promise {
+ const response = await fetch(`${API_BASE}/executions/${execId}/pause`, {
+ method: 'POST',
+ });
+ if (!response.ok) {
+ throw new Error(`Failed to pause execution: ${response.statusText}`);
+ }
+ return response.json();
+}
+
+async function resumeExecution(execId: string): Promise {
+ const response = await fetch(`${API_BASE}/executions/${execId}/resume`, {
+ method: 'POST',
+ });
+ if (!response.ok) {
+ throw new Error(`Failed to resume execution: ${response.statusText}`);
+ }
+ return response.json();
+}
+
+async function stopExecution(execId: string): Promise {
+ const response = await fetch(`${API_BASE}/executions/${execId}/stop`, {
+ method: 'POST',
+ });
+ if (!response.ok) {
+ throw new Error(`Failed to stop execution: ${response.statusText}`);
+ }
+ return response.json();
+}
+
+// ========== Query Hooks ==========
+
+/**
+ * Fetch all flows
+ */
+export function useFlows() {
+ return useQuery({
+ queryKey: flowKeys.lists(),
+ queryFn: fetchFlows,
+ staleTime: 30000, // 30 seconds
+ });
+}
+
+/**
+ * Fetch a single flow by ID
+ */
+export function useFlow(id: string | null) {
+ return useQuery({
+ queryKey: flowKeys.detail(id ?? ''),
+ queryFn: () => fetchFlow(id!),
+ enabled: !!id,
+ staleTime: 30000,
+ });
+}
+
+// ========== Mutation Hooks ==========
+
+/**
+ * Create a new flow
+ */
+export function useCreateFlow() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: createFlow,
+ onSuccess: (newFlow) => {
+ // Optimistically add to list
+ queryClient.setQueryData(flowKeys.lists(), (old) => {
+ if (!old) return { flows: [newFlow], total: 1 };
+ return {
+ flows: [...old.flows, newFlow],
+ total: old.total + 1,
+ };
+ });
+ // Invalidate to refetch
+ queryClient.invalidateQueries({ queryKey: flowKeys.lists() });
+ },
+ });
+}
+
+/**
+ * Update an existing flow
+ */
+export function useUpdateFlow() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: ({ id, flow }: { id: string; flow: Partial }) => updateFlow(id, flow),
+ onSuccess: (updatedFlow) => {
+ // Update in cache
+ queryClient.setQueryData(flowKeys.detail(updatedFlow.id), updatedFlow);
+ queryClient.setQueryData(flowKeys.lists(), (old) => {
+ if (!old) return old;
+ return {
+ ...old,
+ flows: old.flows.map((f) => (f.id === updatedFlow.id ? updatedFlow : f)),
+ };
+ });
+ },
+ });
+}
+
+/**
+ * Delete a flow
+ */
+export function useDeleteFlow() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: deleteFlow,
+ onSuccess: (_, deletedId) => {
+ // Remove from cache
+ queryClient.removeQueries({ queryKey: flowKeys.detail(deletedId) });
+ queryClient.setQueryData(flowKeys.lists(), (old) => {
+ if (!old) return old;
+ return {
+ flows: old.flows.filter((f) => f.id !== deletedId),
+ total: old.total - 1,
+ };
+ });
+ },
+ });
+}
+
+/**
+ * Duplicate a flow
+ */
+export function useDuplicateFlow() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: duplicateFlow,
+ onSuccess: (newFlow) => {
+ // Add to list
+ queryClient.setQueryData(flowKeys.lists(), (old) => {
+ if (!old) return { flows: [newFlow], total: 1 };
+ return {
+ flows: [...old.flows, newFlow],
+ total: old.total + 1,
+ };
+ });
+ queryClient.invalidateQueries({ queryKey: flowKeys.lists() });
+ },
+ });
+}
+
+// ========== Execution Mutation Hooks ==========
+
+/**
+ * Execute a flow
+ */
+export function useExecuteFlow() {
+ return useMutation({
+ mutationFn: executeFlow,
+ });
+}
+
+/**
+ * Pause execution
+ */
+export function usePauseExecution() {
+ return useMutation({
+ mutationFn: pauseExecution,
+ });
+}
+
+/**
+ * Resume execution
+ */
+export function useResumeExecution() {
+ return useMutation({
+ mutationFn: resumeExecution,
+ });
+}
+
+/**
+ * Stop execution
+ */
+export function useStopExecution() {
+ return useMutation({
+ mutationFn: stopExecution,
+ });
+}
diff --git a/ccw/frontend/src/hooks/useIssues.ts b/ccw/frontend/src/hooks/useIssues.ts
new file mode 100644
index 00000000..5c4e065c
--- /dev/null
+++ b/ccw/frontend/src/hooks/useIssues.ts
@@ -0,0 +1,297 @@
+// ========================================
+// useIssues Hook
+// ========================================
+// TanStack Query hooks for issues with queue management
+
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import {
+ fetchIssues,
+ fetchIssueHistory,
+ fetchIssueQueue,
+ createIssue,
+ updateIssue,
+ deleteIssue,
+ type Issue,
+ type IssuesResponse,
+ type IssueQueue,
+} from '../lib/api';
+
+// Query key factory
+export const issuesKeys = {
+ all: ['issues'] as const,
+ lists: () => [...issuesKeys.all, 'list'] as const,
+ list: (filters?: IssuesFilter) => [...issuesKeys.lists(), filters] as const,
+ history: () => [...issuesKeys.all, 'history'] as const,
+ queue: () => [...issuesKeys.all, 'queue'] as const,
+ details: () => [...issuesKeys.all, 'detail'] as const,
+ detail: (id: string) => [...issuesKeys.details(), id] as const,
+};
+
+// Default stale time: 30 seconds
+const STALE_TIME = 30 * 1000;
+
+export interface IssuesFilter {
+ status?: Issue['status'][];
+ priority?: Issue['priority'][];
+ search?: string;
+ includeHistory?: boolean;
+}
+
+export interface UseIssuesOptions {
+ filter?: IssuesFilter;
+ projectPath?: string;
+ staleTime?: number;
+ enabled?: boolean;
+ refetchInterval?: number;
+}
+
+export interface UseIssuesReturn {
+ issues: Issue[];
+ historyIssues: Issue[];
+ allIssues: Issue[];
+ issuesByStatus: Record;
+ issuesByPriority: Record;
+ openCount: number;
+ criticalCount: number;
+ isLoading: boolean;
+ isFetching: boolean;
+ error: Error | null;
+ refetch: () => Promise;
+ invalidate: () => Promise;
+}
+
+/**
+ * Hook for fetching and filtering issues
+ */
+export function useIssues(options: UseIssuesOptions = {}): UseIssuesReturn {
+ const { filter, projectPath, staleTime = STALE_TIME, enabled = true, refetchInterval = 0 } = options;
+ const queryClient = useQueryClient();
+
+ const issuesQuery = useQuery({
+ queryKey: issuesKeys.list(filter),
+ queryFn: () => fetchIssues(projectPath),
+ staleTime,
+ enabled,
+ refetchInterval: refetchInterval > 0 ? refetchInterval : false,
+ retry: 2,
+ });
+
+ const historyQuery = useQuery({
+ queryKey: issuesKeys.history(),
+ queryFn: () => fetchIssueHistory(projectPath),
+ staleTime,
+ enabled: enabled && (filter?.includeHistory ?? false),
+ retry: 2,
+ });
+
+ const allIssues = issuesQuery.data?.issues ?? [];
+ const historyIssues = historyQuery.data?.issues ?? [];
+
+ // Apply filters
+ const filteredIssues = (() => {
+ let issues = [...allIssues];
+
+ if (filter?.includeHistory) {
+ issues = [...issues, ...historyIssues];
+ }
+
+ if (filter?.status && filter.status.length > 0) {
+ issues = issues.filter((i) => filter.status!.includes(i.status));
+ }
+
+ if (filter?.priority && filter.priority.length > 0) {
+ issues = issues.filter((i) => filter.priority!.includes(i.priority));
+ }
+
+ if (filter?.search) {
+ const searchLower = filter.search.toLowerCase();
+ issues = issues.filter(
+ (i) =>
+ i.id.toLowerCase().includes(searchLower) ||
+ i.title.toLowerCase().includes(searchLower) ||
+ i.context?.toLowerCase().includes(searchLower)
+ );
+ }
+
+ return issues;
+ })();
+
+ // Group by status
+ const issuesByStatus: Record = {
+ open: [],
+ in_progress: [],
+ resolved: [],
+ closed: [],
+ completed: [],
+ };
+
+ for (const issue of allIssues) {
+ issuesByStatus[issue.status].push(issue);
+ }
+
+ // Group by priority
+ const issuesByPriority: Record = {
+ low: [],
+ medium: [],
+ high: [],
+ critical: [],
+ };
+
+ for (const issue of allIssues) {
+ issuesByPriority[issue.priority].push(issue);
+ }
+
+ const refetch = async () => {
+ await Promise.all([issuesQuery.refetch(), historyQuery.refetch()]);
+ };
+
+ const invalidate = async () => {
+ await queryClient.invalidateQueries({ queryKey: issuesKeys.all });
+ };
+
+ return {
+ issues: filteredIssues,
+ historyIssues,
+ allIssues,
+ issuesByStatus,
+ issuesByPriority,
+ openCount: issuesByStatus.open.length + issuesByStatus.in_progress.length,
+ criticalCount: issuesByPriority.critical.length,
+ isLoading: issuesQuery.isLoading,
+ isFetching: issuesQuery.isFetching || historyQuery.isFetching,
+ error: issuesQuery.error || historyQuery.error,
+ refetch,
+ invalidate,
+ };
+}
+
+/**
+ * Hook for fetching issue queue
+ */
+export function useIssueQueue(projectPath?: string) {
+ return useQuery({
+ queryKey: issuesKeys.queue(),
+ queryFn: () => fetchIssueQueue(projectPath),
+ staleTime: STALE_TIME,
+ retry: 2,
+ });
+}
+
+// ========== Mutations ==========
+
+export interface UseCreateIssueReturn {
+ createIssue: (input: { title: string; context?: string; priority?: Issue['priority'] }) => Promise;
+ isCreating: boolean;
+ error: Error | null;
+}
+
+export function useCreateIssue(): UseCreateIssueReturn {
+ const queryClient = useQueryClient();
+
+ const mutation = useMutation({
+ mutationFn: createIssue,
+ onSuccess: (newIssue) => {
+ queryClient.setQueryData(issuesKeys.list(), (old) => {
+ if (!old) return { issues: [newIssue] };
+ return {
+ issues: [newIssue, ...old.issues],
+ };
+ });
+ },
+ });
+
+ return {
+ createIssue: mutation.mutateAsync,
+ isCreating: mutation.isPending,
+ error: mutation.error,
+ };
+}
+
+export interface UseUpdateIssueReturn {
+ updateIssue: (issueId: string, input: Partial) => Promise;
+ isUpdating: boolean;
+ error: Error | null;
+}
+
+export function useUpdateIssue(): UseUpdateIssueReturn {
+ const queryClient = useQueryClient();
+
+ const mutation = useMutation({
+ mutationFn: ({ issueId, input }: { issueId: string; input: Partial }) =>
+ updateIssue(issueId, input),
+ onSuccess: (updatedIssue) => {
+ queryClient.setQueryData(issuesKeys.list(), (old) => {
+ if (!old) return old;
+ return {
+ issues: old.issues.map((i) => (i.id === updatedIssue.id ? updatedIssue : i)),
+ };
+ });
+ },
+ });
+
+ return {
+ updateIssue: (issueId, input) => mutation.mutateAsync({ issueId, input }),
+ isUpdating: mutation.isPending,
+ error: mutation.error,
+ };
+}
+
+export interface UseDeleteIssueReturn {
+ deleteIssue: (issueId: string) => Promise;
+ isDeleting: boolean;
+ error: Error | null;
+}
+
+export function useDeleteIssue(): UseDeleteIssueReturn {
+ const queryClient = useQueryClient();
+
+ const mutation = useMutation({
+ mutationFn: deleteIssue,
+ onMutate: async (issueId) => {
+ await queryClient.cancelQueries({ queryKey: issuesKeys.all });
+ const previousIssues = queryClient.getQueryData(issuesKeys.list());
+
+ queryClient.setQueryData(issuesKeys.list(), (old) => {
+ if (!old) return old;
+ return {
+ issues: old.issues.filter((i) => i.id !== issueId),
+ };
+ });
+
+ return { previousIssues };
+ },
+ onError: (_error, _issueId, context) => {
+ if (context?.previousIssues) {
+ queryClient.setQueryData(issuesKeys.list(), context.previousIssues);
+ }
+ },
+ onSettled: () => {
+ queryClient.invalidateQueries({ queryKey: issuesKeys.all });
+ },
+ });
+
+ return {
+ deleteIssue: mutation.mutateAsync,
+ isDeleting: mutation.isPending,
+ error: mutation.error,
+ };
+}
+
+/**
+ * Combined hook for all issue mutations
+ */
+export function useIssueMutations() {
+ const create = useCreateIssue();
+ const update = useUpdateIssue();
+ const remove = useDeleteIssue();
+
+ return {
+ createIssue: create.createIssue,
+ updateIssue: update.updateIssue,
+ deleteIssue: remove.deleteIssue,
+ isCreating: create.isCreating,
+ isUpdating: update.isUpdating,
+ isDeleting: remove.isDeleting,
+ isMutating: create.isCreating || update.isUpdating || remove.isDeleting,
+ };
+}
diff --git a/ccw/frontend/src/hooks/useLoops.ts b/ccw/frontend/src/hooks/useLoops.ts
new file mode 100644
index 00000000..518a639f
--- /dev/null
+++ b/ccw/frontend/src/hooks/useLoops.ts
@@ -0,0 +1,262 @@
+// ========================================
+// useLoops Hook
+// ========================================
+// TanStack Query hooks for loops with real-time updates
+
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import {
+ fetchLoops,
+ fetchLoop,
+ createLoop,
+ updateLoopStatus,
+ deleteLoop,
+ type Loop,
+ type LoopsResponse,
+} from '../lib/api';
+
+// Query key factory
+export const loopsKeys = {
+ all: ['loops'] as const,
+ lists: () => [...loopsKeys.all, 'list'] as const,
+ list: (filters?: LoopsFilter) => [...loopsKeys.lists(), filters] as const,
+ details: () => [...loopsKeys.all, 'detail'] as const,
+ detail: (id: string) => [...loopsKeys.details(), id] as const,
+};
+
+// Default stale time: 10 seconds (loops update frequently)
+const STALE_TIME = 10 * 1000;
+
+export interface LoopsFilter {
+ status?: Loop['status'][];
+ search?: string;
+}
+
+export interface UseLoopsOptions {
+ filter?: LoopsFilter;
+ staleTime?: number;
+ enabled?: boolean;
+ refetchInterval?: number;
+}
+
+export interface UseLoopsReturn {
+ loops: Loop[];
+ loopsByStatus: Record;
+ runningCount: number;
+ completedCount: number;
+ failedCount: number;
+ isLoading: boolean;
+ isFetching: boolean;
+ error: Error | null;
+ refetch: () => Promise;
+ invalidate: () => Promise;
+}
+
+/**
+ * Hook for fetching and filtering loops
+ */
+export function useLoops(options: UseLoopsOptions = {}): UseLoopsReturn {
+ const { filter, staleTime = STALE_TIME, enabled = true, refetchInterval = 0 } = options;
+ const queryClient = useQueryClient();
+
+ const query = useQuery({
+ queryKey: loopsKeys.list(filter),
+ queryFn: fetchLoops,
+ staleTime,
+ enabled,
+ refetchInterval: refetchInterval > 0 ? refetchInterval : false,
+ retry: 2,
+ });
+
+ const allLoops = query.data?.loops ?? [];
+
+ // Apply filters
+ const filteredLoops = (() => {
+ let loops = allLoops;
+
+ if (filter?.status && filter.status.length > 0) {
+ loops = loops.filter((l) => filter.status!.includes(l.status));
+ }
+
+ if (filter?.search) {
+ const searchLower = filter.search.toLowerCase();
+ loops = loops.filter(
+ (l) =>
+ l.id.toLowerCase().includes(searchLower) ||
+ l.name?.toLowerCase().includes(searchLower) ||
+ l.prompt?.toLowerCase().includes(searchLower)
+ );
+ }
+
+ return loops;
+ })();
+
+ // Group by status for Kanban
+ const loopsByStatus: Record = {
+ created: [],
+ running: [],
+ paused: [],
+ completed: [],
+ failed: [],
+ };
+
+ for (const loop of allLoops) {
+ loopsByStatus[loop.status].push(loop);
+ }
+
+ const refetch = async () => {
+ await query.refetch();
+ };
+
+ const invalidate = async () => {
+ await queryClient.invalidateQueries({ queryKey: loopsKeys.all });
+ };
+
+ return {
+ loops: filteredLoops,
+ loopsByStatus,
+ runningCount: loopsByStatus.running.length,
+ completedCount: loopsByStatus.completed.length,
+ failedCount: loopsByStatus.failed.length,
+ isLoading: query.isLoading,
+ isFetching: query.isFetching,
+ error: query.error,
+ refetch,
+ invalidate,
+ };
+}
+
+/**
+ * Hook for fetching a single loop
+ */
+export function useLoop(loopId: string, options: { enabled?: boolean } = {}) {
+ return useQuery({
+ queryKey: loopsKeys.detail(loopId),
+ queryFn: () => fetchLoop(loopId),
+ enabled: options.enabled ?? !!loopId,
+ staleTime: STALE_TIME,
+ });
+}
+
+// ========== Mutations ==========
+
+export interface UseCreateLoopReturn {
+ createLoop: (input: { prompt: string; tool?: string; mode?: string }) => Promise;
+ isCreating: boolean;
+ error: Error | null;
+}
+
+export function useCreateLoop(): UseCreateLoopReturn {
+ const queryClient = useQueryClient();
+
+ const mutation = useMutation({
+ mutationFn: createLoop,
+ onSuccess: (newLoop) => {
+ queryClient.setQueryData(loopsKeys.list(), (old) => {
+ if (!old) return { loops: [newLoop], total: 1 };
+ return {
+ loops: [newLoop, ...old.loops],
+ total: old.total + 1,
+ };
+ });
+ },
+ });
+
+ return {
+ createLoop: mutation.mutateAsync,
+ isCreating: mutation.isPending,
+ error: mutation.error,
+ };
+}
+
+export interface UseUpdateLoopStatusReturn {
+ updateStatus: (loopId: string, action: 'pause' | 'resume' | 'stop') => Promise;
+ isUpdating: boolean;
+ error: Error | null;
+}
+
+export function useUpdateLoopStatus(): UseUpdateLoopStatusReturn {
+ const queryClient = useQueryClient();
+
+ const mutation = useMutation({
+ mutationFn: ({ loopId, action }: { loopId: string; action: 'pause' | 'resume' | 'stop' }) =>
+ updateLoopStatus(loopId, action),
+ onSuccess: (updatedLoop) => {
+ queryClient.setQueryData(loopsKeys.list(), (old) => {
+ if (!old) return old;
+ return {
+ ...old,
+ loops: old.loops.map((l) => (l.id === updatedLoop.id ? updatedLoop : l)),
+ };
+ });
+ queryClient.setQueryData(loopsKeys.detail(updatedLoop.id), updatedLoop);
+ },
+ });
+
+ return {
+ updateStatus: (loopId, action) => mutation.mutateAsync({ loopId, action }),
+ isUpdating: mutation.isPending,
+ error: mutation.error,
+ };
+}
+
+export interface UseDeleteLoopReturn {
+ deleteLoop: (loopId: string) => Promise;
+ isDeleting: boolean;
+ error: Error | null;
+}
+
+export function useDeleteLoop(): UseDeleteLoopReturn {
+ const queryClient = useQueryClient();
+
+ const mutation = useMutation({
+ mutationFn: deleteLoop,
+ onMutate: async (loopId) => {
+ await queryClient.cancelQueries({ queryKey: loopsKeys.all });
+ const previousLoops = queryClient.getQueryData(loopsKeys.list());
+
+ queryClient.setQueryData(loopsKeys.list(), (old) => {
+ if (!old) return old;
+ return {
+ ...old,
+ loops: old.loops.filter((l) => l.id !== loopId),
+ total: old.total - 1,
+ };
+ });
+
+ return { previousLoops };
+ },
+ onError: (_error, _loopId, context) => {
+ if (context?.previousLoops) {
+ queryClient.setQueryData(loopsKeys.list(), context.previousLoops);
+ }
+ },
+ onSettled: () => {
+ queryClient.invalidateQueries({ queryKey: loopsKeys.all });
+ },
+ });
+
+ return {
+ deleteLoop: mutation.mutateAsync,
+ isDeleting: mutation.isPending,
+ error: mutation.error,
+ };
+}
+
+/**
+ * Combined hook for all loop mutations
+ */
+export function useLoopMutations() {
+ const create = useCreateLoop();
+ const update = useUpdateLoopStatus();
+ const remove = useDeleteLoop();
+
+ return {
+ createLoop: create.createLoop,
+ updateStatus: update.updateStatus,
+ deleteLoop: remove.deleteLoop,
+ isCreating: create.isCreating,
+ isUpdating: update.isUpdating,
+ isDeleting: remove.isDeleting,
+ isMutating: create.isCreating || update.isUpdating || remove.isDeleting,
+ };
+}
diff --git a/ccw/frontend/src/hooks/useMemory.ts b/ccw/frontend/src/hooks/useMemory.ts
new file mode 100644
index 00000000..144dd226
--- /dev/null
+++ b/ccw/frontend/src/hooks/useMemory.ts
@@ -0,0 +1,244 @@
+// ========================================
+// useMemory Hook
+// ========================================
+// TanStack Query hooks for core memory management
+
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import {
+ fetchMemories,
+ createMemory,
+ updateMemory,
+ deleteMemory,
+ type CoreMemory,
+ type MemoryResponse,
+} from '../lib/api';
+
+// Query key factory
+export const memoryKeys = {
+ all: ['memory'] as const,
+ lists: () => [...memoryKeys.all, 'list'] as const,
+ list: (filters?: MemoryFilter) => [...memoryKeys.lists(), filters] as const,
+ details: () => [...memoryKeys.all, 'detail'] as const,
+ detail: (id: string) => [...memoryKeys.details(), id] as const,
+};
+
+// Default stale time: 1 minute
+const STALE_TIME = 60 * 1000;
+
+export interface MemoryFilter {
+ search?: string;
+ tags?: string[];
+}
+
+export interface UseMemoryOptions {
+ filter?: MemoryFilter;
+ staleTime?: number;
+ enabled?: boolean;
+}
+
+export interface UseMemoryReturn {
+ memories: CoreMemory[];
+ totalSize: number;
+ claudeMdCount: number;
+ allTags: string[];
+ isLoading: boolean;
+ isFetching: boolean;
+ error: Error | null;
+ refetch: () => Promise;
+ invalidate: () => Promise;
+}
+
+/**
+ * Hook for fetching and filtering memories
+ */
+export function useMemory(options: UseMemoryOptions = {}): UseMemoryReturn {
+ const { filter, staleTime = STALE_TIME, enabled = true } = options;
+ const queryClient = useQueryClient();
+
+ const query = useQuery({
+ queryKey: memoryKeys.list(filter),
+ queryFn: fetchMemories,
+ staleTime,
+ enabled,
+ retry: 2,
+ });
+
+ const allMemories = query.data?.memories ?? [];
+ const totalSize = query.data?.totalSize ?? 0;
+ const claudeMdCount = query.data?.claudeMdCount ?? 0;
+
+ // Apply filters
+ const filteredMemories = (() => {
+ let memories = allMemories;
+
+ if (filter?.search) {
+ const searchLower = filter.search.toLowerCase();
+ memories = memories.filter(
+ (m) =>
+ m.content.toLowerCase().includes(searchLower) ||
+ m.source?.toLowerCase().includes(searchLower) ||
+ m.tags?.some((t) => t.toLowerCase().includes(searchLower))
+ );
+ }
+
+ if (filter?.tags && filter.tags.length > 0) {
+ memories = memories.filter((m) =>
+ filter.tags!.some((tag) => m.tags?.includes(tag))
+ );
+ }
+
+ return memories;
+ })();
+
+ // Collect all unique tags
+ const allTags = Array.from(
+ new Set(allMemories.flatMap((m) => m.tags ?? []))
+ ).sort();
+
+ const refetch = async () => {
+ await query.refetch();
+ };
+
+ const invalidate = async () => {
+ await queryClient.invalidateQueries({ queryKey: memoryKeys.all });
+ };
+
+ return {
+ memories: filteredMemories,
+ totalSize,
+ claudeMdCount,
+ allTags,
+ isLoading: query.isLoading,
+ isFetching: query.isFetching,
+ error: query.error,
+ refetch,
+ invalidate,
+ };
+}
+
+// ========== Mutations ==========
+
+export interface UseCreateMemoryReturn {
+ createMemory: (input: { content: string; tags?: string[] }) => Promise;
+ isCreating: boolean;
+ error: Error | null;
+}
+
+export function useCreateMemory(): UseCreateMemoryReturn {
+ const queryClient = useQueryClient();
+
+ const mutation = useMutation({
+ mutationFn: createMemory,
+ onSuccess: (newMemory) => {
+ queryClient.setQueryData(memoryKeys.list(), (old) => {
+ if (!old) return { memories: [newMemory], totalSize: 0, claudeMdCount: 0 };
+ return {
+ ...old,
+ memories: [newMemory, ...old.memories],
+ totalSize: old.totalSize + (newMemory.size ?? 0),
+ };
+ });
+ },
+ });
+
+ return {
+ createMemory: mutation.mutateAsync,
+ isCreating: mutation.isPending,
+ error: mutation.error,
+ };
+}
+
+export interface UseUpdateMemoryReturn {
+ updateMemory: (memoryId: string, input: Partial) => Promise;
+ isUpdating: boolean;
+ error: Error | null;
+}
+
+export function useUpdateMemory(): UseUpdateMemoryReturn {
+ const queryClient = useQueryClient();
+
+ const mutation = useMutation({
+ mutationFn: ({ memoryId, input }: { memoryId: string; input: Partial }) =>
+ updateMemory(memoryId, input),
+ onSuccess: (updatedMemory) => {
+ queryClient.setQueryData(memoryKeys.list(), (old) => {
+ if (!old) return old;
+ return {
+ ...old,
+ memories: old.memories.map((m) =>
+ m.id === updatedMemory.id ? updatedMemory : m
+ ),
+ };
+ });
+ },
+ });
+
+ return {
+ updateMemory: (memoryId, input) => mutation.mutateAsync({ memoryId, input }),
+ isUpdating: mutation.isPending,
+ error: mutation.error,
+ };
+}
+
+export interface UseDeleteMemoryReturn {
+ deleteMemory: (memoryId: string) => Promise;
+ isDeleting: boolean;
+ error: Error | null;
+}
+
+export function useDeleteMemory(): UseDeleteMemoryReturn {
+ const queryClient = useQueryClient();
+
+ const mutation = useMutation({
+ mutationFn: deleteMemory,
+ onMutate: async (memoryId) => {
+ await queryClient.cancelQueries({ queryKey: memoryKeys.all });
+ const previousMemories = queryClient.getQueryData(memoryKeys.list());
+
+ queryClient.setQueryData(memoryKeys.list(), (old) => {
+ if (!old) return old;
+ const removedMemory = old.memories.find((m) => m.id === memoryId);
+ return {
+ ...old,
+ memories: old.memories.filter((m) => m.id !== memoryId),
+ totalSize: old.totalSize - (removedMemory?.size ?? 0),
+ };
+ });
+
+ return { previousMemories };
+ },
+ onError: (_error, _memoryId, context) => {
+ if (context?.previousMemories) {
+ queryClient.setQueryData(memoryKeys.list(), context.previousMemories);
+ }
+ },
+ onSettled: () => {
+ queryClient.invalidateQueries({ queryKey: memoryKeys.all });
+ },
+ });
+
+ return {
+ deleteMemory: mutation.mutateAsync,
+ isDeleting: mutation.isPending,
+ error: mutation.error,
+ };
+}
+
+/**
+ * Combined hook for all memory mutations
+ */
+export function useMemoryMutations() {
+ const create = useCreateMemory();
+ const update = useUpdateMemory();
+ const remove = useDeleteMemory();
+
+ return {
+ createMemory: create.createMemory,
+ updateMemory: update.updateMemory,
+ deleteMemory: remove.deleteMemory,
+ isCreating: create.isCreating,
+ isUpdating: update.isUpdating,
+ isDeleting: remove.isDeleting,
+ isMutating: create.isCreating || update.isUpdating || remove.isDeleting,
+ };
+}
diff --git a/ccw/frontend/src/hooks/useNotifications.ts b/ccw/frontend/src/hooks/useNotifications.ts
new file mode 100644
index 00000000..909f7626
--- /dev/null
+++ b/ccw/frontend/src/hooks/useNotifications.ts
@@ -0,0 +1,232 @@
+// ========================================
+// useNotifications Hook
+// ========================================
+// Convenient hook for notification and toast management
+
+import { useCallback } from 'react';
+import {
+ useNotificationStore,
+ selectToasts,
+ selectWsStatus,
+ selectWsLastMessage,
+ selectIsPanelVisible,
+ selectPersistentNotifications,
+} from '../stores/notificationStore';
+import type { Toast, ToastType, WebSocketStatus, WebSocketMessage } from '../types/store';
+
+export interface UseNotificationsReturn {
+ /** Current toast queue */
+ toasts: Toast[];
+ /** WebSocket connection status */
+ wsStatus: WebSocketStatus;
+ /** Last WebSocket message received */
+ wsLastMessage: WebSocketMessage | null;
+ /** Whether WebSocket is connected */
+ isWsConnected: boolean;
+ /** Whether notification panel is visible */
+ isPanelVisible: boolean;
+ /** Persistent notifications (stored in localStorage) */
+ persistentNotifications: Toast[];
+ /** Add a toast notification */
+ addToast: (type: ToastType, title: string, message?: string, options?: ToastOptions) => string;
+ /** Show info toast */
+ info: (title: string, message?: string) => string;
+ /** Show success toast */
+ success: (title: string, message?: string) => string;
+ /** Show warning toast */
+ warning: (title: string, message?: string) => string;
+ /** Show error toast (persistent by default) */
+ error: (title: string, message?: string) => string;
+ /** Remove a toast */
+ removeToast: (id: string) => void;
+ /** Clear all toasts */
+ clearAllToasts: () => void;
+ /** Set WebSocket status */
+ setWsStatus: (status: WebSocketStatus) => void;
+ /** Set last WebSocket message */
+ setWsLastMessage: (message: WebSocketMessage | null) => void;
+ /** Toggle notification panel */
+ togglePanel: () => void;
+ /** Set panel visibility */
+ setPanelVisible: (visible: boolean) => void;
+ /** Add persistent notification */
+ addPersistentNotification: (type: ToastType, title: string, message?: string) => void;
+ /** Remove persistent notification */
+ removePersistentNotification: (id: string) => void;
+ /** Clear all persistent notifications */
+ clearPersistentNotifications: () => void;
+}
+
+export interface ToastOptions {
+ /** Duration in ms (0 = persistent) */
+ duration?: number;
+ /** Whether toast can be dismissed */
+ dismissible?: boolean;
+ /** Action button */
+ action?: {
+ label: string;
+ onClick: () => void;
+ };
+}
+
+/**
+ * Hook for managing notifications and toasts
+ * @returns Notification state and actions
+ *
+ * @example
+ * ```tsx
+ * const { toasts, info, success, error, removeToast } = useNotifications();
+ *
+ * const handleSave = async () => {
+ * try {
+ * await save();
+ * success('Saved', 'Your changes have been saved');
+ * } catch (e) {
+ * error('Error', 'Failed to save changes');
+ * }
+ * };
+ * ```
+ */
+export function useNotifications(): UseNotificationsReturn {
+ const toasts = useNotificationStore(selectToasts);
+ const wsStatus = useNotificationStore(selectWsStatus);
+ const wsLastMessage = useNotificationStore(selectWsLastMessage);
+ const isPanelVisible = useNotificationStore(selectIsPanelVisible);
+ const persistentNotifications = useNotificationStore(selectPersistentNotifications);
+
+ // Actions
+ const addToastAction = useNotificationStore((state) => state.addToast);
+ const removeToastAction = useNotificationStore((state) => state.removeToast);
+ const clearAllToastsAction = useNotificationStore((state) => state.clearAllToasts);
+ const setWsStatusAction = useNotificationStore((state) => state.setWsStatus);
+ const setWsLastMessageAction = useNotificationStore((state) => state.setWsLastMessage);
+ const togglePanelAction = useNotificationStore((state) => state.togglePanel);
+ const setPanelVisibleAction = useNotificationStore((state) => state.setPanelVisible);
+ const addPersistentAction = useNotificationStore((state) => state.addPersistentNotification);
+ const removePersistentAction = useNotificationStore((state) => state.removePersistentNotification);
+ const clearPersistentAction = useNotificationStore((state) => state.clearPersistentNotifications);
+
+ // Computed
+ const isWsConnected = wsStatus === 'connected';
+
+ // Callbacks
+ const addToast = useCallback(
+ (type: ToastType, title: string, message?: string, options?: ToastOptions): string => {
+ return addToastAction({
+ type,
+ title,
+ message,
+ duration: options?.duration,
+ dismissible: options?.dismissible,
+ action: options?.action,
+ });
+ },
+ [addToastAction]
+ );
+
+ const info = useCallback(
+ (title: string, message?: string): string => {
+ return addToast('info', title, message);
+ },
+ [addToast]
+ );
+
+ const success = useCallback(
+ (title: string, message?: string): string => {
+ return addToast('success', title, message);
+ },
+ [addToast]
+ );
+
+ const warning = useCallback(
+ (title: string, message?: string): string => {
+ return addToast('warning', title, message);
+ },
+ [addToast]
+ );
+
+ const error = useCallback(
+ (title: string, message?: string): string => {
+ // Error toasts are persistent by default
+ return addToast('error', title, message, { duration: 0 });
+ },
+ [addToast]
+ );
+
+ const removeToast = useCallback(
+ (id: string) => {
+ removeToastAction(id);
+ },
+ [removeToastAction]
+ );
+
+ const clearAllToasts = useCallback(() => {
+ clearAllToastsAction();
+ }, [clearAllToastsAction]);
+
+ const setWsStatus = useCallback(
+ (status: WebSocketStatus) => {
+ setWsStatusAction(status);
+ },
+ [setWsStatusAction]
+ );
+
+ const setWsLastMessage = useCallback(
+ (message: WebSocketMessage | null) => {
+ setWsLastMessageAction(message);
+ },
+ [setWsLastMessageAction]
+ );
+
+ const togglePanel = useCallback(() => {
+ togglePanelAction();
+ }, [togglePanelAction]);
+
+ const setPanelVisible = useCallback(
+ (visible: boolean) => {
+ setPanelVisibleAction(visible);
+ },
+ [setPanelVisibleAction]
+ );
+
+ const addPersistentNotification = useCallback(
+ (type: ToastType, title: string, message?: string) => {
+ addPersistentAction({ type, title, message });
+ },
+ [addPersistentAction]
+ );
+
+ const removePersistentNotification = useCallback(
+ (id: string) => {
+ removePersistentAction(id);
+ },
+ [removePersistentAction]
+ );
+
+ const clearPersistentNotifications = useCallback(() => {
+ clearPersistentAction();
+ }, [clearPersistentAction]);
+
+ return {
+ toasts,
+ wsStatus,
+ wsLastMessage,
+ isWsConnected,
+ isPanelVisible,
+ persistentNotifications,
+ addToast,
+ info,
+ success,
+ warning,
+ error,
+ removeToast,
+ clearAllToasts,
+ setWsStatus,
+ setWsLastMessage,
+ togglePanel,
+ setPanelVisible,
+ addPersistentNotification,
+ removePersistentNotification,
+ clearPersistentNotifications,
+ };
+}
diff --git a/ccw/frontend/src/hooks/useSession.ts b/ccw/frontend/src/hooks/useSession.ts
new file mode 100644
index 00000000..7d86b56a
--- /dev/null
+++ b/ccw/frontend/src/hooks/useSession.ts
@@ -0,0 +1,155 @@
+// ========================================
+// useSession Hook
+// ========================================
+// Convenient hook for session management
+
+import { useCallback, useMemo } from 'react';
+import { useWorkflowStore, selectActiveSessionId } from '../stores/workflowStore';
+import type { SessionMetadata, TaskData } from '../types/store';
+
+export interface UseSessionReturn {
+ /** Currently active session ID */
+ activeSessionId: string | null;
+ /** Currently active session data */
+ activeSession: SessionMetadata | null;
+ /** All active sessions */
+ activeSessions: SessionMetadata[];
+ /** All archived sessions */
+ archivedSessions: SessionMetadata[];
+ /** Filtered sessions based on current filters */
+ filteredSessions: SessionMetadata[];
+ /** Set the active session */
+ setActiveSession: (sessionId: string | null) => void;
+ /** Add a new session */
+ addSession: (session: SessionMetadata) => void;
+ /** Update a session */
+ updateSession: (sessionId: string, updates: Partial) => void;
+ /** Archive a session */
+ archiveSession: (sessionId: string) => void;
+ /** Remove a session */
+ removeSession: (sessionId: string) => void;
+ /** Add a task to a session */
+ addTask: (sessionId: string, task: TaskData) => void;
+ /** Update a task */
+ updateTask: (sessionId: string, taskId: string, updates: Partial) => void;
+ /** Get session by key */
+ getSessionByKey: (key: string) => SessionMetadata | undefined;
+}
+
+/**
+ * Hook for managing session state
+ * @returns Session state and actions
+ *
+ * @example
+ * ```tsx
+ * const { activeSession, activeSessions, setActiveSession } = useSession();
+ *
+ * return (
+ * setActiveSession(id)}
+ * />
+ * );
+ * ```
+ */
+export function useSession(): UseSessionReturn {
+ const activeSessionId = useWorkflowStore(selectActiveSessionId);
+ const workflowData = useWorkflowStore((state) => state.workflowData);
+ const sessionDataStore = useWorkflowStore((state) => state.sessionDataStore);
+
+ // Actions
+ const setActiveSessionId = useWorkflowStore((state) => state.setActiveSessionId);
+ const addSessionAction = useWorkflowStore((state) => state.addSession);
+ const updateSessionAction = useWorkflowStore((state) => state.updateSession);
+ const archiveSessionAction = useWorkflowStore((state) => state.archiveSession);
+ const removeSessionAction = useWorkflowStore((state) => state.removeSession);
+ const addTaskAction = useWorkflowStore((state) => state.addTask);
+ const updateTaskAction = useWorkflowStore((state) => state.updateTask);
+ const getFilteredSessionsAction = useWorkflowStore((state) => state.getFilteredSessions);
+ const getSessionByKeyAction = useWorkflowStore((state) => state.getSessionByKey);
+
+ // Memoized active session
+ const activeSession = useMemo(() => {
+ if (!activeSessionId) return null;
+ const key = `session-${activeSessionId}`.replace(/[^a-zA-Z0-9-]/g, '-');
+ return sessionDataStore[key] || null;
+ }, [activeSessionId, sessionDataStore]);
+
+ // Memoized filtered sessions
+ const filteredSessions = useMemo(() => {
+ return getFilteredSessionsAction();
+ }, [getFilteredSessionsAction, workflowData]);
+
+ // Callbacks
+ const setActiveSession = useCallback(
+ (sessionId: string | null) => {
+ setActiveSessionId(sessionId);
+ },
+ [setActiveSessionId]
+ );
+
+ const addSession = useCallback(
+ (session: SessionMetadata) => {
+ addSessionAction(session);
+ },
+ [addSessionAction]
+ );
+
+ const updateSession = useCallback(
+ (sessionId: string, updates: Partial) => {
+ updateSessionAction(sessionId, updates);
+ },
+ [updateSessionAction]
+ );
+
+ const archiveSession = useCallback(
+ (sessionId: string) => {
+ archiveSessionAction(sessionId);
+ },
+ [archiveSessionAction]
+ );
+
+ const removeSession = useCallback(
+ (sessionId: string) => {
+ removeSessionAction(sessionId);
+ },
+ [removeSessionAction]
+ );
+
+ const addTask = useCallback(
+ (sessionId: string, task: TaskData) => {
+ addTaskAction(sessionId, task);
+ },
+ [addTaskAction]
+ );
+
+ const updateTask = useCallback(
+ (sessionId: string, taskId: string, updates: Partial) => {
+ updateTaskAction(sessionId, taskId, updates);
+ },
+ [updateTaskAction]
+ );
+
+ const getSessionByKey = useCallback(
+ (key: string) => {
+ return getSessionByKeyAction(key);
+ },
+ [getSessionByKeyAction]
+ );
+
+ return {
+ activeSessionId,
+ activeSession,
+ activeSessions: workflowData.activeSessions,
+ archivedSessions: workflowData.archivedSessions,
+ filteredSessions,
+ setActiveSession,
+ addSession,
+ updateSession,
+ archiveSession,
+ removeSession,
+ addTask,
+ updateTask,
+ getSessionByKey,
+ };
+}
diff --git a/ccw/frontend/src/hooks/useSessions.ts b/ccw/frontend/src/hooks/useSessions.ts
new file mode 100644
index 00000000..b3e01034
--- /dev/null
+++ b/ccw/frontend/src/hooks/useSessions.ts
@@ -0,0 +1,373 @@
+// ========================================
+// useSessions Hook
+// ========================================
+// TanStack Query hooks for sessions with optimistic updates
+
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import {
+ fetchSessions,
+ createSession,
+ updateSession,
+ archiveSession,
+ deleteSession,
+ type SessionsResponse,
+ type CreateSessionInput,
+ type UpdateSessionInput,
+} from '../lib/api';
+import type { SessionMetadata } from '../types/store';
+import { dashboardStatsKeys } from './useDashboardStats';
+
+// Query key factory
+export const sessionsKeys = {
+ all: ['sessions'] as const,
+ lists: () => [...sessionsKeys.all, 'list'] as const,
+ list: (filters?: SessionsFilter) => [...sessionsKeys.lists(), filters] as const,
+ details: () => [...sessionsKeys.all, 'detail'] as const,
+ detail: (id: string) => [...sessionsKeys.details(), id] as const,
+};
+
+// Default stale time: 30 seconds
+const STALE_TIME = 30 * 1000;
+
+export interface SessionsFilter {
+ status?: SessionMetadata['status'][];
+ search?: string;
+ location?: 'active' | 'archived' | 'all';
+}
+
+export interface UseSessionsOptions {
+ /** Filter options */
+ filter?: SessionsFilter;
+ /** Override default stale time (ms) */
+ staleTime?: number;
+ /** Enable/disable the query */
+ enabled?: boolean;
+ /** Refetch interval (ms), 0 to disable */
+ refetchInterval?: number;
+}
+
+export interface UseSessionsReturn {
+ /** All sessions data */
+ sessions: SessionsResponse | undefined;
+ /** Active sessions */
+ activeSessions: SessionMetadata[];
+ /** Archived sessions */
+ archivedSessions: SessionMetadata[];
+ /** Filtered sessions based on filter options */
+ filteredSessions: SessionMetadata[];
+ /** Loading state for initial fetch */
+ isLoading: boolean;
+ /** Fetching state (initial or refetch) */
+ isFetching: boolean;
+ /** Error object if query failed */
+ error: Error | null;
+ /** Manually refetch data */
+ refetch: () => Promise;
+ /** Invalidate and refetch sessions */
+ invalidate: () => Promise;
+}
+
+/**
+ * Hook for fetching sessions data
+ *
+ * @example
+ * ```tsx
+ * const { activeSessions, isLoading } = useSessions({
+ * filter: { location: 'active' }
+ * });
+ * ```
+ */
+export function useSessions(options: UseSessionsOptions = {}): UseSessionsReturn {
+ const { filter, staleTime = STALE_TIME, enabled = true, refetchInterval = 0 } = options;
+ const queryClient = useQueryClient();
+
+ const query = useQuery({
+ queryKey: sessionsKeys.list(filter),
+ queryFn: fetchSessions,
+ staleTime,
+ enabled,
+ refetchInterval: refetchInterval > 0 ? refetchInterval : false,
+ retry: 2,
+ retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000),
+ });
+
+ const activeSessions = query.data?.activeSessions ?? [];
+ const archivedSessions = query.data?.archivedSessions ?? [];
+
+ // Apply client-side filtering
+ const filteredSessions = (() => {
+ let sessions: SessionMetadata[] = [];
+
+ if (!filter?.location || filter.location === 'all') {
+ sessions = [...activeSessions, ...archivedSessions];
+ } else if (filter.location === 'active') {
+ sessions = activeSessions;
+ } else {
+ sessions = archivedSessions;
+ }
+
+ // Apply status filter
+ if (filter?.status && filter.status.length > 0) {
+ sessions = sessions.filter((s) => filter.status!.includes(s.status));
+ }
+
+ // Apply search filter
+ if (filter?.search) {
+ const searchLower = filter.search.toLowerCase();
+ sessions = sessions.filter(
+ (s) =>
+ s.session_id.toLowerCase().includes(searchLower) ||
+ s.title?.toLowerCase().includes(searchLower) ||
+ s.description?.toLowerCase().includes(searchLower)
+ );
+ }
+
+ return sessions;
+ })();
+
+ const refetch = async () => {
+ await query.refetch();
+ };
+
+ const invalidate = async () => {
+ await queryClient.invalidateQueries({ queryKey: sessionsKeys.all });
+ };
+
+ return {
+ sessions: query.data,
+ activeSessions,
+ archivedSessions,
+ filteredSessions,
+ isLoading: query.isLoading,
+ isFetching: query.isFetching,
+ error: query.error,
+ refetch,
+ invalidate,
+ };
+}
+
+// ========== Mutations ==========
+
+export interface UseCreateSessionReturn {
+ createSession: (input: CreateSessionInput) => Promise;
+ isCreating: boolean;
+ error: Error | null;
+}
+
+/**
+ * Hook for creating a new session
+ */
+export function useCreateSession(): UseCreateSessionReturn {
+ const queryClient = useQueryClient();
+
+ const mutation = useMutation({
+ mutationFn: createSession,
+ onSuccess: (newSession) => {
+ // Update sessions cache
+ queryClient.setQueryData(sessionsKeys.list(), (old) => {
+ if (!old) return { activeSessions: [newSession], archivedSessions: [] };
+ return {
+ ...old,
+ activeSessions: [newSession, ...old.activeSessions],
+ };
+ });
+ // Invalidate dashboard stats
+ queryClient.invalidateQueries({ queryKey: dashboardStatsKeys.all });
+ },
+ });
+
+ return {
+ createSession: mutation.mutateAsync,
+ isCreating: mutation.isPending,
+ error: mutation.error,
+ };
+}
+
+export interface UseUpdateSessionReturn {
+ updateSession: (sessionId: string, input: UpdateSessionInput) => Promise;
+ isUpdating: boolean;
+ error: Error | null;
+}
+
+/**
+ * Hook for updating a session
+ */
+export function useUpdateSession(): UseUpdateSessionReturn {
+ const queryClient = useQueryClient();
+
+ const mutation = useMutation({
+ mutationFn: ({ sessionId, input }: { sessionId: string; input: UpdateSessionInput }) =>
+ updateSession(sessionId, input),
+ onSuccess: (updatedSession) => {
+ // Update sessions cache
+ queryClient.setQueryData(sessionsKeys.list(), (old) => {
+ if (!old) return old;
+ return {
+ activeSessions: old.activeSessions.map((s) =>
+ s.session_id === updatedSession.session_id ? updatedSession : s
+ ),
+ archivedSessions: old.archivedSessions.map((s) =>
+ s.session_id === updatedSession.session_id ? updatedSession : s
+ ),
+ };
+ });
+ },
+ });
+
+ return {
+ updateSession: (sessionId, input) => mutation.mutateAsync({ sessionId, input }),
+ isUpdating: mutation.isPending,
+ error: mutation.error,
+ };
+}
+
+export interface UseArchiveSessionReturn {
+ archiveSession: (sessionId: string) => Promise;
+ isArchiving: boolean;
+ error: Error | null;
+}
+
+/**
+ * Hook for archiving a session with optimistic update
+ */
+export function useArchiveSession(): UseArchiveSessionReturn {
+ const queryClient = useQueryClient();
+
+ const mutation = useMutation({
+ mutationFn: archiveSession,
+ onMutate: async (sessionId) => {
+ // Cancel outgoing refetches
+ await queryClient.cancelQueries({ queryKey: sessionsKeys.all });
+
+ // Snapshot previous value
+ const previousSessions = queryClient.getQueryData(sessionsKeys.list());
+
+ // Optimistically update
+ queryClient.setQueryData(sessionsKeys.list(), (old) => {
+ if (!old) return old;
+ const session = old.activeSessions.find((s) => s.session_id === sessionId);
+ if (!session) return old;
+
+ const archivedSession: SessionMetadata = {
+ ...session,
+ status: 'archived',
+ location: 'archived',
+ updated_at: new Date().toISOString(),
+ };
+
+ return {
+ activeSessions: old.activeSessions.filter((s) => s.session_id !== sessionId),
+ archivedSessions: [archivedSession, ...old.archivedSessions],
+ };
+ });
+
+ return { previousSessions };
+ },
+ onError: (_error, _sessionId, context) => {
+ // Rollback on error
+ if (context?.previousSessions) {
+ queryClient.setQueryData(sessionsKeys.list(), context.previousSessions);
+ }
+ },
+ onSettled: () => {
+ // Invalidate to ensure sync with server
+ queryClient.invalidateQueries({ queryKey: sessionsKeys.all });
+ queryClient.invalidateQueries({ queryKey: dashboardStatsKeys.all });
+ },
+ });
+
+ return {
+ archiveSession: mutation.mutateAsync,
+ isArchiving: mutation.isPending,
+ error: mutation.error,
+ };
+}
+
+export interface UseDeleteSessionReturn {
+ deleteSession: (sessionId: string) => Promise;
+ isDeleting: boolean;
+ error: Error | null;
+}
+
+/**
+ * Hook for deleting a session with optimistic update
+ */
+export function useDeleteSession(): UseDeleteSessionReturn {
+ const queryClient = useQueryClient();
+
+ const mutation = useMutation({
+ mutationFn: deleteSession,
+ onMutate: async (sessionId) => {
+ // Cancel outgoing refetches
+ await queryClient.cancelQueries({ queryKey: sessionsKeys.all });
+
+ // Snapshot previous value
+ const previousSessions = queryClient.getQueryData(sessionsKeys.list());
+
+ // Optimistically remove
+ queryClient.setQueryData(sessionsKeys.list(), (old) => {
+ if (!old) return old;
+ return {
+ activeSessions: old.activeSessions.filter((s) => s.session_id !== sessionId),
+ archivedSessions: old.archivedSessions.filter((s) => s.session_id !== sessionId),
+ };
+ });
+
+ return { previousSessions };
+ },
+ onError: (_error, _sessionId, context) => {
+ // Rollback on error
+ if (context?.previousSessions) {
+ queryClient.setQueryData(sessionsKeys.list(), context.previousSessions);
+ }
+ },
+ onSettled: () => {
+ // Invalidate to ensure sync with server
+ queryClient.invalidateQueries({ queryKey: sessionsKeys.all });
+ queryClient.invalidateQueries({ queryKey: dashboardStatsKeys.all });
+ },
+ });
+
+ return {
+ deleteSession: mutation.mutateAsync,
+ isDeleting: mutation.isPending,
+ error: mutation.error,
+ };
+}
+
+/**
+ * Combined hook for all session mutations
+ */
+export function useSessionMutations() {
+ const create = useCreateSession();
+ const update = useUpdateSession();
+ const archive = useArchiveSession();
+ const remove = useDeleteSession();
+
+ return {
+ createSession: create.createSession,
+ updateSession: update.updateSession,
+ archiveSession: archive.archiveSession,
+ deleteSession: remove.deleteSession,
+ isCreating: create.isCreating,
+ isUpdating: update.isUpdating,
+ isArchiving: archive.isArchiving,
+ isDeleting: remove.isDeleting,
+ isMutating: create.isCreating || update.isUpdating || archive.isArchiving || remove.isDeleting,
+ };
+}
+
+/**
+ * Hook to prefetch sessions data
+ */
+export function usePrefetchSessions() {
+ const queryClient = useQueryClient();
+
+ return (filter?: SessionsFilter) => {
+ queryClient.prefetchQuery({
+ queryKey: sessionsKeys.list(filter),
+ queryFn: fetchSessions,
+ staleTime: STALE_TIME,
+ });
+ };
+}
diff --git a/ccw/frontend/src/hooks/useSkills.ts b/ccw/frontend/src/hooks/useSkills.ts
new file mode 100644
index 00000000..a66bcfe0
--- /dev/null
+++ b/ccw/frontend/src/hooks/useSkills.ts
@@ -0,0 +1,193 @@
+// ========================================
+// useSkills Hook
+// ========================================
+// TanStack Query hooks for skills management
+
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import {
+ fetchSkills,
+ toggleSkill,
+ type Skill,
+ type SkillsResponse,
+} from '../lib/api';
+
+// Query key factory
+export const skillsKeys = {
+ all: ['skills'] as const,
+ lists: () => [...skillsKeys.all, 'list'] as const,
+ list: (filters?: SkillsFilter) => [...skillsKeys.lists(), filters] as const,
+};
+
+// Default stale time: 5 minutes (skills don't change frequently)
+const STALE_TIME = 5 * 60 * 1000;
+
+export interface SkillsFilter {
+ search?: string;
+ category?: string;
+ source?: Skill['source'];
+ enabledOnly?: boolean;
+}
+
+export interface UseSkillsOptions {
+ filter?: SkillsFilter;
+ staleTime?: number;
+ enabled?: boolean;
+}
+
+export interface UseSkillsReturn {
+ skills: Skill[];
+ enabledSkills: Skill[];
+ categories: string[];
+ skillsByCategory: Record;
+ totalCount: number;
+ enabledCount: number;
+ isLoading: boolean;
+ isFetching: boolean;
+ error: Error | null;
+ refetch: () => Promise;
+ invalidate: () => Promise;
+}
+
+/**
+ * Hook for fetching and filtering skills
+ */
+export function useSkills(options: UseSkillsOptions = {}): UseSkillsReturn {
+ const { filter, staleTime = STALE_TIME, enabled = true } = options;
+ const queryClient = useQueryClient();
+
+ const query = useQuery({
+ queryKey: skillsKeys.list(filter),
+ queryFn: fetchSkills,
+ staleTime,
+ enabled,
+ retry: 2,
+ });
+
+ const allSkills = query.data?.skills ?? [];
+
+ // Apply filters
+ const filteredSkills = (() => {
+ let skills = allSkills;
+
+ if (filter?.search) {
+ const searchLower = filter.search.toLowerCase();
+ skills = skills.filter(
+ (s) =>
+ s.name.toLowerCase().includes(searchLower) ||
+ s.description.toLowerCase().includes(searchLower) ||
+ s.triggers.some((t) => t.toLowerCase().includes(searchLower))
+ );
+ }
+
+ if (filter?.category) {
+ skills = skills.filter((s) => s.category === filter.category);
+ }
+
+ if (filter?.source) {
+ skills = skills.filter((s) => s.source === filter.source);
+ }
+
+ if (filter?.enabledOnly) {
+ skills = skills.filter((s) => s.enabled);
+ }
+
+ return skills;
+ })();
+
+ // Group by category
+ const skillsByCategory: Record = {};
+ const categories = new Set();
+
+ for (const skill of allSkills) {
+ const category = skill.category || 'Uncategorized';
+ categories.add(category);
+ if (!skillsByCategory[category]) {
+ skillsByCategory[category] = [];
+ }
+ skillsByCategory[category].push(skill);
+ }
+
+ const enabledSkills = allSkills.filter((s) => s.enabled);
+
+ const refetch = async () => {
+ await query.refetch();
+ };
+
+ const invalidate = async () => {
+ await queryClient.invalidateQueries({ queryKey: skillsKeys.all });
+ };
+
+ return {
+ skills: filteredSkills,
+ enabledSkills,
+ categories: Array.from(categories).sort(),
+ skillsByCategory,
+ totalCount: allSkills.length,
+ enabledCount: enabledSkills.length,
+ isLoading: query.isLoading,
+ isFetching: query.isFetching,
+ error: query.error,
+ refetch,
+ invalidate,
+ };
+}
+
+// ========== Mutations ==========
+
+export interface UseToggleSkillReturn {
+ toggleSkill: (skillName: string, enabled: boolean) => Promise;
+ isToggling: boolean;
+ error: Error | null;
+}
+
+export function useToggleSkill(): UseToggleSkillReturn {
+ const queryClient = useQueryClient();
+
+ const mutation = useMutation({
+ mutationFn: ({ skillName, enabled }: { skillName: string; enabled: boolean }) =>
+ toggleSkill(skillName, enabled),
+ onMutate: async ({ skillName, enabled }) => {
+ await queryClient.cancelQueries({ queryKey: skillsKeys.all });
+ const previousSkills = queryClient.getQueryData(skillsKeys.list());
+
+ // Optimistic update
+ queryClient.setQueryData(skillsKeys.list(), (old) => {
+ if (!old) return old;
+ return {
+ skills: old.skills.map((s) =>
+ s.name === skillName ? { ...s, enabled } : s
+ ),
+ };
+ });
+
+ return { previousSkills };
+ },
+ onError: (_error, _vars, context) => {
+ if (context?.previousSkills) {
+ queryClient.setQueryData(skillsKeys.list(), context.previousSkills);
+ }
+ },
+ onSettled: () => {
+ queryClient.invalidateQueries({ queryKey: skillsKeys.all });
+ },
+ });
+
+ return {
+ toggleSkill: (skillName, enabled) => mutation.mutateAsync({ skillName, enabled }),
+ isToggling: mutation.isPending,
+ error: mutation.error,
+ };
+}
+
+/**
+ * Combined hook for all skill mutations
+ */
+export function useSkillMutations() {
+ const toggle = useToggleSkill();
+
+ return {
+ toggleSkill: toggle.toggleSkill,
+ isToggling: toggle.isToggling,
+ isMutating: toggle.isToggling,
+ };
+}
diff --git a/ccw/frontend/src/hooks/useTemplates.ts b/ccw/frontend/src/hooks/useTemplates.ts
new file mode 100644
index 00000000..f35859cf
--- /dev/null
+++ b/ccw/frontend/src/hooks/useTemplates.ts
@@ -0,0 +1,184 @@
+// ========================================
+// useTemplates Hook
+// ========================================
+// TanStack Query hooks for template API operations
+
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import type { FlowTemplate, TemplateInstallRequest, TemplateExportRequest } from '../types/execution';
+import type { Flow } from '../types/flow';
+
+// API base URL
+const API_BASE = '/api/orchestrator';
+
+// Query keys
+export const templateKeys = {
+ all: ['templates'] as const,
+ lists: () => [...templateKeys.all, 'list'] as const,
+ list: (filters?: Record) => [...templateKeys.lists(), filters] as const,
+ details: () => [...templateKeys.all, 'detail'] as const,
+ detail: (id: string) => [...templateKeys.details(), id] as const,
+ categories: () => [...templateKeys.all, 'categories'] as const,
+};
+
+// API response types
+interface TemplatesListResponse {
+ templates: FlowTemplate[];
+ total: number;
+ categories: string[];
+}
+
+interface TemplateDetailResponse extends FlowTemplate {
+ flow: Flow;
+}
+
+interface InstallTemplateResponse {
+ flow: Flow;
+ message: string;
+}
+
+interface ExportTemplateResponse {
+ template: FlowTemplate;
+ message: string;
+}
+
+// ========== Fetch Functions ==========
+
+async function fetchTemplates(category?: string): Promise {
+ const url = category
+ ? `${API_BASE}/templates?category=${encodeURIComponent(category)}`
+ : `${API_BASE}/templates`;
+ const response = await fetch(url);
+ if (!response.ok) {
+ throw new Error(`Failed to fetch templates: ${response.statusText}`);
+ }
+ return response.json();
+}
+
+async function fetchTemplate(id: string): Promise {
+ const response = await fetch(`${API_BASE}/templates/${id}`);
+ if (!response.ok) {
+ throw new Error(`Failed to fetch template: ${response.statusText}`);
+ }
+ return response.json();
+}
+
+async function installTemplate(request: TemplateInstallRequest): Promise {
+ const response = await fetch(`${API_BASE}/templates/install`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(request),
+ });
+ if (!response.ok) {
+ throw new Error(`Failed to install template: ${response.statusText}`);
+ }
+ return response.json();
+}
+
+async function exportTemplate(request: TemplateExportRequest): Promise {
+ const response = await fetch(`${API_BASE}/templates/export`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(request),
+ });
+ if (!response.ok) {
+ throw new Error(`Failed to export template: ${response.statusText}`);
+ }
+ return response.json();
+}
+
+async function deleteTemplate(id: string): Promise {
+ const response = await fetch(`${API_BASE}/templates/${id}`, {
+ method: 'DELETE',
+ });
+ if (!response.ok) {
+ throw new Error(`Failed to delete template: ${response.statusText}`);
+ }
+}
+
+// ========== Query Hooks ==========
+
+/**
+ * Fetch all templates
+ */
+export function useTemplates(category?: string) {
+ return useQuery({
+ queryKey: templateKeys.list({ category }),
+ queryFn: () => fetchTemplates(category),
+ staleTime: 60000, // 1 minute
+ });
+}
+
+/**
+ * Fetch a single template by ID
+ */
+export function useTemplate(id: string | null) {
+ return useQuery({
+ queryKey: templateKeys.detail(id ?? ''),
+ queryFn: () => fetchTemplate(id!),
+ enabled: !!id,
+ staleTime: 60000,
+ });
+}
+
+// ========== Mutation Hooks ==========
+
+/**
+ * Install a template as a new flow
+ */
+export function useInstallTemplate() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: installTemplate,
+ onSuccess: () => {
+ // Invalidate flows list to show the new flow
+ queryClient.invalidateQueries({ queryKey: ['flows'] });
+ },
+ });
+}
+
+/**
+ * Export a flow as a template
+ */
+export function useExportTemplate() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: exportTemplate,
+ onSuccess: (result) => {
+ // Add to templates list
+ queryClient.setQueryData(templateKeys.lists(), (old) => {
+ if (!old) return { templates: [result.template], total: 1, categories: [] };
+ return {
+ ...old,
+ templates: [...old.templates, result.template],
+ total: old.total + 1,
+ };
+ });
+ queryClient.invalidateQueries({ queryKey: templateKeys.lists() });
+ },
+ });
+}
+
+/**
+ * Delete a template
+ */
+export function useDeleteTemplate() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: deleteTemplate,
+ onSuccess: (_, deletedId) => {
+ // Remove from cache
+ queryClient.removeQueries({ queryKey: templateKeys.detail(deletedId) });
+ queryClient.setQueryData(templateKeys.lists(), (old) => {
+ if (!old) return old;
+ return {
+ ...old,
+ templates: old.templates.filter((t) => t.id !== deletedId),
+ total: old.total - 1,
+ };
+ });
+ },
+ });
+}
diff --git a/ccw/frontend/src/hooks/useTheme.ts b/ccw/frontend/src/hooks/useTheme.ts
new file mode 100644
index 00000000..70d448a0
--- /dev/null
+++ b/ccw/frontend/src/hooks/useTheme.ts
@@ -0,0 +1,62 @@
+// ========================================
+// useTheme Hook
+// ========================================
+// Convenient hook for theme management
+
+import { useCallback } from 'react';
+import { useAppStore, selectTheme, selectResolvedTheme } from '../stores/appStore';
+import type { Theme } from '../types/store';
+
+export interface UseThemeReturn {
+ /** Current theme preference ('light', 'dark', 'system') */
+ theme: Theme;
+ /** Resolved theme based on preference and system settings */
+ resolvedTheme: 'light' | 'dark';
+ /** Whether the resolved theme is dark */
+ isDark: boolean;
+ /** Set theme preference */
+ setTheme: (theme: Theme) => void;
+ /** Toggle between light and dark (ignores system) */
+ toggleTheme: () => void;
+}
+
+/**
+ * Hook for managing theme state
+ * @returns Theme state and actions
+ *
+ * @example
+ * ```tsx
+ * const { theme, isDark, setTheme, toggleTheme } = useTheme();
+ *
+ * return (
+ *
+ * );
+ * ```
+ */
+export function useTheme(): UseThemeReturn {
+ const theme = useAppStore(selectTheme);
+ const resolvedTheme = useAppStore(selectResolvedTheme);
+ const setThemeAction = useAppStore((state) => state.setTheme);
+ const toggleThemeAction = useAppStore((state) => state.toggleTheme);
+
+ const setTheme = useCallback(
+ (newTheme: Theme) => {
+ setThemeAction(newTheme);
+ },
+ [setThemeAction]
+ );
+
+ const toggleTheme = useCallback(() => {
+ toggleThemeAction();
+ }, [toggleThemeAction]);
+
+ return {
+ theme,
+ resolvedTheme,
+ isDark: resolvedTheme === 'dark',
+ setTheme,
+ toggleTheme,
+ };
+}
diff --git a/ccw/frontend/src/hooks/useWebSocket.ts b/ccw/frontend/src/hooks/useWebSocket.ts
new file mode 100644
index 00000000..6aecd810
--- /dev/null
+++ b/ccw/frontend/src/hooks/useWebSocket.ts
@@ -0,0 +1,254 @@
+// ========================================
+// useWebSocket Hook
+// ========================================
+// Typed WebSocket connection management with auto-reconnect
+
+import { useEffect, useRef, useCallback } from 'react';
+import { useNotificationStore } from '@/stores';
+import { useExecutionStore } from '@/stores/executionStore';
+import { useFlowStore } from '@/stores';
+import {
+ OrchestratorMessageSchema,
+ type OrchestratorWebSocketMessage,
+ type ExecutionLog,
+} from '../types/execution';
+
+// Constants
+const RECONNECT_DELAY_BASE = 1000; // 1 second
+const RECONNECT_DELAY_MAX = 30000; // 30 seconds
+const RECONNECT_DELAY_MULTIPLIER = 1.5;
+
+interface UseWebSocketOptions {
+ enabled?: boolean;
+ onMessage?: (message: OrchestratorWebSocketMessage) => void;
+}
+
+interface UseWebSocketReturn {
+ isConnected: boolean;
+ send: (message: unknown) => void;
+ reconnect: () => void;
+}
+
+export function useWebSocket(options: UseWebSocketOptions = {}): UseWebSocketReturn {
+ const { enabled = true, onMessage } = options;
+
+ const wsRef = useRef(null);
+ const reconnectTimeoutRef = useRef(null);
+ const reconnectDelayRef = useRef(RECONNECT_DELAY_BASE);
+
+ // Notification store for connection status
+ const setWsStatus = useNotificationStore((state) => state.setWsStatus);
+ const setWsLastMessage = useNotificationStore((state) => state.setWsLastMessage);
+ const incrementReconnectAttempts = useNotificationStore((state) => state.incrementReconnectAttempts);
+ const resetReconnectAttempts = useNotificationStore((state) => state.resetReconnectAttempts);
+
+ // Execution store for state updates
+ const setExecutionStatus = useExecutionStore((state) => state.setExecutionStatus);
+ const setNodeStarted = useExecutionStore((state) => state.setNodeStarted);
+ const setNodeCompleted = useExecutionStore((state) => state.setNodeCompleted);
+ const setNodeFailed = useExecutionStore((state) => state.setNodeFailed);
+ const addLog = useExecutionStore((state) => state.addLog);
+ const completeExecution = useExecutionStore((state) => state.completeExecution);
+ const currentExecution = useExecutionStore((state) => state.currentExecution);
+
+ // Flow store for node status updates on canvas
+ const updateNode = useFlowStore((state) => state.updateNode);
+
+ // Handle incoming WebSocket messages
+ const handleMessage = useCallback(
+ (event: MessageEvent) => {
+ try {
+ const data = JSON.parse(event.data);
+
+ // Store last message for debugging
+ setWsLastMessage(data);
+
+ // Check if this is an orchestrator message
+ if (!data.type?.startsWith('ORCHESTRATOR_')) {
+ return;
+ }
+
+ // Validate message with zod schema
+ const parsed = OrchestratorMessageSchema.safeParse(data);
+ if (!parsed.success) {
+ console.warn('[WebSocket] Invalid orchestrator message:', parsed.error.issues);
+ return;
+ }
+
+ // Cast validated data to our TypeScript interface
+ const message = parsed.data as OrchestratorWebSocketMessage;
+
+ // Only process messages for current execution
+ if (currentExecution && message.execId !== currentExecution.execId) {
+ return;
+ }
+
+ // Dispatch to execution store based on message type
+ switch (message.type) {
+ case 'ORCHESTRATOR_STATE_UPDATE':
+ setExecutionStatus(message.status, message.currentNodeId);
+ // Check for completion
+ if (message.status === 'completed' || message.status === 'failed') {
+ completeExecution(message.status);
+ }
+ break;
+
+ case 'ORCHESTRATOR_NODE_STARTED':
+ setNodeStarted(message.nodeId);
+ // Update canvas node status
+ updateNode(message.nodeId, { executionStatus: 'running' });
+ break;
+
+ case 'ORCHESTRATOR_NODE_COMPLETED':
+ setNodeCompleted(message.nodeId, message.result);
+ // Update canvas node status
+ updateNode(message.nodeId, {
+ executionStatus: 'completed',
+ executionResult: message.result,
+ });
+ break;
+
+ case 'ORCHESTRATOR_NODE_FAILED':
+ setNodeFailed(message.nodeId, message.error);
+ // Update canvas node status
+ updateNode(message.nodeId, {
+ executionStatus: 'failed',
+ executionError: message.error,
+ });
+ break;
+
+ case 'ORCHESTRATOR_LOG':
+ addLog(message.log as ExecutionLog);
+ break;
+ }
+
+ // Call custom message handler if provided
+ onMessage?.(message);
+ } catch (error) {
+ console.error('[WebSocket] Failed to parse message:', error);
+ }
+ },
+ [
+ currentExecution,
+ setWsLastMessage,
+ setExecutionStatus,
+ setNodeStarted,
+ setNodeCompleted,
+ setNodeFailed,
+ addLog,
+ completeExecution,
+ updateNode,
+ onMessage,
+ ]
+ );
+
+ // Connect to WebSocket
+ const connect = useCallback(() => {
+ if (!enabled) return;
+
+ // Construct WebSocket URL
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
+ const wsUrl = `${protocol}//${window.location.host}/ws`;
+
+ try {
+ setWsStatus('connecting');
+
+ const ws = new WebSocket(wsUrl);
+ wsRef.current = ws;
+
+ ws.onopen = () => {
+ console.log('[WebSocket] Connected');
+ setWsStatus('connected');
+ resetReconnectAttempts();
+ reconnectDelayRef.current = RECONNECT_DELAY_BASE;
+ };
+
+ ws.onmessage = handleMessage;
+
+ ws.onclose = () => {
+ console.log('[WebSocket] Disconnected');
+ setWsStatus('disconnected');
+ wsRef.current = null;
+ scheduleReconnect();
+ };
+
+ ws.onerror = (error) => {
+ console.error('[WebSocket] Error:', error);
+ setWsStatus('error');
+ };
+ } catch (error) {
+ console.error('[WebSocket] Failed to connect:', error);
+ setWsStatus('error');
+ scheduleReconnect();
+ }
+ }, [enabled, handleMessage, setWsStatus, resetReconnectAttempts]);
+
+ // Schedule reconnection with exponential backoff
+ const scheduleReconnect = useCallback(() => {
+ if (reconnectTimeoutRef.current) {
+ clearTimeout(reconnectTimeoutRef.current);
+ }
+
+ const delay = reconnectDelayRef.current;
+ console.log(`[WebSocket] Reconnecting in ${delay}ms...`);
+
+ setWsStatus('reconnecting');
+ incrementReconnectAttempts();
+
+ reconnectTimeoutRef.current = setTimeout(() => {
+ connect();
+ }, delay);
+
+ // Increase delay for next attempt (exponential backoff)
+ reconnectDelayRef.current = Math.min(
+ reconnectDelayRef.current * RECONNECT_DELAY_MULTIPLIER,
+ RECONNECT_DELAY_MAX
+ );
+ }, [connect, setWsStatus, incrementReconnectAttempts]);
+
+ // Send message through WebSocket
+ const send = useCallback((message: unknown) => {
+ if (wsRef.current?.readyState === WebSocket.OPEN) {
+ wsRef.current.send(JSON.stringify(message));
+ } else {
+ console.warn('[WebSocket] Cannot send message: not connected');
+ }
+ }, []);
+
+ // Manual reconnect
+ const reconnect = useCallback(() => {
+ if (wsRef.current) {
+ wsRef.current.close();
+ }
+ reconnectDelayRef.current = RECONNECT_DELAY_BASE;
+ connect();
+ }, [connect]);
+
+ // Check connection status
+ const isConnected = wsRef.current?.readyState === WebSocket.OPEN;
+
+ // Connect on mount, cleanup on unmount
+ useEffect(() => {
+ if (enabled) {
+ connect();
+ }
+
+ return () => {
+ if (reconnectTimeoutRef.current) {
+ clearTimeout(reconnectTimeoutRef.current);
+ }
+ if (wsRef.current) {
+ wsRef.current.close();
+ wsRef.current = null;
+ }
+ };
+ }, [enabled, connect]);
+
+ return {
+ isConnected,
+ send,
+ reconnect,
+ };
+}
+
+export default useWebSocket;
diff --git a/ccw/frontend/src/index.css b/ccw/frontend/src/index.css
new file mode 100644
index 00000000..ad2c3d1b
--- /dev/null
+++ b/ccw/frontend/src/index.css
@@ -0,0 +1,117 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+/* CSS Custom Properties - Light Mode */
+:root {
+ --background: 0 0% 98%;
+ --foreground: 0 0% 13%;
+ --card: 0 0% 100%;
+ --card-foreground: 0 0% 13%;
+ --border: 0 0% 90%;
+ --input: 0 0% 90%;
+ --ring: 220 65% 50%;
+ --primary: 220 65% 50%;
+ --primary-foreground: 0 0% 100%;
+ --primary-light: 220 65% 95%;
+ --secondary: 220 60% 65%;
+ --secondary-foreground: 0 0% 100%;
+ --accent: 220 40% 95%;
+ --accent-foreground: 0 0% 13%;
+ --destructive: 8 75% 55%;
+ --destructive-foreground: 0 0% 100%;
+ --muted: 0 0% 96%;
+ --muted-foreground: 0 0% 45%;
+ --sidebar-background: 0 0% 97%;
+ --sidebar-foreground: 0 0% 13%;
+ --hover: 0 0% 93%;
+ --success: 142 71% 45%;
+ --success-light: 142 76% 90%;
+ --warning: 38 92% 50%;
+ --warning-light: 48 96% 89%;
+ --info: 210 80% 55%;
+ --info-light: 210 80% 92%;
+ --indigo: 239 65% 60%;
+ --indigo-light: 239 65% 92%;
+ --orange: 25 90% 55%;
+ --orange-light: 25 90% 92%;
+}
+
+/* Dark Mode */
+[data-theme="dark"] {
+ --background: 220 13% 10%;
+ --foreground: 0 0% 90%;
+ --card: 220 13% 14%;
+ --card-foreground: 0 0% 90%;
+ --border: 220 13% 20%;
+ --input: 220 13% 20%;
+ --ring: 220 65% 55%;
+ --primary: 220 65% 55%;
+ --primary-foreground: 0 0% 100%;
+ --primary-light: 220 50% 25%;
+ --secondary: 220 60% 60%;
+ --secondary-foreground: 0 0% 100%;
+ --accent: 220 30% 20%;
+ --accent-foreground: 0 0% 90%;
+ --destructive: 8 70% 50%;
+ --destructive-foreground: 0 0% 100%;
+ --muted: 220 13% 18%;
+ --muted-foreground: 0 0% 55%;
+ --sidebar-background: 220 13% 12%;
+ --sidebar-foreground: 0 0% 90%;
+ --hover: 220 13% 22%;
+ --success: 142 71% 40%;
+ --success-light: 142 50% 20%;
+ --warning: 38 85% 45%;
+ --warning-light: 40 50% 20%;
+ --info: 210 75% 50%;
+ --info-light: 210 50% 20%;
+ --indigo: 239 60% 55%;
+ --indigo-light: 239 40% 20%;
+ --orange: 25 85% 50%;
+ --orange-light: 25 50% 20%;
+}
+
+/* Base styles */
+@layer base {
+ * {
+ @apply border-border;
+ }
+
+ body {
+ @apply bg-background text-foreground;
+ }
+}
+
+/* Scrollbar styling */
+::-webkit-scrollbar {
+ width: 6px;
+ height: 6px;
+}
+
+::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+::-webkit-scrollbar-thumb {
+ background: hsl(var(--border));
+ border-radius: 3px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: hsl(var(--muted-foreground));
+}
+
+/* Animations */
+@keyframes spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+.animate-spin {
+ animation: spin 1s linear infinite;
+}
diff --git a/ccw/frontend/src/lib/api.ts b/ccw/frontend/src/lib/api.ts
new file mode 100644
index 00000000..16c75ec3
--- /dev/null
+++ b/ccw/frontend/src/lib/api.ts
@@ -0,0 +1,604 @@
+// ========================================
+// API Client
+// ========================================
+// Typed fetch functions for API communication with CSRF token handling
+
+import type { SessionMetadata, TaskData } from '../types/store';
+
+// ========== Types ==========
+
+export interface DashboardStats {
+ totalSessions: number;
+ activeSessions: number;
+ archivedSessions: number;
+ totalTasks: number;
+ completedTasks: number;
+ pendingTasks: number;
+ failedTasks: number;
+ todayActivity: number;
+}
+
+export interface SessionsResponse {
+ activeSessions: SessionMetadata[];
+ archivedSessions: SessionMetadata[];
+}
+
+export interface CreateSessionInput {
+ session_id: string;
+ title?: string;
+ description?: string;
+ type?: 'workflow' | 'review' | 'lite-plan' | 'lite-fix';
+}
+
+export interface UpdateSessionInput {
+ title?: string;
+ description?: string;
+ status?: SessionMetadata['status'];
+}
+
+export interface ApiError {
+ message: string;
+ status: number;
+ code?: string;
+}
+
+// ========== CSRF Token Handling ==========
+
+/**
+ * Get CSRF token from cookie
+ */
+function getCsrfToken(): string | null {
+ const match = document.cookie.match(/XSRF-TOKEN=([^;]+)/);
+ return match ? decodeURIComponent(match[1]) : null;
+}
+
+// ========== Base Fetch Wrapper ==========
+
+/**
+ * Base fetch wrapper with CSRF token and error handling
+ */
+async function fetchApi(
+ url: string,
+ options: RequestInit = {}
+): Promise {
+ const headers = new Headers(options.headers);
+
+ // Add CSRF token for mutating requests
+ if (options.method && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(options.method)) {
+ const csrfToken = getCsrfToken();
+ if (csrfToken) {
+ headers.set('X-CSRF-Token', csrfToken);
+ }
+ }
+
+ // Set content type for JSON requests
+ if (options.body && typeof options.body === 'string') {
+ headers.set('Content-Type', 'application/json');
+ }
+
+ const response = await fetch(url, {
+ ...options,
+ headers,
+ credentials: 'same-origin',
+ });
+
+ if (!response.ok) {
+ const error: ApiError = {
+ message: response.statusText || 'Request failed',
+ status: response.status,
+ };
+
+ try {
+ const body = await response.json();
+ if (body.message) error.message = body.message;
+ if (body.code) error.code = body.code;
+ } catch {
+ // Ignore JSON parse errors
+ }
+
+ throw error;
+ }
+
+ // Handle no-content responses
+ if (response.status === 204) {
+ return undefined as T;
+ }
+
+ return response.json();
+}
+
+// ========== Dashboard API ==========
+
+/**
+ * Fetch dashboard statistics
+ */
+export async function fetchDashboardStats(): Promise {
+ const data = await fetchApi<{ statistics?: DashboardStats }>('/api/data');
+
+ // Extract statistics from response, with defaults
+ return {
+ totalSessions: data.statistics?.totalSessions ?? 0,
+ activeSessions: data.statistics?.activeSessions ?? 0,
+ archivedSessions: data.statistics?.archivedSessions ?? 0,
+ totalTasks: data.statistics?.totalTasks ?? 0,
+ completedTasks: data.statistics?.completedTasks ?? 0,
+ pendingTasks: data.statistics?.pendingTasks ?? 0,
+ failedTasks: data.statistics?.failedTasks ?? 0,
+ todayActivity: data.statistics?.todayActivity ?? 0,
+ };
+}
+
+// ========== Sessions API ==========
+
+/**
+ * Fetch all sessions (active and archived)
+ */
+export async function fetchSessions(): Promise {
+ const data = await fetchApi<{
+ activeSessions?: SessionMetadata[];
+ archivedSessions?: SessionMetadata[];
+ }>('/api/data');
+
+ return {
+ activeSessions: data.activeSessions ?? [],
+ archivedSessions: data.archivedSessions ?? [],
+ };
+}
+
+/**
+ * Fetch a single session by ID
+ */
+export async function fetchSession(sessionId: string): Promise {
+ return fetchApi(`/api/sessions/${encodeURIComponent(sessionId)}`);
+}
+
+/**
+ * Create a new session
+ */
+export async function createSession(input: CreateSessionInput): Promise {
+ return fetchApi('/api/sessions', {
+ method: 'POST',
+ body: JSON.stringify(input),
+ });
+}
+
+/**
+ * Update a session
+ */
+export async function updateSession(
+ sessionId: string,
+ input: UpdateSessionInput
+): Promise {
+ return fetchApi(`/api/sessions/${encodeURIComponent(sessionId)}`, {
+ method: 'PATCH',
+ body: JSON.stringify(input),
+ });
+}
+
+/**
+ * Archive a session
+ */
+export async function archiveSession(sessionId: string): Promise {
+ return fetchApi(`/api/sessions/${encodeURIComponent(sessionId)}/archive`, {
+ method: 'POST',
+ });
+}
+
+/**
+ * Delete a session
+ */
+export async function deleteSession(sessionId: string): Promise {
+ return fetchApi(`/api/sessions/${encodeURIComponent(sessionId)}`, {
+ method: 'DELETE',
+ });
+}
+
+// ========== Tasks API ==========
+
+/**
+ * Fetch tasks for a session
+ */
+export async function fetchSessionTasks(sessionId: string): Promise {
+ return fetchApi(`/api/sessions/${encodeURIComponent(sessionId)}/tasks`);
+}
+
+/**
+ * Update a task status
+ */
+export async function updateTask(
+ sessionId: string,
+ taskId: string,
+ updates: Partial
+): Promise {
+ return fetchApi(
+ `/api/sessions/${encodeURIComponent(sessionId)}/tasks/${encodeURIComponent(taskId)}`,
+ {
+ method: 'PATCH',
+ body: JSON.stringify(updates),
+ }
+ );
+}
+
+// ========== Path Management API ==========
+
+/**
+ * Fetch recent paths
+ */
+export async function fetchRecentPaths(): Promise {
+ const data = await fetchApi<{ paths?: string[] }>('/api/recent-paths');
+ return data.paths ?? [];
+}
+
+/**
+ * Remove a recent path
+ */
+export async function removeRecentPath(path: string): Promise {
+ const data = await fetchApi<{ paths: string[] }>('/api/remove-recent-path', {
+ method: 'POST',
+ body: JSON.stringify({ path }),
+ });
+ return data.paths;
+}
+
+/**
+ * Switch to a different project path and load its data
+ */
+export async function loadDashboardData(path: string): Promise<{
+ activeSessions: SessionMetadata[];
+ archivedSessions: SessionMetadata[];
+ statistics: DashboardStats;
+ projectPath: string;
+ recentPaths: string[];
+}> {
+ return fetchApi(`/api/data?path=${encodeURIComponent(path)}`);
+}
+
+// ========== Loops API ==========
+
+export interface Loop {
+ id: string;
+ name?: string;
+ status: 'created' | 'running' | 'paused' | 'completed' | 'failed';
+ currentStep: number;
+ totalSteps: number;
+ createdAt: string;
+ updatedAt?: string;
+ startedAt?: string;
+ completedAt?: string;
+ prompt?: string;
+ tool?: string;
+ error?: string;
+ context?: {
+ workingDir?: string;
+ mode?: string;
+ };
+}
+
+export interface LoopsResponse {
+ loops: Loop[];
+ total: number;
+}
+
+/**
+ * Fetch all loops
+ */
+export async function fetchLoops(): Promise {
+ const data = await fetchApi<{ loops?: Loop[] }>('/api/loops');
+ return {
+ loops: data.loops ?? [],
+ total: data.loops?.length ?? 0,
+ };
+}
+
+/**
+ * Fetch a single loop by ID
+ */
+export async function fetchLoop(loopId: string): Promise {
+ return fetchApi(`/api/loops/${encodeURIComponent(loopId)}`);
+}
+
+/**
+ * Create a new loop
+ */
+export async function createLoop(input: {
+ prompt: string;
+ tool?: string;
+ mode?: string;
+}): Promise {
+ return fetchApi('/api/loops', {
+ method: 'POST',
+ body: JSON.stringify(input),
+ });
+}
+
+/**
+ * Update a loop's status (pause, resume, stop)
+ */
+export async function updateLoopStatus(
+ loopId: string,
+ action: 'pause' | 'resume' | 'stop'
+): Promise {
+ return fetchApi(`/api/loops/${encodeURIComponent(loopId)}/${action}`, {
+ method: 'POST',
+ });
+}
+
+/**
+ * Delete a loop
+ */
+export async function deleteLoop(loopId: string): Promise {
+ return fetchApi(`/api/loops/${encodeURIComponent(loopId)}`, {
+ method: 'DELETE',
+ });
+}
+
+// ========== Issues API ==========
+
+export interface IssueSolution {
+ id: string;
+ description: string;
+ approach?: string;
+ status: 'pending' | 'in_progress' | 'completed' | 'rejected';
+ estimatedEffort?: string;
+}
+
+export interface Issue {
+ id: string;
+ title: string;
+ context?: string;
+ status: 'open' | 'in_progress' | 'resolved' | 'closed' | 'completed';
+ priority: 'low' | 'medium' | 'high' | 'critical';
+ createdAt: string;
+ updatedAt?: string;
+ solutions?: IssueSolution[];
+ labels?: string[];
+ assignee?: string;
+}
+
+export interface IssueQueue {
+ tasks: string[];
+ solutions: string[];
+ conflicts: string[];
+ execution_groups: string[];
+ grouped_items: Record;
+}
+
+export interface IssuesResponse {
+ issues: Issue[];
+}
+
+/**
+ * Fetch all issues
+ */
+export async function fetchIssues(projectPath?: string): Promise {
+ const url = projectPath
+ ? `/api/issues?path=${encodeURIComponent(projectPath)}`
+ : '/api/issues';
+ const data = await fetchApi<{ issues?: Issue[] }>(url);
+ return {
+ issues: data.issues ?? [],
+ };
+}
+
+/**
+ * Fetch issue history
+ */
+export async function fetchIssueHistory(projectPath?: string): Promise {
+ const url = projectPath
+ ? `/api/issues/history?path=${encodeURIComponent(projectPath)}`
+ : '/api/issues/history';
+ const data = await fetchApi<{ issues?: Issue[] }>(url);
+ return {
+ issues: data.issues ?? [],
+ };
+}
+
+/**
+ * Fetch issue queue
+ */
+export async function fetchIssueQueue(projectPath?: string): Promise {
+ const url = projectPath
+ ? `/api/queue?path=${encodeURIComponent(projectPath)}`
+ : '/api/queue';
+ return fetchApi(url);
+}
+
+/**
+ * Create a new issue
+ */
+export async function createIssue(input: {
+ title: string;
+ context?: string;
+ priority?: Issue['priority'];
+}): Promise {
+ return fetchApi('/api/issues', {
+ method: 'POST',
+ body: JSON.stringify(input),
+ });
+}
+
+/**
+ * Update an issue
+ */
+export async function updateIssue(
+ issueId: string,
+ input: Partial
+): Promise {
+ return fetchApi(`/api/issues/${encodeURIComponent(issueId)}`, {
+ method: 'PATCH',
+ body: JSON.stringify(input),
+ });
+}
+
+/**
+ * Delete an issue
+ */
+export async function deleteIssue(issueId: string): Promise {
+ return fetchApi(`/api/issues/${encodeURIComponent(issueId)}`, {
+ method: 'DELETE',
+ });
+}
+
+// ========== Skills API ==========
+
+export interface Skill {
+ name: string;
+ description: string;
+ enabled: boolean;
+ triggers: string[];
+ category?: string;
+ source?: 'builtin' | 'custom' | 'community';
+ version?: string;
+ author?: string;
+}
+
+export interface SkillsResponse {
+ skills: Skill[];
+}
+
+/**
+ * Fetch all skills
+ */
+export async function fetchSkills(): Promise {
+ const data = await fetchApi<{ skills?: Skill[] }>('/api/skills');
+ return {
+ skills: data.skills ?? [],
+ };
+}
+
+/**
+ * Toggle skill enabled status
+ */
+export async function toggleSkill(skillName: string, enabled: boolean): Promise {
+ return fetchApi(`/api/skills/${encodeURIComponent(skillName)}`, {
+ method: 'PATCH',
+ body: JSON.stringify({ enabled }),
+ });
+}
+
+// ========== Commands API ==========
+
+export interface Command {
+ name: string;
+ description: string;
+ usage?: string;
+ examples?: string[];
+ category?: string;
+ aliases?: string[];
+ source?: 'builtin' | 'custom';
+}
+
+export interface CommandsResponse {
+ commands: Command[];
+}
+
+/**
+ * Fetch all commands
+ */
+export async function fetchCommands(): Promise {
+ const data = await fetchApi<{ commands?: Command[] }>('/api/commands');
+ return {
+ commands: data.commands ?? [],
+ };
+}
+
+// ========== Memory API ==========
+
+export interface CoreMemory {
+ id: string;
+ content: string;
+ createdAt: string;
+ updatedAt?: string;
+ source?: string;
+ tags?: string[];
+ size?: number;
+}
+
+export interface MemoryResponse {
+ memories: CoreMemory[];
+ totalSize: number;
+ claudeMdCount: number;
+}
+
+/**
+ * Fetch all memories
+ */
+export async function fetchMemories(): Promise {
+ const data = await fetchApi<{
+ memories?: CoreMemory[];
+ totalSize?: number;
+ claudeMdCount?: number;
+ }>('/api/memory');
+ return {
+ memories: data.memories ?? [],
+ totalSize: data.totalSize ?? 0,
+ claudeMdCount: data.claudeMdCount ?? 0,
+ };
+}
+
+/**
+ * Create a new memory entry
+ */
+export async function createMemory(input: {
+ content: string;
+ tags?: string[];
+}): Promise {
+ return fetchApi('/api/memory', {
+ method: 'POST',
+ body: JSON.stringify(input),
+ });
+}
+
+/**
+ * Update a memory entry
+ */
+export async function updateMemory(
+ memoryId: string,
+ input: Partial
+): Promise {
+ return fetchApi(`/api/memory/${encodeURIComponent(memoryId)}`, {
+ method: 'PATCH',
+ body: JSON.stringify(input),
+ });
+}
+
+/**
+ * Delete a memory entry
+ */
+export async function deleteMemory(memoryId: string): Promise {
+ return fetchApi