From 7ee51543d97f697b8e5bb1550b37eeb81d067b99 Mon Sep 17 00:00:00 2001 From: sayan Date: Tue, 28 Apr 2026 11:32:56 +0530 Subject: [PATCH] Merge Conflicts (#41) Co-authored-by: Sayan Datta Reviewed-on: https://git.desineuron.in/sagnik/Project_Velocity/pulls/41 --- .Agent Context/Codebase Analysis v1.1.md | 565 + ...elocity_Ipad_Production_Readiness_Audit.md | 3068 ++ .github/workflows/production-readiness.yml | 63 + app/dist/index.html | 29 +- .../.tmp/tsconfig.app.tsbuildinfo | 124 +- .../.vite/deps/@radix-ui_react-avatar.js | 14 +- .../.vite/deps/@radix-ui_react-avatar.js.map | 7 - .../deps/@radix-ui_react-dropdown-menu.js | 908 +- .../deps/@radix-ui_react-dropdown-menu.js.map | 7 - .../.vite/deps/@radix-ui_react-slot.js | 17 - .../.vite/deps/@radix-ui_react-slot.js.map | 7 - .../.vite/deps/@react-three_drei.js | 3704 +- .../.vite/deps/@react-three_drei.js.map | 7 - .../.vite/deps/@react-three_fiber.js | 69 - .../.vite/deps/@react-three_fiber.js.map | 7 - app/node_modules/.vite/deps/_metadata.json | 385 +- app/node_modules/.vite/deps/chunk-G3PMV62Z.js | 35 - .../.vite/deps/chunk-G3PMV62Z.js.map | 7 - app/node_modules/.vite/deps/chunk-OAEA5FZL.js | 32999 ---------------- .../.vite/deps/chunk-OAEA5FZL.js.map | 7 - app/node_modules/.vite/deps/chunk-U7P2NEEE.js | 21 - .../.vite/deps/chunk-U7P2NEEE.js.map | 7 - .../.vite/deps/class-variance-authority.js | 51 - .../deps/class-variance-authority.js.map | 7 - app/node_modules/.vite/deps/clsx.js | 9 - app/node_modules/.vite/deps/clsx.js.map | 7 - app/node_modules/.vite/deps/framer-motion.js | 12319 ------ .../.vite/deps/framer-motion.js.map | 7 - app/node_modules/.vite/deps/hls-Q6LDPZPT.js | 98 - .../.vite/deps/hls-Q6LDPZPT.js.map | 7 - app/node_modules/.vite/deps/lucide-react.js | 27351 ------------- .../.vite/deps/lucide-react.js.map | 7 - app/node_modules/.vite/deps/react-dom.js | 264 +- app/node_modules/.vite/deps/react-dom.js.map | 6 +- .../.vite/deps/react-dom_client.js | 8 - .../.vite/deps/react-dom_client.js.map | 7 - app/node_modules/.vite/deps/react.js | 3 +- .../.vite/deps/react_jsx-dev-runtime.js | 6 +- .../.vite/deps/react_jsx-dev-runtime.js.map | 4 +- .../.vite/deps/react_jsx-runtime.js | 276 +- .../.vite/deps/react_jsx-runtime.js.map | 6 +- app/node_modules/.vite/deps/recharts.js | 2506 +- app/node_modules/.vite/deps/recharts.js.map | 7 - app/node_modules/.vite/deps/tailwind-merge.js | 3095 -- .../.vite/deps/tailwind-merge.js.map | 7 - app/node_modules/.vite/deps/three.js | 877 - app/node_modules/.vite/deps/three.js.map | 7 - app/node_modules/.vite/deps/zustand.js | 14 - app/node_modules/.vite/deps/zustand.js.map | 7 - .../.vite/deps/zustand_middleware.js | 19 - .../.vite/deps/zustand_middleware.js.map | 7 - app/src/components/modules/Catalyst.tsx | 11 +- .../modules/CatalystDreamWeaverTab.tsx | 524 + app/src/lib/api.ts | 13 +- app/src/lib/crmApi.ts | 55 +- app/src/lib/dreamWeaverApi.ts | 197 + app/src/lib/velocityPlatformClient.ts | 39 +- app/src/lib/velocitySession.ts | 37 + app/src/oracle/lib/oracleApiClient.ts | 2 +- app/src/store/useMarketingStore.ts | 2 +- app/src/types/crmTypes.ts | 11 + backend/api/routes_admin_surface.py | 8 +- backend/api/routes_crm.py | 897 +- backend/api/routes_crm_imports.py | 691 +- backend/api/routes_inventory.py | 34 +- backend/api/routes_mobile_edge.py | 88 +- backend/api/routes_observability.py | 24 + backend/auth/dependencies.py | 15 +- backend/auth/routes.py | 105 + backend/auth/service.py | 123 + backend/auth/user_directory.py | 45 + backend/crm/canonical_schema.py | 88 + backend/db/schema.sql | 2 + backend/db/schema_crm_canonical.sql | 28 + backend/main.py | 172 +- backend/migrations/__init__.py | 2 + backend/migrations/runner.py | 102 + .../202604230001_observability_foundation.sql | 22 + ...2604260001_mobile_edge_calendar_events.sql | 30 + ...02604260002_calendar_event_done_status.sql | 6 + backend/observability.py | 103 + backend/oracle/schema_extension_v2.sql | 2 +- backend/scripts/seed_ipad_investor_demo.py | 850 + .../client_graph/aggregation_service.py | 65 +- backend/services/imports/ingest_service.py | 18 +- backend/tests/test_auth_tenant_contract.py | 212 + backend/tests/test_canonical_crm_auth.py | 162 + .../test_canonical_crm_tenant_scoping.py | 517 + backend/tests/test_crm_routes.py | 155 +- .../tests/test_dream_weaver_gateway_auth.py | 30 + .../tests/test_legacy_crm_canonical_bridge.py | 238 + backend/tests/test_legacy_crm_write_bridge.py | 243 + .../test_migrations_and_observability.py | 40 + .../test_surface_route_tenant_scoping.py | 470 + comfy_engine/scripts/dw_gateway_v2.py | 202 +- comfy_engine/scripts/gateway_auth.py | 49 + dw_gateway_v2_min.py | 172 +- iOS/App/ContentView.swift | 181 - iOS/Core/Config/AppConfig.swift | 19 - iOS/Core/Math/SunMath.swift | 92 - iOS/Core/Networking/ComfyClient.swift | 100 - iOS/Core/Networking/VelocityAPIClient.swift | 258 - iOS/Core/State/AppStore.swift | 256 - iOS/Features/Calendar/CalendarView.swift | 363 - iOS/Features/Dashboard/DashboardView.swift | 442 - iOS/Features/Inventory/ARSunOverlayView.swift | 118 - iOS/Features/Inventory/InventoryView.swift | 439 - iOS/Features/Oracle/OracleView.swift | 960 - iOS/Features/Sentinel/SentinelView.swift | 413 - iOS/Features/Settings/SettingsView.swift | 141 - iOS/README.md | 10 + .../velocity.xcodeproj/project.pbxproj | 171 +- .../contents.xcworkspacedata | 0 .../xcshareddata/swiftpm/Package.resolved | 0 .../velocity/App/ConfigurationGateView.swift | 42 + .../velocity/App/ContentView.swift | 53 +- .../velocity}/App/VelocityApp.swift | 0 .../AccentColor.colorset/Contents.json | 0 .../AppIcon.appiconset/Contents.json | 0 .../velocity/Assets.xcassets/Contents.json | 0 .../velocity/Core/Config/AppConfig.swift | 302 + .../Core/Config/SessionConfiguration.swift | 247 + .../velocity/Core/Config/SessionStore.swift | 204 + .../velocity/Core/Math/SunMath.swift | 0 .../Core/Networking/ComfyClient.swift | 176 +- .../Core/Networking/VelocityAPIClient.swift | 2165 + .../velocity/Core/State/AppStore.swift | 815 + .../Core/State/AppStoreRefreshPolicy.swift | 31 + .../velocity}/Core/UI/GlassBlurView.swift | 0 .../velocity}/Core/UI/VelocityTheme.swift | 0 .../Features/Calendar/CalendarView.swift | 1260 + .../Features/Clients/ClientsView.swift | 490 + .../Communications/CommunicationsView.swift | 17 +- .../Features/Dashboard/DashboardView.swift | 84 +- .../Features/Imports/ImportsView.swift | 467 + .../Features/Inventory/ARSunOverlayView.swift | 4 +- .../Inventory/InventoryModeAvailability.swift | 35 + .../Features/Inventory/InventoryView.swift | 88 +- .../Inventory/SimulatorSunOverlayView.swift | 4 +- .../Inventory/SunseekerViewModel.swift | 0 .../Oracle/OracleModeAvailability.swift | 19 + .../velocity/Features/Oracle/OracleView.swift | 1223 + .../Features/Sentinel/SentinelScope.swift | 28 + .../Features/Sentinel/SentinelView.swift | 17 +- .../Settings/SessionConfigurationPanel.swift | 267 + .../Features/Settings/SettingsView.swift | 224 + .../velocity/Info.plist | 23 +- .../velocityTests/VelocitySmokeTests.swift | 973 + iOS/velocity/velocity/App/VelocityApp.swift | 11 - .../velocity/Core/Config/AppConfig.swift | 37 - .../Core/Networking/VelocityAPIClient.swift | 363 - .../velocity/Core/State/AppStore.swift | 149 - .../velocity/Core/UI/GlassBlurView.swift | 17 - .../velocity/Core/UI/VelocityTheme.swift | 60 - .../Features/Calendar/CalendarView.swift | 363 - .../Communications/CommunicationsView.swift | 448 - .../velocity/Features/Oracle/OracleView.swift | 325 - .../Features/Settings/SettingsView.swift | 144 - 158 files changed, 23889 insertions(+), 87196 deletions(-) create mode 100644 .Agent Context/Codebase Analysis v1.1.md create mode 100644 .Agent Context/Sayan's Ipad docs/Velocity_Ipad_Production_Readiness_Audit.md create mode 100644 .github/workflows/production-readiness.yml delete mode 100644 app/node_modules/.vite/deps/@radix-ui_react-avatar.js.map delete mode 100644 app/node_modules/.vite/deps/@radix-ui_react-dropdown-menu.js.map delete mode 100644 app/node_modules/.vite/deps/@radix-ui_react-slot.js delete mode 100644 app/node_modules/.vite/deps/@radix-ui_react-slot.js.map delete mode 100644 app/node_modules/.vite/deps/@react-three_drei.js.map delete mode 100644 app/node_modules/.vite/deps/@react-three_fiber.js delete mode 100644 app/node_modules/.vite/deps/@react-three_fiber.js.map delete mode 100644 app/node_modules/.vite/deps/chunk-G3PMV62Z.js delete mode 100644 app/node_modules/.vite/deps/chunk-G3PMV62Z.js.map delete mode 100644 app/node_modules/.vite/deps/chunk-OAEA5FZL.js delete mode 100644 app/node_modules/.vite/deps/chunk-OAEA5FZL.js.map delete mode 100644 app/node_modules/.vite/deps/chunk-U7P2NEEE.js delete mode 100644 app/node_modules/.vite/deps/chunk-U7P2NEEE.js.map delete mode 100644 app/node_modules/.vite/deps/class-variance-authority.js delete mode 100644 app/node_modules/.vite/deps/class-variance-authority.js.map delete mode 100644 app/node_modules/.vite/deps/clsx.js delete mode 100644 app/node_modules/.vite/deps/clsx.js.map delete mode 100644 app/node_modules/.vite/deps/framer-motion.js delete mode 100644 app/node_modules/.vite/deps/framer-motion.js.map delete mode 100644 app/node_modules/.vite/deps/hls-Q6LDPZPT.js delete mode 100644 app/node_modules/.vite/deps/hls-Q6LDPZPT.js.map delete mode 100644 app/node_modules/.vite/deps/lucide-react.js delete mode 100644 app/node_modules/.vite/deps/lucide-react.js.map delete mode 100644 app/node_modules/.vite/deps/react-dom_client.js delete mode 100644 app/node_modules/.vite/deps/react-dom_client.js.map delete mode 100644 app/node_modules/.vite/deps/recharts.js.map delete mode 100644 app/node_modules/.vite/deps/tailwind-merge.js delete mode 100644 app/node_modules/.vite/deps/tailwind-merge.js.map delete mode 100644 app/node_modules/.vite/deps/three.js delete mode 100644 app/node_modules/.vite/deps/three.js.map delete mode 100644 app/node_modules/.vite/deps/zustand.js delete mode 100644 app/node_modules/.vite/deps/zustand.js.map delete mode 100644 app/node_modules/.vite/deps/zustand_middleware.js delete mode 100644 app/node_modules/.vite/deps/zustand_middleware.js.map create mode 100644 app/src/components/modules/CatalystDreamWeaverTab.tsx create mode 100644 app/src/lib/dreamWeaverApi.ts create mode 100644 app/src/lib/velocitySession.ts create mode 100644 backend/api/routes_observability.py create mode 100644 backend/auth/routes.py create mode 100644 backend/auth/service.py create mode 100644 backend/auth/user_directory.py create mode 100644 backend/crm/canonical_schema.py create mode 100644 backend/migrations/__init__.py create mode 100644 backend/migrations/runner.py create mode 100644 backend/migrations/versions/202604230001_observability_foundation.sql create mode 100644 backend/migrations/versions/202604260001_mobile_edge_calendar_events.sql create mode 100644 backend/migrations/versions/202604260002_calendar_event_done_status.sql create mode 100644 backend/observability.py create mode 100644 backend/scripts/seed_ipad_investor_demo.py create mode 100644 backend/tests/test_auth_tenant_contract.py create mode 100644 backend/tests/test_canonical_crm_auth.py create mode 100644 backend/tests/test_canonical_crm_tenant_scoping.py create mode 100644 backend/tests/test_dream_weaver_gateway_auth.py create mode 100644 backend/tests/test_legacy_crm_canonical_bridge.py create mode 100644 backend/tests/test_legacy_crm_write_bridge.py create mode 100644 backend/tests/test_migrations_and_observability.py create mode 100644 backend/tests/test_surface_route_tenant_scoping.py create mode 100644 comfy_engine/scripts/gateway_auth.py delete mode 100644 iOS/App/ContentView.swift delete mode 100644 iOS/Core/Config/AppConfig.swift delete mode 100644 iOS/Core/Math/SunMath.swift delete mode 100644 iOS/Core/Networking/ComfyClient.swift delete mode 100644 iOS/Core/Networking/VelocityAPIClient.swift delete mode 100644 iOS/Core/State/AppStore.swift delete mode 100644 iOS/Features/Calendar/CalendarView.swift delete mode 100644 iOS/Features/Dashboard/DashboardView.swift delete mode 100644 iOS/Features/Inventory/ARSunOverlayView.swift delete mode 100644 iOS/Features/Inventory/InventoryView.swift delete mode 100644 iOS/Features/Oracle/OracleView.swift delete mode 100644 iOS/Features/Sentinel/SentinelView.swift delete mode 100644 iOS/Features/Settings/SettingsView.swift create mode 100644 iOS/README.md rename iOS/{velocity => velocity-ipad}/velocity.xcodeproj/project.pbxproj (69%) rename iOS/{velocity => velocity-ipad}/velocity.xcodeproj/project.xcworkspace/contents.xcworkspacedata (100%) rename iOS/{velocity => velocity-ipad}/velocity.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved (100%) create mode 100644 iOS/velocity-ipad/velocity/App/ConfigurationGateView.swift rename iOS/{velocity => velocity-ipad}/velocity/App/ContentView.swift (79%) rename iOS/{ => velocity-ipad/velocity}/App/VelocityApp.swift (100%) rename iOS/{velocity => velocity-ipad}/velocity/Assets.xcassets/AccentColor.colorset/Contents.json (100%) rename iOS/{velocity => velocity-ipad}/velocity/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename iOS/{velocity => velocity-ipad}/velocity/Assets.xcassets/Contents.json (100%) create mode 100644 iOS/velocity-ipad/velocity/Core/Config/AppConfig.swift create mode 100644 iOS/velocity-ipad/velocity/Core/Config/SessionConfiguration.swift create mode 100644 iOS/velocity-ipad/velocity/Core/Config/SessionStore.swift rename iOS/{velocity => velocity-ipad}/velocity/Core/Math/SunMath.swift (100%) rename iOS/{velocity => velocity-ipad}/velocity/Core/Networking/ComfyClient.swift (51%) create mode 100644 iOS/velocity-ipad/velocity/Core/Networking/VelocityAPIClient.swift create mode 100644 iOS/velocity-ipad/velocity/Core/State/AppStore.swift create mode 100644 iOS/velocity-ipad/velocity/Core/State/AppStoreRefreshPolicy.swift rename iOS/{ => velocity-ipad/velocity}/Core/UI/GlassBlurView.swift (100%) rename iOS/{ => velocity-ipad/velocity}/Core/UI/VelocityTheme.swift (100%) create mode 100644 iOS/velocity-ipad/velocity/Features/Calendar/CalendarView.swift create mode 100644 iOS/velocity-ipad/velocity/Features/Clients/ClientsView.swift rename iOS/{ => velocity-ipad/velocity}/Features/Communications/CommunicationsView.swift (95%) rename iOS/{velocity => velocity-ipad}/velocity/Features/Dashboard/DashboardView.swift (72%) create mode 100644 iOS/velocity-ipad/velocity/Features/Imports/ImportsView.swift rename iOS/{velocity => velocity-ipad}/velocity/Features/Inventory/ARSunOverlayView.swift (99%) create mode 100644 iOS/velocity-ipad/velocity/Features/Inventory/InventoryModeAvailability.swift rename iOS/{velocity => velocity-ipad}/velocity/Features/Inventory/InventoryView.swift (90%) rename iOS/{velocity => velocity-ipad}/velocity/Features/Inventory/SimulatorSunOverlayView.swift (98%) rename iOS/{velocity => velocity-ipad}/velocity/Features/Inventory/SunseekerViewModel.swift (100%) create mode 100644 iOS/velocity-ipad/velocity/Features/Oracle/OracleModeAvailability.swift create mode 100644 iOS/velocity-ipad/velocity/Features/Oracle/OracleView.swift create mode 100644 iOS/velocity-ipad/velocity/Features/Sentinel/SentinelScope.swift rename iOS/{velocity => velocity-ipad}/velocity/Features/Sentinel/SentinelView.swift (90%) create mode 100644 iOS/velocity-ipad/velocity/Features/Settings/SessionConfigurationPanel.swift create mode 100644 iOS/velocity-ipad/velocity/Features/Settings/SettingsView.swift rename iOS/{velocity => velocity-ipad}/velocity/Info.plist (90%) create mode 100644 iOS/velocity-ipad/velocityTests/VelocitySmokeTests.swift delete mode 100644 iOS/velocity/velocity/App/VelocityApp.swift delete mode 100644 iOS/velocity/velocity/Core/Config/AppConfig.swift delete mode 100644 iOS/velocity/velocity/Core/Networking/VelocityAPIClient.swift delete mode 100644 iOS/velocity/velocity/Core/State/AppStore.swift delete mode 100644 iOS/velocity/velocity/Core/UI/GlassBlurView.swift delete mode 100644 iOS/velocity/velocity/Core/UI/VelocityTheme.swift delete mode 100644 iOS/velocity/velocity/Features/Calendar/CalendarView.swift delete mode 100644 iOS/velocity/velocity/Features/Communications/CommunicationsView.swift delete mode 100644 iOS/velocity/velocity/Features/Oracle/OracleView.swift delete mode 100644 iOS/velocity/velocity/Features/Settings/SettingsView.swift diff --git a/.Agent Context/Codebase Analysis v1.1.md b/.Agent Context/Codebase Analysis v1.1.md new file mode 100644 index 00000000..6bd98ded --- /dev/null +++ b/.Agent Context/Codebase Analysis v1.1.md @@ -0,0 +1,565 @@ +# Codebase Analysis v1.1.md + +## Table of Contents / Chapters + +### 1. Overview +- Introduction to Project Velocity and its purpose +- Core principles and approach + +### 2. Architectural Mapping +- Overall System Architecture (Mermaid diagram) +- File Dependency Graph (Mermaid diagram) +- Data Flow Architecture (Mermaid diagram) + +### 3. Logic Decomposition +- Authentication & Authorization +- CRM Data Model +- Sentinel Biometric Intelligence +- Oracle Natural Language Intelligence +- Catalyst Marketing Orchestration +- Infrastructure & Deployment + +### 4. Connectivity Matrix +- Component interconnections and data flow +- Interconnection rationale + +### 5. First-Principles Guide +- Core Concept: AI-Augmented Sales Intelligence +- Why Real Estate Specifically? +- Principle 1: Data Sovereignty First +- Principle 2: Real-Time Perception Matters +- Principle 3: Intelligence Through Conversation +- Principle 4: Visual Storytelling Drives Sales +- Principle 5: Revision Control for Business Logic +- Design Philosophy: Production-Ready Craft +- Why This Architecture Succeeds + +### 6. API Endpoints Reference +- Authentication Endpoints +- CRM Endpoints +- Analytics Endpoints +- Oracle AI Intelligence Endpoints + - Oracle Canvas Management (v1) + - Oracle Template Management +- Sentinel Biometric Intelligence Endpoints +- Catalyst Marketing Orchestration Endpoints +- Vault Trackable Links Endpoints +- CCTV Surveillance Integration Endpoints +- Video Scene Mapping Endpoints +- Marketing Videos Endpoints +- Mobile Edge Communication Endpoints +- Inventory Management Endpoints +- Admin Surface Control Endpoints +- CRM Canonical Data Endpoints +- Runtime LLM Endpoints +- Infrastructure Notes + +## Overview + +Project Velocity is an on-prem real estate operating system designed for high-value property sales. It combines a premium WebOS, an iPad field app, a FastAPI neural core, ComfyUI-based media generation, and biometric/sentiment-assisted sales intelligence. The system enables brokers to operate at the speed of AI while preserving control, provenance, and safety for customer and revenue-critical data. + +This analysis provides a comprehensive understanding of the codebase from first principles, applying the Feynman Technique to distill complex implementations into intuitive concepts. + +## Architectural Mapping + +### Overall System Architecture + +```mermaid +graph TB + subgraph "User Interfaces" + WebOS[Velocity WebOS
React + TypeScript] + iPad[iPad App
Swift + MediaPipe] + end + + subgraph "Core Backend" + FastAPI[FastAPI Neural Core
PostgreSQL + JWT Auth] + end + + subgraph "AI Services" + Oracle[The Oracle
Natural Language Intelligence] + Sentinel[The Sentinel
Biometric Perception Engine] + Catalyst[The Catalyst
Marketing Campaign Orchestration] + Comfy[ComfyUI / Dream Weaver
Media Generation] + end + + subgraph "Infrastructure" + AWS[AWS GPU Workers
NVIDIA GPUs] + S3[S3 Asset Store
Models + Media] + Linux[Linux Control Surface
On-prem Deployment] + end + + WebOS --> FastAPI + iPad --> FastAPI + FastAPI --> Oracle + FastAPI --> Sentinel + FastAPI --> Catalyst + Catalyst --> Comfy + Comfy --> AWS + Comfy --> S3 + FastAPI --> Linux + + style FastAPI fill:#e1f5fe + style Oracle fill:#f3e5f5 + style Sentinel fill:#e8f5e8 + style Catalyst fill:#fff3e0 +``` + +### File Dependency Graph + +```mermaid +graph TD + subgraph "Frontend (React/Vite)" + App[App.tsx
Routing & Auth] + Store[useStore.ts
Zustand State] + Components[Components/
Modules & UI] + API[api.ts
HTTP Client] + end + + subgraph "Backend (FastAPI)" + Main[main.py
App Entry] + Routers[routers/
API Endpoints] + Services[services/
Business Logic] + DB[db/
Schema & Pool] + Auth[auth/
JWT & Users] + Oracle[oracle/
AI Intelligence] + end + + subgraph "AI Infrastructure" + Comfy[comfy_engine/
Media Generation] + Models[models/
AI Models] + Prompts[nemoclaw_prompts/
LLM Templates] + end + + subgraph "Deployment" + Infra[infrastructure/
Linux + AWS] + Agents[agents/
Orchestration] + end + + App --> API + API --> Main + Main --> Routers + Routers --> Services + Services --> DB + Services --> Auth + Services --> Oracle + Oracle --> Prompts + Comfy --> Infra + Infra --> Agents + + style Main fill:#e3f2fd + style Oracle fill:#fce4ec +``` + +### Data Flow Architecture + +```mermaid +flowchart LR + User[User Input] --> UI[WebOS/iPad UI] + UI --> API[FastAPI Endpoints] + API --> Auth[JWT Authentication] + API --> Policy[Policy Engine
Authorization] + API --> LLM[Nemoclaw LLM
Reasoning & Planning] + LLM --> Query[SQL Generation
Safe Queries] + Query --> DB[(PostgreSQL
CRM + Intelligence)] + DB --> Results[Query Results] + Results --> Viz[Visualization
Components] + Viz --> Canvas[Oracle Canvas
Persistent Views] + Canvas --> UI + + Sentinel[Sentinel Biometric] --> WS[WebSocket
Real-time] + WS --> Perception[Face Analysis
MediaPipe] + Perception --> QD[QD Scoring
NemoClaw] + QD --> DB + + Catalyst[Catalyst Marketing] --> Comfy[ComfyUI
Media Generation] + Comfy --> S3[S3 Assets] + Comfy --> GPU[AWS GPUs] + + style DB fill:#fff9c4 + style LLM fill:#e8f5e8 + style Comfy fill:#fce4ec +``` + +## Logic Decomposition + +### Authentication & Authorization + +**What:** Secure access control for all system functions +**How:** JWT tokens with role-based permissions (ADMIN, SALES_DIRECTOR, SENIOR_BROKER, JUNIOR_BROKER) +**Why:** Real estate involves sensitive client data; strict access prevents unauthorized sales interference + +**Key Implementation:** +- `backend/auth/dependencies.py`: JWT validation, user extraction, role enforcement +- `main.py`: Login endpoint with password hashing, user profile management +- Role hierarchy prevents junior brokers from approving high-value deals + +### CRM Data Model + +**What:** Canonical client and interaction records +**How:** PostgreSQL schema with leads, contacts, opportunities, interactions +**Why:** Real estate sales require accurate pipeline tracking and relationship history + +**Key Tables:** +- `leads_intelligence`: Core lead data with QD scores and tags +- `omnichannel_logs`: All interactions (calls, emails, visits) with timestamps +- `crm_people/crm_leads`: Structured client relationships and deal stages + +### Sentinel Biometric Intelligence + +**What:** Real-time visitor sentiment analysis for showroom engagement +**How:** Browser webcam + MediaPipe face landmarking → blend shapes → QD scoring +**Why:** Human emotion drives buying decisions; AI detects subtle cues brokers miss + +**Execution Flow:** +1. Browser captures video stream +2. MediaPipe extracts facial landmarks (68 points) +3. Blend shapes calculated (brow furrow, smile intensity, etc.) +4. NemoClaw LLM scores 1-100 QD (Qualification Desire) +5. Real-time dashboard updates for brokers + +**Key Decision:** Browser-side processing preserves privacy (no video leaves client device) + +### Oracle Natural Language Intelligence + +**What:** AI-powered data analysis through conversational queries +**How:** Natural language → SQL planning → visualization components → canvas +**Why:** Brokers shouldn't need SQL knowledge; AI translates business questions to insights + +**Architecture Layers:** +- **Prompt Orchestrator:** Decomposes complex requests into sub-queries +- **Natural DB Agent:** Schema introspection + safe SQL generation +- **Canvas Service:** Persistent visual workspaces with revision history +- **Collaboration:** Fork/merge workflows for team coordination + +**Why Canvas?** Unlike transient chat responses, canvases persist as living documents + +### Catalyst Marketing Orchestration + +**What:** Automated campaign creation and asset generation +**How:** Meta Ads API integration + ComfyUI media workflows +**Why:** Luxury real estate needs high-quality visuals; manual creation is too slow + +**Workflow:** +1. Campaign parameters → Meta API calls +2. Creative assets → ComfyUI (Dream Weaver, Wan 2.2, Qwen poster generation) +3. GPU processing on AWS → S3 storage +4. Performance tracking back to CRM + +### Infrastructure & Deployment + +**What:** Production-ready on-prem + cloud hybrid deployment +**How:** Linux control surface + AWS GPU workers + stable ingress +**Why:** Real estate firms demand data sovereignty; cloud-only solutions unacceptable + +**Key Components:** +- **Ingress:** Caddy on EC2 t4g.micro with TLS termination +- **Backend:** Linux systemd services for FastAPI, ComfyUI, LLM runtime +- **GPU:** AWS managed instances for media generation +- **Assets:** S3 for model storage, NVMe for fast access + +## Connectivity Matrix + +| Component | Inputs | Outputs | Dependencies | Protocols | +|-----------|--------|---------|--------------|-----------| +| WebOS Frontend | User actions, API responses | UI renders, API calls | FastAPI backend | HTTP/WS, JWT | +| iPad App | Camera feeds, user input | Biometric data, inventory scans | FastAPI backend | HTTP/WS | +| FastAPI Core | API requests, WS connections | DB queries, AI responses | PostgreSQL, Redis (future) | SQL, HTTP | +| Oracle Engine | Natural language prompts | Canvas components | NemoClaw LLM, PostgreSQL | Internal API | +| Sentinel Engine | Webcam streams | QD scores, alerts | MediaPipe, NemoClaw | WS real-time | +| Catalyst Engine | Campaign specs | Ad creatives, assets | Meta API, ComfyUI | HTTP, S3 | +| ComfyUI | Generation requests | Images/videos | GPU workers, S3 | Internal queue | +| PostgreSQL | SQL queries | Structured data | - | SQL | +| AWS GPUs | Media jobs | Generated assets | ComfyUI workflows | SSH/tunnel | + +**Interconnection Rationale:** +- **WebOS ↔ Backend:** Thin client architecture; all business logic server-side for security +- **Backend ↔ AI Services:** Modular design; each AI component (Oracle, Sentinel, Catalyst) operates independently but shares auth/policy +- **AI Services ↔ DB:** Direct SQL access with row-level security; no ORM abstraction to maintain performance +- **Infrastructure:** Hybrid on-prem/cloud; sensitive data stays on-prem, compute-intensive tasks use cloud GPUs + +## First-Principles Guide + +### Core Concept: AI-Augmented Sales Intelligence + +At its foundation, Project Velocity operates on the principle that human sales professionals excel at relationship-building and deal-closing, while AI excels at pattern recognition, data synthesis, and repetitive analysis. The system doesn't replace brokers—it amplifies their capabilities by providing real-time insights they couldn't otherwise access. + +**Why Real Estate Specifically?** +- High-value transactions ($M+ deals) with long sales cycles (months) +- Emotional decision-making influenced by subtle cues +- Complex data relationships (properties, buyers, markets, timing) +- Regulatory compliance requirements +- Need for visual storytelling (luxury properties) + +### Principle 1: Data Sovereignty First + +**Fundamental Truth:** Real estate firms own their client relationships. Project Velocity runs on-premise or in tenant-controlled cloud to maintain this ownership. + +**Implementation:** Linux-based deployment with optional AWS GPU extensions. All client data remains within tenant boundaries; only anonymous model requests leave for AI processing. + +### Principle 2: Real-Time Perception Matters + +**Fundamental Truth:** Buying decisions happen in moments of emotional connection. Project Velocity captures these moments through biometric analysis. + +**Implementation:** Sentinel uses facial expression analysis to score "Qualification Desire" (QD) on a 1-100 scale, alerting brokers to engagement spikes during property tours. + +### Principle 3: Intelligence Through Conversation + +**Fundamental Truth:** Sales professionals think in business terms, not database queries. The Oracle translates natural language into structured analytics. + +**Implementation:** Users ask "Show me whale leads from Dubai this quarter" and receive visual dashboards. The system plans safe SQL queries, executes them, and renders results as persistent canvas components. + +### Principle 4: Visual Storytelling Drives Sales + +**Fundamental Truth:** Luxury properties sell through aspiration and emotion. AI-generated media must be photorealistic and brand-consistent. + +**Implementation:** ComfyUI workflows (Dream Weaver, Wan 2.2) create property visualizations. Catalyst orchestrates campaigns with generated assets automatically uploaded to Meta Ads. + +### Principle 5: Revision Control for Business Logic + +**Fundamental Truth:** Sales strategies evolve through collaboration and iteration. Oracle canvases use Git-like branching for analytical workflows. + +**Implementation:** Canvas pages support forks, merge requests, and revision history. Brokers can experiment with analysis approaches without breaking production views. + +### Design Philosophy: Production-Ready Craft + +Project Velocity follows "experienced engineer" principles: +- **Error Handling:** No silent failures; all errors surface useful messages +- **Type Safety:** TypeScript frontend, typed Python backend +- **Performance:** Async everywhere, connection pooling, efficient queries +- **Security:** JWT auth, role-based access, input validation +- **Observability:** Structured logging, health checks, WebSocket monitoring +- **Maintainability:** Clear separation of concerns, comprehensive documentation + +### Why This Architecture Succeeds + +1. **Modular AI Services:** Each intelligence component (Oracle, Sentinel, Catalyst) operates independently, allowing incremental improvement and specialized optimization. + +2. **Hybrid Infrastructure:** Combines on-prem reliability with cloud scalability. Sensitive CRM data stays local; compute-intensive media generation uses managed GPUs. + +3. **Real-Time Integration:** WebSockets enable live updates across all surfaces. Brokers see lead scoring changes instantly, dashboard metrics update in real-time. + +4. **Business Logic in Code:** Revenue-critical workflows (lead qualification, campaign approval, deal closing) are explicit code paths, not AI hallucinations. + +5. **User-Centric Design:** The WebOS feels like a native application, not a bolted-on AI interface. Familiar patterns (canvases, dashboards, forms) reduce training time. + +This architecture transforms real estate sales from intuition-driven processes into data-augmented, AI-accelerated operations while preserving the human elements that drive luxury transactions. + +## API Endpoints Reference + +This section provides a comprehensive catalog of all API endpoints exposed by the Project Velocity backend, organized by functional module. Each endpoint includes the HTTP method, URI path, absolute HTTPS URL, and a brief description of its functionality. This serves as the definitive source of truth for the project's routing and interface architecture. + +### Authentication Endpoints + +| Method | Path | Absolute URL | Description | +|--------|------|--------------|-------------| +| POST | /api/auth/login | https://velocity.desineuron.in/api/auth/login | Authenticate a user with email/password and return JWT token | +| GET | /api/auth/me | https://velocity.desineuron.in/api/auth/me | Get current authenticated user's profile information | +| GET | /api/auth/users | https://velocity.desineuron.in/api/auth/users | List all active users in the system | +| POST | /api/auth/profile/avatar | https://velocity.desineuron.in/api/auth/profile/avatar | Upload and update user's profile avatar image | + +### CRM Endpoints + +| Method | Path | Absolute URL | Description | +|--------|------|--------------|-------------| +| GET | /api/leads | https://velocity.desineuron.in/api/leads | List leads with pagination and filtering | +| GET | /api/leads/{lead_id} | https://velocity.desineuron.in/api/leads/{lead_id} | Get detailed information for a specific lead | +| GET | /api/kanban/board | https://velocity.desineuron.in/api/kanban/board | Retrieve the kanban board view of leads by stage | +| GET | /api/chat-logs | https://velocity.desineuron.in/api/chat-logs | List chat logs for leads with optional lead filtering | +| GET | /api/leads/demographics | https://velocity.desineuron.in/api/leads/demographics | Get demographic analytics for leads (source, qualification) | + +### Analytics Endpoints + +| Method | Path | Absolute URL | Description | +|--------|------|--------------|-------------| +| GET | /api/analytics/sentiment-scatter | https://velocity.desineuron.in/api/analytics/sentiment-scatter | Get scatter plot data for sentiment analysis | + +### Oracle AI Intelligence Endpoints + +| Method | Path | Absolute URL | Description | +|--------|------|--------------|-------------| +| GET | /api/oracle/health | https://velocity.desineuron.in/api/oracle/health | Check Oracle system health and MCP tool availability | +| GET | /api/oracle/data-health | https://velocity.desineuron.in/api/oracle/data-health | Get data health metrics for database tables | +| GET | /api/oracle/schema-catalog | https://velocity.desineuron.in/api/oracle/schema-catalog | Retrieve schema catalog for database introspection | +| POST | /api/oracle/query | https://velocity.desineuron.in/api/oracle/query | Execute natural language query against database | +| GET | /api/oracle/mcp/tools | https://velocity.desineuron.in/api/oracle/mcp/tools | List available MCP tools for execution | +| POST | /api/oracle/mcp/execute | https://velocity.desineuron.in/api/oracle/mcp/execute | Execute an MCP tool with given query | +| POST | /api/oracle/workflow/preview | https://velocity.desineuron.in/api/oracle/workflow/preview | Preview workflow plan for a natural language prompt | + +#### Oracle Canvas Management (v1) + +| Method | Path | Absolute URL | Description | +|--------|------|--------------|-------------| +| POST | /api/oracle/v1/canvas/pages | https://velocity.desineuron.in/api/oracle/v1/canvas/pages | Create a new Oracle canvas page | +| GET | /api/oracle/v1/canvas/pages | https://velocity.desineuron.in/api/oracle/v1/canvas/pages | List user's Oracle canvas pages | +| GET | /api/oracle/v1/canvas/pages/{page_id} | https://velocity.desineuron.in/api/oracle/v1/canvas/pages/{page_id} | Get a specific canvas page | +| PUT | /api/oracle/v1/canvas/pages/{page_id} | https://velocity.desineuron.in/api/oracle/v1/canvas/pages/{page_id} | Update a canvas page | +| DELETE | /api/oracle/v1/canvas/pages/{page_id} | https://velocity.desineuron.in/api/oracle/v1/canvas/pages/{page_id} | Delete a canvas page | +| POST | /api/oracle/v1/canvas/pages/{page_id}/fork | https://velocity.desineuron.in/api/oracle/v1/canvas/pages/{page_id}/fork | Create a fork of a canvas page | +| POST | /api/oracle/v1/canvas/pages/{page_id}/merge | https://velocity.desineuron.in/api/oracle/v1/canvas/pages/{page_id}/merge | Merge changes into a canvas page | +| GET | /api/oracle/v1/canvas/pages/{page_id}/revisions | https://velocity.desineuron.in/api/oracle/v1/canvas/pages/{page_id}/revisions | List revisions for a canvas page | +| POST | /api/oracle/v1/canvas/pages/{page_id}/revisions | https://velocity.desineuron.in/api/oracle/v1/canvas/pages/{page_id}/revisions | Create new revision for a canvas page | +| GET | /api/oracle/v1/canvas/pages/{page_id}/revisions/{revision_id} | https://velocity.desineuron.in/api/oracle/v1/canvas/pages/{page_id}/revisions/{revision_id} | Get specific revision | +| PUT | /api/oracle/v1/canvas/pages/{page_id}/revisions/{revision_id} | https://velocity.desineuron.in/api/oracle/v1/canvas/pages/{page_id}/revisions/{revision_id} | Update a revision | +| DELETE | /api/oracle/v1/canvas/pages/{page_id}/revisions/{revision_id} | https://velocity.desineuron.in/api/oracle/v1/canvas/pages/{page_id}/revisions/{revision_id} | Delete a revision | +| GET | /api/oracle/v1/canvas/pages/{page_id}/revisions/{revision_id}/components | https://velocity.desineuron.in/api/oracle/v1/canvas/pages/{page_id}/revisions/{revision_id}/components | List components in a revision | +| POST | /api/oracle/v1/canvas/pages/{page_id}/revisions/{revision_id}/components | https://velocity.desineuron.in/api/oracle/v1/canvas/pages/{page_id}/revisions/{revision_id}/components | Add component to revision | +| PUT | /api/oracle/v1/canvas/pages/{page_id}/revisions/{revision_id}/components/{component_id} | https://velocity.desineuron.in/api/oracle/v1/canvas/pages/{page_id}/revisions/{revision_id}/components/{component_id} | Update component | +| DELETE | /api/oracle/v1/canvas/pages/{page_id}/revisions/{revision_id}/components/{component_id} | https://velocity.desineuron.in/api/oracle/v1/canvas/pages/{page_id}/revisions/{revision_id}/components/{component_id} | Delete component | + +#### Oracle Template Management + +| Method | Path | Absolute URL | Description | +|--------|------|--------------|-------------| +| GET | /api/oracle/template-chapters | https://velocity.desineuron.in/api/oracle/template-chapters | List Oracle template chapters | +| POST | /api/oracle/template-chapters | https://velocity.desineuron.in/api/oracle/template-chapters | Create new template chapter | +| GET | /api/oracle/template-subchapters | https://velocity.desineuron.in/api/oracle/template-subchapters | List Oracle template subchapters | +| POST | /api/oracle/template-subchapters | https://velocity.desineuron.in/api/oracle/template-subchapters | Create new template subchapter | +| GET | /api/oracle/component-templates | https://velocity.desineuron.in/api/oracle/component-templates | List Oracle component templates | +| POST | /api/oracle/component-templates | https://velocity.desineuron.in/api/oracle/component-templates | Create new component template | +| GET | /api/oracle/component-templates/{template_id} | https://velocity.desineuron.in/api/oracle/component-templates/{template_id} | Get specific component template | +| POST | /api/oracle/component-templates/{template_id}/seed | https://velocity.desineuron.in/api/oracle/component-templates/{template_id}/seed | Add seed example to template | +| GET | /api/oracle/component-templates/{template_id}/seed | https://velocity.desineuron.in/api/oracle/component-templates/{template_id}/seed | List seed examples for template | +| POST | /api/oracle/component-templates/synthetic-jobs | https://velocity.desineuron.in/api/oracle/component-templates/synthetic-jobs | Submit synthetic template generation job | + +### Sentinel Biometric Intelligence Endpoints + +| Method | Path | Absolute URL | Description | +|--------|------|--------------|-------------| +| WebSocket | /api/sentinel/ws/notifications | wss://velocity.desineuron.in/api/sentinel/ws/notifications | Real-time notifications WebSocket | +| WebSocket | /api/sentinel/ws/perception | wss://velocity.desineuron.in/api/sentinel/ws/perception | Biometric perception data WebSocket | +| POST | /api/sentinel/consent | https://velocity.desineuron.in/api/sentinel/consent | Record biometric consent for lead | +| POST | /api/sentinel/session/complete | https://velocity.desineuron.in/api/sentinel/session/complete | Close a perception session and finalize QD score | +| POST | /api/sentinel/tag-lead | https://velocity.desineuron.in/api/sentinel/tag-lead | Apply NemoClaw lead tagging to CRM lead | +| GET | /api/sentinel/qd-score/{lead_id} | https://velocity.desineuron.in/api/sentinel/qd-score/{lead_id} | Get current QD score for a lead | + +### Catalyst Marketing Orchestration Endpoints + +| Method | Path | Absolute URL | Description | +|--------|------|--------------|-------------| +| POST | /api/catalyst/campaigns/create | https://velocity.desineuron.in/api/catalyst/campaigns/create | Bulk create Meta ad campaigns | +| POST | /api/catalyst/creative/sync | https://velocity.desineuron.in/api/catalyst/creative/sync | Upload ComfyUI assets to Meta | +| GET | /api/catalyst/insights/realtime | https://velocity.desineuron.in/api/catalyst/insights/realtime | Poll Meta Ads Insights API | +| POST | /api/catalyst/audiences/lookalike | https://velocity.desineuron.in/api/catalyst/audiences/lookalike | Push CRM leads to Meta Custom Audience | +| POST | /api/catalyst/auth/meta | https://velocity.desineuron.in/api/catalyst/auth/meta | OAuth token acquisition for Meta | + +### Vault Trackable Links Endpoints + +| Method | Path | Absolute URL | Description | +|--------|------|--------------|-------------| +| POST | /api/vault/generate-link | https://velocity.desineuron.in/api/vault/generate-link | Generate trackable URL for shared asset | +| GET | /vault/{tracking_hash} | https://velocity.desineuron.in/vault/{tracking_hash} | Public access to trackable vault link (no auth required) | + +### CCTV Surveillance Integration Endpoints + +| Method | Path | Absolute URL | Description | +|--------|------|--------------|-------------| +| POST | /api/cctv/event | https://velocity.desineuron.in/api/cctv/event | Ingest CCTV frame event from RTSP/ONVIF bridge | +| POST | /api/cctv/finalize-auto-mode | https://velocity.desineuron.in/api/cctv/finalize-auto-mode | Match or create lead after auto-mode session | + +### Video Scene Mapping Endpoints + +| Method | Path | Absolute URL | Description | +|--------|------|--------------|-------------| +| POST | /api/scenes/upload | https://velocity.desineuron.in/api/scenes/upload | Upload CSV scene map for marketing video | +| GET | /api/scenes/{video_asset_id} | https://velocity.desineuron.in/api/scenes/{video_asset_id} | Get scene map for specific video asset | + +### Marketing Videos Endpoints + +| Method | Path | Absolute URL | Description | +|--------|------|--------------|-------------| +| GET | /api/videos/marketing | https://velocity.desineuron.in/api/videos/marketing | List marketing videos available for Sentinel sessions | + +### Mobile Edge Communication Endpoints + +| Method | Path | Absolute URL | Description | +|--------|------|--------------|-------------| +| GET | /api/mobile-edge/events | https://velocity.desineuron.in/api/mobile-edge/events | List communication events for a lead | +| POST | /api/mobile-edge/events | https://velocity.desineuron.in/api/mobile-edge/events | Log new communication event | +| GET | /api/mobile-edge/memory | https://velocity.desineuron.in/api/mobile-edge/memory | List memory facts for a lead | +| POST | /api/mobile-edge/imports | https://velocity.desineuron.in/api/mobile-edge/imports | Operator-assisted import of recording/note | +| POST | /api/mobile-edge/notes | https://velocity.desineuron.in/api/mobile-edge/notes | Quick note attachment to lead | +| GET | /api/mobile-edge/calendar | https://velocity.desineuron.in/api/mobile-edge/calendar | Calendar events for authenticated user | +| POST | /api/mobile-edge/calendar | https://velocity.desineuron.in/api/mobile-edge/calendar | Create calendar event | +| PATCH | /api/mobile-edge/calendar/{calendar_event_id} | https://velocity.desineuron.in/api/mobile-edge/calendar/{calendar_event_id} | Update calendar event | +| DELETE | /api/mobile-edge/calendar/{calendar_event_id} | https://velocity.desineuron.in/api/mobile-edge/calendar/{calendar_event_id} | Cancel calendar event | +| GET | /api/mobile-edge/transcripts/{event_id} | https://velocity.desineuron.in/api/mobile-edge/transcripts/{event_id} | Transcript segments for event | +| GET | /api/mobile-edge/insights/{lead_id} | https://velocity.desineuron.in/api/mobile-edge/insights/{lead_id} | Insight recommendations for lead | +| POST | /api/mobile-edge/insights/{recommendation_id}/act | https://velocity.desineuron.in/api/mobile-edge/insights/{recommendation_id}/act | Act on or dismiss insight | +| GET | /api/mobile-edge/alerts | https://velocity.desineuron.in/api/mobile-edge/alerts | Active alerts for authenticated user | +| POST | /api/mobile-edge/session | https://velocity.desineuron.in/api/mobile-edge/session | Register surface session heartbeat | + +### Inventory Management Endpoints + +| Method | Path | Absolute URL | Description | +|--------|------|--------------|-------------| +| POST | /api/inventory/import-batches | https://velocity.desineuron.in/api/inventory/import-batches | Create inventory import batch | +| GET | /api/inventory/import-batches | https://velocity.desineuron.in/api/inventory/import-batches | List import batches | +| GET | /api/inventory/import-batches/{batch_id} | https://velocity.desineuron.in/api/inventory/import-batches/{batch_id} | Get batch status | +| POST | /api/inventory/properties | https://velocity.desineuron.in/api/inventory/properties | Create single property | +| GET | /api/inventory/properties | https://velocity.desineuron.in/api/inventory/properties | List inventory properties | +| GET | /api/inventory/properties/{property_id} | https://velocity.desineuron.in/api/inventory/properties/{property_id} | Get property details | +| PATCH | /api/inventory/properties/{property_id} | https://velocity.desineuron.in/api/inventory/properties/{property_id} | Update property | +| DELETE | /api/inventory/properties/{property_id} | https://velocity.desineuron.in/api/inventory/properties/{property_id} | Archive property | +| POST | /api/inventory/properties/{property_id}/media | https://velocity.desineuron.in/api/inventory/properties/{property_id}/media | Attach media to property | +| GET | /api/inventory/properties/{property_id}/media | https://velocity.desineuron.in/api/inventory/properties/{property_id}/media | List media for property | +| DELETE | /api/inventory/media/{media_asset_id} | https://velocity.desineuron.in/api/inventory/media/{media_asset_id} | Remove media asset | + +### Admin Surface Control Endpoints + +| Method | Path | Absolute URL | Description | +|--------|------|--------------|-------------| +| GET | /api/admin-surface/health | https://velocity.desineuron.in/api/admin-surface/health | System health overview | +| GET | /api/admin-surface/queues | https://velocity.desineuron.in/api/admin-surface/queues | Queue depth snapshot | +| GET | /api/admin-surface/installs | https://velocity.desineuron.in/api/admin-surface/installs | Surface session/install overview | +| POST | /api/admin-surface/actions | https://velocity.desineuron.in/api/admin-surface/actions | Submit admin action | +| GET | /api/admin-surface/actions | https://velocity.desineuron.in/api/admin-surface/actions | List admin action history | +| GET | /api/admin-surface/actions/{action_event_id} | https://velocity.desineuron.in/api/admin-surface/actions/{action_event_id} | Get specific admin action | +| GET | /api/admin-surface/logs | https://velocity.desineuron.in/api/admin-surface/logs | Recent Oracle audit event log | +| GET | /api/admin-surface/templates | https://velocity.desineuron.in/api/admin-surface/templates | Template catalog admin view | +| POST | /api/admin-surface/templates/{template_id}/publish | https://velocity.desineuron.in/api/admin-surface/templates/{template_id}/publish | Publish template | +| POST | /api/admin-surface/templates/{template_id}/archive | https://velocity.desineuron.in/api/admin-surface/templates/{template_id}/archive | Archive template | +| GET | /api/admin-surface/template-chapters | https://velocity.desineuron.in/api/admin-surface/template-chapters | List template chapters (admin) | +| GET | /api/admin-surface/synthetic-jobs | https://velocity.desineuron.in/api/admin-surface/synthetic-jobs | List synthetic generation jobs | +| POST | /api/admin-surface/synthetic-jobs/{job_id}/cancel | https://velocity.desineuron.in/api/admin-surface/synthetic-jobs/{job_id}/cancel | Cancel synthetic job | + +### CRM Canonical Data Endpoints + +| Method | Path | Absolute URL | Description | +|--------|------|--------------|-------------| +| POST | /api/crm/imports | https://velocity.desineuron.in/api/crm/imports | Upload CSV batch for import | +| GET | /api/crm/imports | https://velocity.desineuron.in/api/crm/imports | List import batches | +| GET | /api/crm/imports/{batch_id} | https://velocity.desineuron.in/api/crm/imports/{batch_id} | Get batch detail and proposals | +| PUT | /api/crm/imports/{batch_id}/review-proposal | https://velocity.desineuron.in/api/crm/imports/{batch_id}/review-proposal | Review import proposal | +| POST | /api/crm/imports/{batch_id}/commit | https://velocity.desineuron.in/api/crm/imports/{batch_id}/commit | Commit approved proposals | +| GET | /api/crm/contacts | https://velocity.desineuron.in/api/crm/contacts | Canonical contact list with QD summary | +| POST | /api/crm/contacts | https://velocity.desineuron.in/api/crm/contacts | Create new contact | +| GET | /api/crm/contacts/{person_id} | https://velocity.desineuron.in/api/crm/contacts/{person_id} | Canonical contact detail | +| GET | /api/crm/client-360/{person_id} | https://velocity.desineuron.in/api/crm/client-360/{person_id} | Client 360 aggregated snapshot | +| GET | /api/crm/opportunities | https://velocity.desineuron.in/api/crm/opportunities | Opportunity pipeline list | +| GET | /api/crm/tasks | https://velocity.desineuron.in/api/crm/tasks | Reminder/task list | +| POST | /api/crm/tasks | https://velocity.desineuron.in/api/crm/tasks | Create new task | +| GET | /api/crm/kanban | https://velocity.desineuron.in/api/crm/kanban | Kanban board (canonical leads) | +| GET | /api/crm/qd/{person_id} | https://velocity.desineuron.in/api/crm/qd/{person_id} | QD score history for person | +| GET | /api/crm/client-data | https://velocity.desineuron.in/api/crm/client-data | List client data records | +| GET | /api/crm/client-data/{person_id} | https://velocity.desineuron.in/api/crm/client-data/{person_id} | Get client data for person | +| PATCH | /api/crm/client-data/{person_id} | https://velocity.desineuron.in/api/crm/client-data/{person_id} | Update client data | +| GET | /api/crm/client-data/{person_id}/timeline | https://velocity.desineuron.in/api/crm/client-data/{person_id}/timeline | Client data timeline | +| POST | /api/crm/client-data/{person_id}/tasks | https://velocity.desineuron.in/api/crm/client-data/{person_id}/tasks | Create task for client | + +### Runtime LLM Endpoints +Method Path Absolute URL Description +GET /api/runtime/llm/providers https://velocity.desineuron.in/api/runtime/llm/providers List configured LLM providers and models +POST /api/runtime/llm/chat https://velocity.desineuron.in/api/runtime/llm/chat Execute single LLM chat completion +POST /api/runtime/llm/batch https://velocity.desineuron.in/api/runtime/llm/batch Submit persisted LLM batch job +GET /api/runtime/llm/jobs/{job_id} https://velocity.desineuron.in/api/runtime/llm/jobs/{job_id} Get batch job status +GET /api/runtime/llm/jobs/{job_id}/results https://velocity.desineuron.in/api/runtime/llm/jobs/{job_id}/results Get batch job results + +### Infrastructure Notes + +- **Caddy Reverse Proxy**: All endpoints are served through Caddy on port 443, proxying to FastAPI on localhost:8443 with TLS termination +- **Authentication**: JWT-based auth required for most endpoints (except public vault links) +- **WebSockets**: Real-time features use WebSocket connections for live updates +- **Role-Based Access**: Endpoints enforce role permissions (SENIOR_BROKER, ADMIN, etc.) +- **Tenant Isolation**: Multi-tenant architecture with tenant_id scoping +- **Audit Logging**: All mutations create immutable audit records +- **Health Checks**: System provides comprehensive health and queue monitoring endpoints \ No newline at end of file diff --git a/.Agent Context/Sayan's Ipad docs/Velocity_Ipad_Production_Readiness_Audit.md b/.Agent Context/Sayan's Ipad docs/Velocity_Ipad_Production_Readiness_Audit.md new file mode 100644 index 00000000..990e75a3 --- /dev/null +++ b/.Agent Context/Sayan's Ipad docs/Velocity_Ipad_Production_Readiness_Audit.md @@ -0,0 +1,3068 @@ +# Velocity iPad Production Readiness Audit + +Date: 2026-04-22 +Author: Codex +Scope: `iOS/velocity-ipad` plus the backend, API, database, Dream Weaver gateway, and web platform dependencies that determine whether the iPad app is actually functional in production. + +## Executive Summary + +The `velocity-ipad` app is no longer a concept stub. It is a materially implemented SwiftUI application with a real navigation shell, a meaningful live-data client, and several genuine product surfaces: + +- Dashboard +- Communications +- Calendar +- Oracle +- Sentinel +- Inventory +- Settings + +However, the app is not production-ready today. + +The biggest issue is not that the UI is missing. The UI is mostly present. The bigger issue is that multiple parts of the stack are only partially wired, operationally brittle, or explicitly disabled: + +- Dream Weaver is high-risk because the app, gateway, and public endpoint configuration are not cleanly aligned. +- Sentinel on iPad is now explicitly positioned as `Operator Posture` because it remains a live alert/load summary, not a visitor analytics surface. +- Oracle production scope is now intentionally limited to three live-backed modes, with unsupported `Team Performance` and `Lead Map` views hidden until real backend contracts exist. +- Dollhouse is now hidden from the production iPad scope unless a verified `Building.usdz` or `Building.scn` asset is actually shipped in the app bundle. +- The iPad app now has runtime session setup, Keychain-backed secure credential storage, and a test scaffold, but it still has no verifiable full Xcode build result from this machine. +- The backend has strong breadth but weak operational hygiene: environment drift, mixed schema models, legacy-shaped CRM dependencies with incomplete tenant modeling, and missing migration discipline. + +If the question is "Can this be turned into an investor-safe demo and then into a production system?" the answer is yes. + +If the question is "Is it already production ready today?" the answer is no. + +My assessment: + +- UI implementation maturity: medium to high +- Backend breadth: high +- Backend production rigor: medium-low +- Data model consistency: medium-low +- Operational reliability: low to medium +- Investor demo readiness next Tuesday: achievable only with deliberate scope control and a focused hardening sprint + +## Audit Method + +This audit was performed by: + +- Reading the full `iOS/velocity-ipad` target and its local feature implementations +- Tracing every iPad API dependency into `backend/` +- Reviewing the database schema files that back the app +- Reviewing Dream Weaver gateway implementations in the repo +- Running local validation where possible +- Verifying the configured public API endpoint on 2026-04-22 + +Validation constraints: + +- Full Xcode is not installed on this machine, so a true `xcodebuild` compile could not be completed +- Backend test execution is blocked in the current local environment because `httpx` is not installed even though it is listed in `backend/requirements.txt` +- Public endpoint verification was done live on 2026-04-22 and showed an HTTP redirect to a broken HTTPS TLS handshake + +## Current UI Screenshots + +The following screenshots were provided in this review thread and represent the present visible state of the iPad app as of 2026-04-22. They are included here as part of the audit record so Sagnik and other reviewers can compare the written assessment against the current UI. + +Important note: + +- These screenshots clearly reinforce one of the most important audit findings: the app shell is materially implemented, but the live backend path is currently failing with a TLS error in multiple surfaces. +- The screenshots are part of this review context and should be treated as the current-state visual appendix for this audit. + +### Dashboard - Current State + +Observed in screenshot: + +- Sidebar navigation is fully implemented +- Dashboard layout is visually coherent and investor-presentable +- Live status panel is present +- Metrics cards are present +- The screen shows `Live backend` +- The screen also shows a red error banner: `A TLS error caused the secure connection to fail.` +- Leads, whale leads, inventory, and today counts are all `0` in the captured state + +Interpretation: + +- The frontend shell is working +- The backend connectivity is not healthy in the captured runtime + +### Communications - Current State + +Observed in screenshot: + +- Communications surface is fully laid out +- Active Threads rail exists +- Detail panel exists +- Live thread count badge exists +- The screen shows a red TLS error banner +- No communication events are present in the captured state + +Interpretation: + +- The screen implementation is real +- Live communication data is not currently flowing in the captured runtime + +### Calendar - Current State + +Observed in screenshot: + +- Calendar metrics cards render correctly +- Week Grid rail is implemented +- Agenda/detail panel is implemented +- The screen shows a red TLS error banner +- All metrics are `0` in the captured state +- The synthesis card is present and readable + +Interpretation: + +- The calendar UI is production-shaped +- The live calendar backend dependency is not healthy in the captured runtime + +### Oracle - Current State + +Observed in screenshot captured before Workstream 5 hardening: + +- Oracle header and mode pills are present +- Pipeline, Account Timeline, Calendar & Tasks, Team Performance, and Lead Map controls are visible +- The screen shows a red TLS error banner +- Pipeline Summary card renders correctly +- No live pipeline rows are present in the captured state + +Interpretation: + +- Oracle mobile UI is present and credible +- The screenshot visually confirms that the surface is implemented even though data is not currently loading +- Update 2026-04-22 after Workstream 5: + - the production iPad scope now hides `Team Performance` and `Lead Map` + - only live-backed Oracle modes remain visible in the intended production path + +### Sentinel - Current State + +Observed in screenshot captured before Workstream 7 hardening: + +- Sentinel header is present +- Feed Availability card is visible +- Pending insights, transcript queue, and upcoming 24h KPI cards are present +- Recent Operator Timeline card is present +- The screen shows a red TLS error banner +- The screen explicitly says `No mock feed` + +Interpretation: + +- This screenshot strongly confirms the audit finding that iPad Sentinel is currently an operator-posture surface, not a full live visitor analytics surface +- Update 2026-04-22 after Workstream 7: + - the production app shell now labels this surface as `Operator Posture` + - the Sentinel screen now presents `Sentinel` as the product family and `Operator Posture` as the truthful iPad surface name + - the availability badge and copy now describe production scope directly instead of relying on the weaker `No mock feed` phrasing + +### Inventory - Current State + +Observed in screenshot captured before Workstream 6 hardening: + +- Inventory tab layout is implemented +- Sunseeker, Dream Weaver, and Dollhouse mode tabs are present +- The captured state shows the simulator-safe Sunseeker placeholder: + `Sunseeker requires a real device` +- The explanatory copy matches the current implementation + +Interpretation: + +- This screenshot supports the audit conclusion that Sunseeker is a genuine device-dependent feature +- It also confirms that the current captured state is not showing a live AR run +- Update 2026-04-22 after Workstream 6: + - `Dollhouse` is now hidden from the production iPad scope when the model asset is absent + - the inventory mode list now matches the assets that are actually shipped + +### Settings - Current State + +Observed in screenshot: + +- Settings surface is fully implemented +- Backend endpoint, auth mode, and last refresh are shown +- Identity, lead records loaded, and property records loaded are shown +- Production Notes panel is present +- The endpoint shown is `https://api.desineuron.in` +- Auth mode is `Email/password` +- No live fetch has occurred in the captured state + +Interpretation: + +- Settings is one of the cleanest implemented screens in the app +- The screenshot also reinforces the runtime issue: configuration is present, but live data has not successfully loaded + +## Workstream 1 Status - iPad Release Hygiene and App-Store-Grade Foundation + +Date completed: 2026-04-22 +Status: materially improved and complete enough to move to the next workstream, with explicit external limitations still open + +### What was changed + +The following repo-local changes were completed in the active iPad target under `iOS/velocity-ipad`: + +- iPad target bundle identifier corrected to `com.desineuron.velocity.ipad` +- Target device family changed from universal (`1,2`) to iPad-only (`2`) +- Deployment target normalized to iPadOS 17.0 from the invalid `26.2` +- Release metadata aligned to `MARKETING_VERSION = 1.1` to better match the current product labeling +- Global ATS bypass was removed from `Info.plist` +- `Info.plist` was normalized to placeholder-driven config keys rather than hardcoded local credentials and local IPs +- Canonical permission strings were moved back into `Info.plist` so the app target no longer depends on duplicated permission text in build settings +- A dedicated unit-test bundle target `velocityTests` was added +- A practical smoke-test file was added covering: + - root shell construction + - `AppSection` navigation stability + - config parsing behavior + - auth mode description logic +- `AppConfig` was refactored slightly to expose deterministic parsing helpers for testability without changing backend contracts or screen behavior + +### Source-of-truth decision now recorded + +For all future production work on the iPad app, the active source of truth should be treated as: + +- `iOS/velocity-ipad/velocity/...` + +The duplicate tree drift issue remains open and is not resolved by this workstream. + +### What is materially better now + +- The iPad target now looks like a real iPad app instead of an accidental prototype/universal target +- The target no longer ships with blanket ATS disablement +- The app no longer embeds hardcoded email/password credentials and a local HTTP backend URL directly in the committed `Info.plist` +- There is now a repo-local iPad validation scaffold instead of relying entirely on manual trust +- The release identity is cleaner and consistent with an App-Store-grade direction + +### What remains blocked or incomplete + +- Full Xcode-based build/test execution could not be completed from this machine because full Xcode is not installed +- The new `velocityTests` target was added and structurally validated, but not executed via `xcodebuild` for the same reason +- External ingress/TLS is still broken as of this pass: + - `https://api.desineuron.in/health` still fails TLS handshake + - this continues to point to an external infrastructure problem rather than an iPad ATS problem +- Build-time config injection remains a production gap even after removing hardcoded secrets from the committed plist +- Duplicate iOS source trees remain present and drifting + +### Tradeoff introduced by this hardening pass + +Because ATS is now strict by default: + +- plain HTTP local backend development is no longer implicitly allowed +- developers using `http://192.168.x.x` style local endpoints will now need a proper dev-only exception strategy, HTTPS local proxying, or a separate debug-only configuration in a later workstream + +This is an intentional App-Store-grade tradeoff, not a regression. + +### Validation run for this workstream + +The following checks were run after implementation: + +- `plutil -lint iOS/velocity-ipad/velocity.xcodeproj/project.pbxproj` + - result: OK +- `plutil -lint iOS/velocity-ipad/velocity/Info.plist` + - result: OK +- static inspection confirmed: + - bundle identifier updated to `com.desineuron.velocity.ipad` + - deployment target updated to `17.0` + - target device family updated to `2` + - `velocityTests` target exists in the Xcode project +- `xcodebuild -project iOS/velocity-ipad/velocity.xcodeproj -list` + - result: could not run because this machine only has Command Line Tools, not full Xcode +- `curl -svk https://api.desineuron.in/health` + - result: TLS handshake still fails +- `openssl s_client -connect api.desineuron.in:443 -servername api.desineuron.in` + - result: TLS handshake still fails and no peer certificate is served + +### Current judgment after Workstream 1 + +This workstream was successful. + +It did not make the iPad app fully production-ready by itself, but it removed several release-hygiene and security anti-patterns that had to be addressed before any serious production work could continue. + +It is complete enough to move to the next item once approved. + +## Workstream 2 Status - Runtime Auth, Secure Session Storage, and First-Run Configuration + +Date completed: 2026-04-22 +Status: materially improved and complete enough to move to the next workstream, with external backend/TLS limitations still open + +### What was changed + +The following repo-local changes were completed in the active iPad target under `iOS/velocity-ipad`: + +- Added a real runtime session model with on-device persistence +- Added Keychain-backed storage for password and bearer token secrets +- Added first-run configuration gating so an unconfigured build no longer drops the user into a broken shell without a credential path +- Added an editable session configuration panel inside Settings +- Added secure runtime override handling for: + - backend endpoint + - email/password login + - bearer token auth +- Refactored `AppConfig` so runtime overrides take precedence over build settings while still allowing build settings as fallback defaults +- Added session reset behavior so cached bearer tokens are cleared when credentials change +- Added app-state reset behavior so stale lead, property, calendar, and alert data does not linger after endpoint or auth changes +- Added validation rules for configuration input: + - backend endpoint must be an HTTPS origin + - email/password mode requires a valid-looking operator email + - auth mode changes cannot silently reuse the wrong secret +- Added unit coverage for the new configuration validation and normalization logic + +### What is materially better now + +- The iPad app no longer depends exclusively on build-time embedded secrets to function +- Production-facing installs can now be configured directly on-device without rebuilding the app +- Secrets are now stored in Keychain when entered through the runtime setup flow +- The app has a first-run experience that matches a real deployed product more closely +- Settings is now an operational control surface, not just a read-only status panel +- Session changes now clear the cached API token and trigger a truthful live refresh + +### What remains blocked or incomplete + +- Full `xcodebuild` execution is still blocked on this machine because full Xcode is not installed +- The new runtime session flow does not include password recovery, SSO, MDM distribution hooks, or multi-user session switching +- Build-time config fallback still exists, which is useful operationally but should not be the preferred distribution path for investor or enterprise installs +- The production backend endpoint still fails TLS handshake, so the new runtime session flow cannot complete a successful live verification against `https://api.desineuron.in` from this machine +- Dream Weaver still shares the same base endpoint assumption and has not yet been split into an explicit dedicated configuration path + +### Validation run for this workstream + +The following checks were run after implementation: + +- static typecheck: + - `swiftc -typecheck iOS/velocity-ipad/velocity/Core/Config/SessionConfiguration.swift iOS/velocity-ipad/velocity/Core/Config/AppConfig.swift` + - result: OK +- static inspection confirmed: + - runtime session model exists + - Keychain persistence path exists + - first-run configuration gate exists + - Settings now includes a writable session configuration surface + - unit tests now cover runtime session validation helpers +- `curl -svk https://api.desineuron.in/health` + - result: TLS handshake still fails +- `openssl s_client -connect api.desineuron.in:443 -servername api.desineuron.in` + - result: TLS handshake still fails and no peer certificate is served + +### Current judgment after Workstream 2 + +This workstream was successful. + +It closed one of the largest remaining product-readiness gaps on the iPad app itself: there is now a real runtime authentication and credential configuration path instead of a build-time-only secret model. + +It did not solve the external backend TLS problem, and it does not replace the need for a full Xcode build and real-device validation pass. + +It is complete enough to move to the next item once approved. + +## Workstream 3 Status - Dream Weaver Endpoint Separation and Gateway Contract Hardening + +Date completed: 2026-04-22 +Status: materially improved and complete enough to move to the next workstream, with external deployment validation still open + +### What was changed + +The following repo-local changes were completed across the active iPad target and the minimal Dream Weaver gateway: + +- Added a dedicated Dream Weaver endpoint configuration path to the iPad session model +- Added `DREAM_WEAVER_BASE_URL` support to the iPad app configuration layer with runtime override support +- Updated the session configuration UI so operators can point Dream Weaver at a separate endpoint without rebuilding the app +- Made blank Dream Weaver endpoint input behave intentionally as "share the backend endpoint" instead of silently reverting to an embedded dedicated gateway value +- Refactored `ComfyClient` to use `dreamWeaverBaseURL` instead of assuming `BASE_URL` owns both CRM/API traffic and generation traffic +- Hardened Dream Weaver route resolution so the iPad client now accepts: + - full gateway responses with `poll_url` and `result_url` + - minimal gateway responses that only include `job_id` and `status` + - relative or absolute route URLs +- Strengthened the Dream Weaver health check so it now verifies: + - `/health` + - actual Dream Weaver route presence via a route probe +- Patched `dw_gateway_v2_min.py` so it now: + - returns `poll_url` + - returns `result_url` + - exposes `/dream-weaver/result/{job_id}` + - includes `result_url` in ready status payloads +- Added a fallback prompt-expansion path in `dw_gateway_v2_min.py` so the minimal gateway no longer crashes when `prompt_expander` is absent +- Added unit coverage for: + - Dream Weaver endpoint validation rules + - minimal gateway contract fallback decoding + - result URL resolution behavior + +### What is materially better now + +- The iPad app no longer assumes the primary backend host also owns Dream Weaver generation routes +- Dream Weaver can now be pointed at a dedicated gateway in production-facing installs without changing the core API endpoint +- The iPad client is materially more robust against contract drift between the full Dream Weaver gateway and the minimal gateway +- The minimal gateway in this repo is now much closer to the contract the iPad app expects +- Dream Weaver health reporting is more truthful because it now checks route presence, not just generic liveness + +### What remains blocked or incomplete + +- Public production TLS and ingress are still externally broken as of 2026-04-22 +- A successful end-to-end Dream Weaver generation round-trip was not verified against a deployed HTTPS endpoint from this machine +- Full Xcode build execution is still blocked on this machine because full Xcode is not installed +- Real-device validation of capture → generate → download is still required +- `backend/main.py` still does not directly mount Dream Weaver, so production endpoint ownership and reverse-proxy responsibility still need to be finalized at the infrastructure level + +### Validation run for this workstream + +The following checks were run after implementation: + +- `plutil -lint iOS/velocity-ipad/velocity/Info.plist` + - result: OK +- `swiftc -typecheck iOS/velocity-ipad/velocity/Core/Config/SessionConfiguration.swift iOS/velocity-ipad/velocity/Core/Config/AppConfig.swift` + - result: OK +- `python3 -m py_compile dw_gateway_v2_min.py` + - result: OK +- static inspection confirmed: + - iPad runtime configuration now includes a dedicated Dream Weaver endpoint path + - `ComfyClient` now resolves Dream Weaver routes against `dreamWeaverBaseURL` + - minimal gateway payload fallback is handled in the iPad client + - `dw_gateway_v2_min.py` now exposes `/dream-weaver/result/{job_id}` +- full `xcodebuild` execution still could not be run from this machine because full Xcode is not installed + +### Current judgment after Workstream 3 + +This workstream was successful. + +It materially reduced one of the biggest repo-local product risks in the iPad stack by separating endpoint ownership and repairing the contract mismatch between the app and the minimal gateway implementation. + +It did not prove Dream Weaver production readiness end-to-end, because deployed HTTPS routing, full Xcode validation, and real-device generation testing are still open. + +It is complete enough to move to the next item once approved. + +## Workstream 4 Status - Legacy CRM Route Authentication and Tenant Scoping Hardening + +Date completed: 2026-04-22 +Status: materially improved and complete enough to move to the next workstream, with architectural tenant-model limitations still open + +### What was changed + +The following repo-local changes were completed across the backend auth layer and legacy CRM routes: + +- Added an explicit `tenant_id` claim and default tenant helper to the auth dependency layer +- Extended `UserPrincipal` to carry `tenant_id` instead of forcing downstream routes to improvise tenant scope +- Updated `/api/auth/me` to return `tenant_id` +- Hardened the legacy CRM route family in `backend/api/routes_crm.py` so the following routes now require authenticated users: + - `/api/leads` + - `/api/leads/{lead_id}` + - `/api/leads/demographics` + - `/api/chat-logs` + - `/api/kanban/board` + - `/api/kanban/move` + - `/api/analytics/sentiment-scatter` + - legacy write routes including create, update, delete, and seed +- Added tenant scoping to the legacy `leads` and `chat_logs` tables +- Added schema backfill logic so pre-existing legacy rows get a default tenant assignment during route bootstrap +- Added tenant-aware indexes for the legacy CRM tables +- Scoped all legacy CRM reads and writes by `tenant_id` +- Preserved the existing iPad-facing response contract for `/api/leads` so the mobile client did not require a DTO or route-path change +- Expanded backend tests to cover: + - authenticated CRUD flow + - authentication required behavior + - tenant isolation across authenticated clients + +### What is materially better now + +- The iPad app no longer reads lead data from an unauthenticated public CRM route +- The legacy CRM surface now has a first-class tenant scope in its auth and query path +- Cross-tenant leakage risk in the legacy `leads` / `chat_logs` tables is materially reduced +- The backend auth model is slightly more production-shaped because tenant scope is now carried in the principal instead of inferred ad hoc in every route + +### What remains blocked or incomplete + +- `users_and_roles` is now tenant-aware, but it still models only a single direct tenant binding per user rather than a fuller org or membership graph +- runtime schema mutation and default-tenant backfill are still being used as an interim migration strategy instead of a governed migration system +- The legacy CRM route family is still legacy-shaped and separate from the canonical `crm_people` / `crm_leads` model +- This workstream does not complete the canonical CRM migration +- Full backend test execution still depends on local Python/runtime dependency hygiene outside this single route family + +### Validation run for this workstream + +The following checks were run after implementation: + +- `python3 -m py_compile backend/auth/dependencies.py backend/api/routes_crm.py backend/main.py backend/tests/test_crm_routes.py` + - result: OK +- targeted route tests: + - command: + `PYTHONPATH="/tmp/velocity_pydeps:$PYTHONPATH" python3 -m pytest backend/tests/test_crm_routes.py -q` + - result: `4 passed in 0.33s` +- temporary local validation dependencies were installed outside the repo under `/tmp/velocity_pydeps` only so the route tests could run on this machine's incomplete Python environment + +### Current judgment after Workstream 4 + +This workstream was successful. + +It closed one of the most important backend production gaps that directly affected the iPad app: lead data is no longer served from an unauthenticated, unscoped legacy route. + +It did not solve the broader canonical CRM migration or enterprise-grade tenant modeling problem, but it materially improved security and correctness without breaking the current iPad contract. + +It is complete enough to move to the next item once approved. + +## Workstream 5 Status - Oracle Production-Safe Mode Gating + +Date completed: 2026-04-22 +Status: materially improved and complete enough to move to the next workstream, with full Oracle feature parity intentionally deferred + +### What was changed + +The following repo-local changes were completed in the active iPad Oracle surface: + +- Added a dedicated Oracle production-scope policy helper in `OracleModeAvailability.swift` +- Explicitly defined the production-visible Oracle modes as: + - `Pipeline` + - `Account Timeline` + - `Calendar & Tasks` +- Explicitly defined `Team Performance` and `Lead Map` as hidden until backend support exists +- Hardened `OracleView` so unsupported mode selections are sanitized back to `Pipeline` +- Updated the Oracle mode picker so the production UI no longer advertises unsupported modes +- Added an in-product scope note explaining that only live-backed Oracle views are intentionally shown in the production iPad build +- Added smoke-test coverage for: + - visible Oracle production modes + - hidden unsupported modes + - selection sanitization behavior + +### What is materially better now + +- The production iPad Oracle surface no longer over-promises capability by showing pills for unsupported views +- Stakeholder demos are safer because the UI now matches the real mobile backend scope more honestly +- Accidental deep-linking or stale state into unsupported Oracle modes now falls back to a known-good live mode instead of leaving the user in an invalid product state +- The Oracle production story is narrower, but materially more trustworthy + +### What remains blocked or incomplete + +- The backend still does not expose real mobile contracts for `Team Performance` or `Lead Map` +- This workstream intentionally hides unsupported Oracle modes; it does not implement them +- Full Xcode test execution is still blocked on this machine because full Xcode is not installed +- A real-device validation pass is still required to confirm the Oracle UX feels correct on the target iPad hardware + +### Validation run for this workstream + +The following checks were run after implementation: + +- `swiftc -typecheck` against the Oracle production-scope helper and a preview-free validation copy of `OracleView.swift` + - result: OK +- static inspection confirmed: + - the Oracle picker now shows only `Pipeline`, `Account Timeline`, and `Calendar & Tasks` + - unsupported Oracle selections are sanitized back to `Pipeline` + - the production-scope note is present in the live-backed Oracle canvases +- smoke-test coverage was expanded in `velocityTests/VelocitySmokeTests.swift` + - execution is still pending a full Xcode environment on this machine + +### Current judgment after Workstream 5 + +This workstream was successful. + +It did not make Oracle feature-complete on iPad, but it did make the production build materially more honest and robust by aligning the visible surface area with the backend that actually exists today. + +It is complete enough to move to the next item once approved. + +## Workstream 6 Status - Dollhouse Asset Gating and Production-Safe Inventory Scope + +Date completed: 2026-04-22 +Status: materially improved and complete enough to move to the next workstream, with real Dollhouse content still intentionally deferred until the asset exists + +### What was changed + +The following repo-local changes were completed in the active iPad Inventory surface: + +- Added a dedicated inventory production-scope policy helper in `InventoryModeAvailability.swift` +- Defined Dollhouse visibility as asset-gated rather than always advertised +- The production mode picker now shows: + - `Sunseeker` + - `Dream Weaver` + - `Dollhouse` only when a real `Building.usdz` or `Building.scn` asset is present in the bundle +- Hardened `InventoryView` so stale or unsupported `Dollhouse` selections are sanitized back to `Sunseeker` +- Updated the Inventory subtitle so the visible mode list reflects the assets actually shipped in the production build +- Added an in-product production-scope card explaining why Dollhouse is hidden when the asset is missing +- Aligned the SceneKit Dollhouse loader with the same shared asset-candidate list used by the production gate +- Added smoke-test coverage for: + - hidden Dollhouse mode when no asset is shipped + - visible Dollhouse mode when an asset is present + - selection sanitization behavior + +### What is materially better now + +- The production iPad build no longer advertises a Dollhouse experience that it cannot actually deliver +- Investor and stakeholder demos are safer because the Inventory surface now matches the app bundle contents more honestly +- If a stale state or future persistence bug points the user at `Dollhouse` without an asset, the UI now falls back to a known-good mode instead of presenting a meaningless SceneKit floor as if it were a real feature +- If the real Dollhouse asset is later added to the bundle, the mode can reappear automatically without another scope-policy rewrite + +### What remains blocked or incomplete + +- No real `Building.usdz` or `Building.scn` asset exists in the current iPad bundle +- This workstream intentionally hides Dollhouse when the asset is absent; it does not create the missing 3D experience +- Full Xcode test execution is still blocked on this machine because full Xcode and the iOS simulator SDK are not installed +- Real-device validation is still required if the Dollhouse asset is later shipped and re-enabled + +### Validation run for this workstream + +The following checks were run after implementation: + +- filesystem validation confirmed there is still no `Building.usdz` or `Building.scn` under `iOS/velocity-ipad` +- `swiftc -typecheck` against `InventoryModeAvailability.swift` with a temporary local stub for `InventoryStore.Mode` + - result: OK +- static inspection confirmed: + - the Inventory picker now uses production-visible modes rather than `Mode.allCases` + - missing Dollhouse assets trigger a production-scope notice + - stale `Dollhouse` selections are sanitized back to `Sunseeker` + - the SceneKit loader and the production gate now share the same asset-candidate list +- direct typechecking of the full Inventory view against real iOS frameworks could not be completed on this machine because Command Line Tools are present but the iOS simulator SDK is not installed +- smoke-test coverage was expanded in `velocityTests/VelocitySmokeTests.swift` + - execution is still pending a full Xcode environment on this machine + +### Current judgment after Workstream 6 + +This workstream was successful. + +It did not make Dollhouse real, but it did make the production Inventory surface materially more honest and robust by hiding the feature until the required 3D asset actually exists. + +It is complete enough to move to the next item once approved. + +## Workstream 7 Status - Sentinel Relabeling and Operator-Posture Production Framing + +Date completed: 2026-04-22 +Status: materially improved and complete enough to move to the next workstream, with real Sentinel analytics still intentionally deferred + +### What was changed + +The following repo-local changes were completed across the active iPad shell and Sentinel-facing surfaces: + +- Added a dedicated Sentinel production-scope helper in `SentinelScope.swift` +- Preserved the internal `AppSection.sentinel` identity while adding a separate production display title +- Updated the sidebar navigation so the user-facing label is now `Operator Posture` instead of plain `Sentinel` +- Updated the Sentinel screen header so: + - `Sentinel` remains visible as the product family + - `Operator Posture` is the large production surface title +- Reworked the Sentinel availability card so it now uses: + - `Production Scope` as the section title + - `Operator posture only` as the badge state + - explicit capability wording for what is disabled versus what is live-backed +- Updated Settings production notes so the product messaging stays aligned with the relabeled iPad scope +- Added smoke-test coverage for: + - user-facing section display titles + - Sentinel scope constants and production phrasing + +### What is materially better now + +- The iPad app no longer markets this surface as if it were already a full production Sentinel analytics console +- Investor and operator demos are safer because the navigation, screen header, and production notes now tell the same truthful story +- Internal section identity remains stable, so the relabeling does not force a fragile route or state migration +- The production posture language is now centralized in one helper instead of being scattered across ad hoc strings + +### What remains blocked or incomplete + +- The backend still does not provide the real Sentinel perception stream needed for visitor analytics +- This workstream intentionally relabels and scopes the iPad experience; it does not implement live visitor analytics +- Full Xcode test execution is still blocked on this machine because full Xcode and the iOS simulator SDK are not installed +- Real-device validation is still required once a true production Sentinel feed exists + +### Validation run for this workstream + +The following checks were run after implementation: + +- `swiftc -typecheck iOS/velocity-ipad/velocity/Features/Sentinel/SentinelScope.swift` + - result: OK +- static inspection confirmed: + - the sidebar now displays `Operator Posture` for the Sentinel section + - the Sentinel screen header preserves `Sentinel` as the product family while using `Operator Posture` as the truthful iPad title + - the availability badge now reads `Operator posture only` + - Settings production notes now use the same scope language +- smoke-test coverage was expanded in `velocityTests/VelocitySmokeTests.swift` + - execution is still pending a full Xcode environment on this machine + +### Current judgment after Workstream 7 + +This workstream was successful. + +It did not turn the iPad surface into real production Sentinel analytics, but it did make the app materially more honest and investor-safe by aligning the user-facing framing with the backend reality that exists today. + +It is complete enough to move to the next item once approved. + +## Workstream 8 Status - WebOS-Grounded Shared Refresh Hardening + +Date completed: 2026-04-22 +Status: materially improved and complete enough to move to the next workstream, with deeper shared-platform optimization still open + +### What was changed + +This workstream was explicitly grounded against the Velocity WebOS implementation under `app/`, especially: + +- `app/src/lib/velocityPlatformClient.ts` +- `app/src/hooks/useCrmBootstrap.ts` + +The following repo-local changes were completed in the active iPad shared data layer: + +- Added a dedicated `AppStoreRefreshPolicy.swift` helper to centralize shared hydration limits and refresh scope +- Aligned the iPad inventory property hydration limit to the WebOS bootstrap default of `100` +- Refactored `AppStore.refresh()` so concurrent callers now reuse a single in-flight refresh task instead of kicking off overlapping refreshes independently +- Replaced sequential per-lead communication fetches with parallel task-group hydration for the prioritized iPad lead set +- Kept the iPad lead-event hydration set intentionally smaller than WebOS: + - WebOS bootstraps a broader CRM/chat context + - iPad continues to focus on the top operator-facing lead slice for its smaller surfaces +- Added smoke-test coverage for: + - WebOS-aligned inventory hydration limit + - iPad lead prioritization policy + +### What is materially better now + +- The iPad shared data bootstrap now follows the WebOS property-slice baseline for shared inventory-backed surfaces +- Repeated refresh calls from multiple iPad screens are materially less wasteful because they now coalesce around one active refresh task +- Lead-event hydration is no longer serialized one lead at a time, reducing avoidable latency in operator surfaces that depend on timeline data +- The shared refresh behavior is now easier to audit because the scope policy lives in one place instead of being buried in inline magic numbers + +### What remains blocked or incomplete + +- The iPad app still relies on per-lead event fetches rather than a true bulk mobile-edge endpoint +- Refresh timers across multiple screens still exist; this workstream reduces duplicate work but does not redesign the entire refresh architecture +- WebOS and iPad still intentionally diverge on lead-event hydration depth because the iPad UI shows a narrower operator slice +- Full Xcode test execution is still blocked on this machine because full Xcode is not installed + +### Validation run for this workstream + +The following checks were run after implementation: + +- `swiftc -typecheck` across: + - `SessionConfiguration.swift` + - `AppConfig.swift` + - `AppStoreRefreshPolicy.swift` + - `VelocityAPIClient.swift` + - `AppStore.swift` + - result: OK +- static comparison against WebOS confirmed: + - WebOS inventory hydration uses `listInventoryProperties(100)` + - iPad property hydration now aligns to that same `100` row default + - WebOS hydrates lead-linked data in parallel + - iPad lead-linked event hydration now also runs in parallel for its prioritized operator slice +- smoke-test coverage was expanded in `velocityTests/VelocitySmokeTests.swift` + - execution is still pending a full Xcode environment on this machine + +### Current judgment after Workstream 8 + +This workstream was successful. + +It did not eliminate every refresh inefficiency in the iPad app, but it materially improved performance discipline and shared-platform parity by aligning the iPad bootstrap more closely with the WebOS ground truth without changing WebOS behavior. + +It is complete enough to move to the next item once approved. + +## Workstream 9 Status - Shared Backend Tenant Scoping Hardening for WebOS and iPad + +Date completed: 2026-04-22 +Status: materially improved and complete enough to move to the next workstream, with true user-to-tenant modeling still open + +### What was changed + +This workstream was explicitly grounded against the shared WebOS and iPad dependency surface, especially: + +- `app/src/lib/velocityPlatformClient.ts` +- `iOS/velocity-ipad/velocity/Core/Networking/VelocityAPIClient.swift` + +The following repo-local changes were completed in the backend route families both clients depend on: + +- Added explicit tenant-scope helpers to: + - `backend/api/routes_mobile_edge.py` + - `backend/api/routes_inventory.py` +- Replaced role-based tenant scoping with `user.tenant_id` throughout the shared mobile-edge route family +- Replaced role-based tenant scoping with `user.tenant_id` throughout the shared inventory route family +- Added focused backend tests covering tenant isolation for: + - `mobile-edge/events` + - `mobile-edge/calendar` + - `inventory/properties` + - `inventory/import-batches` +- Normalized these shared route files from `datetime.UTC` to `timezone.utc` so the local validation path is less fragile on Python environments older than 3.11 + +### What is materially better now + +- WebOS and iPad shared reads and writes are no longer scoped by role in these route families +- Users who share the same role but belong to different tenants are now materially less likely to see cross-tenant data leakage through mobile-edge or inventory routes +- The backend contract now better matches the auth layer, which already carries a first-class `tenant_id` +- Shared route coverage is stronger because tenant isolation is now proven with targeted tests instead of assumed from inspection + +### What remains blocked or incomplete + +- shared routes are now backed by a user directory that resolves tenant scope per user, but the overall backend still lacks a fuller org or tenant-membership model +- runtime schema mutation is still being used for interim compatibility instead of an explicit migration chain +- this workstream hardens shared route families but does not complete the broader tenant-model redesign across the whole backend +- full backend suite execution is still limited by local dependency and environment hygiene outside the targeted route families + +### Validation run for this workstream + +The following checks were run after implementation: + +- `python3 -m py_compile` across: + - `backend/api/routes_mobile_edge.py` + - `backend/api/routes_inventory.py` + - `backend/tests/test_surface_route_tenant_scoping.py` + - result: OK +- targeted shared-route tests: + - command: + `PYTHONPATH="/tmp/velocity_pydeps:$PYTHONPATH" python3 -m pytest backend/tests/test_surface_route_tenant_scoping.py -q` + - result: `4 passed in 0.55s` +- regression check on the earlier CRM hardening tests: + - command: + `PYTHONPATH="/tmp/velocity_pydeps:$PYTHONPATH" python3 -m pytest backend/tests/test_crm_routes.py -q` + - result: `4 passed in 0.36s` +- static inspection confirmed there are no remaining `user.role` tenant-scope calls inside the shared `routes_mobile_edge.py` and `routes_inventory.py` families + +### Current judgment after Workstream 9 + +This workstream was successful. + +It did not complete the full multi-tenant architecture, but it materially improved the shared backend contract that both WebOS and iPad rely on by replacing role-scoped isolation with tenant-scoped isolation in the route families that matter most to both clients today. + +It is complete enough to move to the next item once approved. + +## Workstream 10 Status - Shared Auth Tenant Resolution and Directory Scoping Hardening + +Date completed: 2026-04-22 +Status: materially improved and complete enough to move to the next workstream, with broader tenant-architecture and migration-discipline limitations still open + +### What was changed + +This workstream was explicitly grounded against the shared WebOS and iPad auth contract, especially: + +- `app/src/lib/velocityPlatformClient.ts` +- `iOS/velocity-ipad/velocity/Core/Networking/VelocityAPIClient.swift` + +The following repo-local changes were completed across the shared backend auth layer: + +- Added a dedicated auth schema hardening helper in `backend/auth/user_directory.py` +- Added a dedicated auth service layer in `backend/auth/service.py` so login and user-directory behavior can be validated without importing the entire backend shell +- Moved the shared auth HTTP surface into `backend/auth/routes.py` +- Added a real `tenant_id` column to the `users_and_roles` schema in `backend/db/schema.sql` +- Added runtime backfill and indexing so pre-existing auth rows receive a tenant assignment during bootstrap +- Updated `/api/auth/login` to mint JWTs from the user row's resolved `tenant_id` instead of always falling back to the default tenant +- Updated `/api/auth/me` to read the authenticated profile inside the caller's tenant scope +- Updated `/api/auth/users` to return only active users from the authenticated tenant, which aligns better with the WebOS share modal and any shared user-picker behavior +- Updated the WebOS client types in `app/src/lib/velocityPlatformClient.ts` so `tenant_id` is represented in the shared auth DTOs +- Normalized `backend/main.py` and `backend/api/routes_admin_surface.py` from `datetime.UTC` to `timezone.utc` so local validation is less fragile on Python environments older than 3.11 + +### What is materially better now + +- WebOS and iPad now authenticate through a shared backend path that resolves tenant scope from the user directory instead of defaulting every login to the same tenant +- the user directory has a first-class `tenant_id` field instead of forcing downstream routes to rely on a default-only auth claim +- cross-tenant user leakage risk is materially reduced for shared directory reads such as `/api/auth/users` +- the shared auth contract is more production-shaped because auth route logic now lives in dedicated auth modules instead of being embedded only inside `backend/main.py` +- local backend validation is stronger because auth tenant behavior is now covered with focused tests + +### What remains blocked or incomplete + +- the backend still models only a single direct `tenant_id` on each user instead of a fuller enterprise tenant or membership design +- runtime schema mutation and row backfill are still being used as an interim compatibility mechanism instead of governed migrations +- the local auth tests intentionally stub password verification because the temporary bcrypt/passlib dependency path under `/tmp/velocity_pydeps` is unreliable on this machine +- this workstream hardens shared auth identity and user-directory scope, but it does not complete the broader canonical CRM migration or tenant-admin model + +### Validation run for this workstream + +The following checks were run after implementation: + +- `python3 -m py_compile` across: + - `backend/auth/user_directory.py` + - `backend/auth/service.py` + - `backend/auth/routes.py` + - `backend/main.py` + - `backend/api/routes_admin_surface.py` + - `backend/tests/test_auth_tenant_contract.py` + - result: OK +- targeted shared auth contract tests: + - command: + `PYTHONPATH="/tmp/velocity_pydeps:$PYTHONPATH" python3 -m pytest backend/tests/test_auth_tenant_contract.py -q` + - result: `4 passed in 0.22s` +- regression check on previously hardened tenant-scoped route families: + - command: + `PYTHONPATH="/tmp/velocity_pydeps:$PYTHONPATH" python3 -m pytest backend/tests/test_surface_route_tenant_scoping.py -q backend/tests/test_crm_routes.py -q` + - result: `8 passed` + +### Current judgment after Workstream 10 + +This workstream was successful. + +It did not complete the final enterprise tenant architecture, but it materially improved the shared auth contract that both WebOS and iPad rely on by making tenant identity come from the user directory, scoping the shared auth directory by tenant, and isolating the auth logic into production-shaped modules. + +It is complete enough to move to the next item once approved. + +## Workstream 11 Status - WebOS Authenticated CRM Bootstrap Parity Hardening + +Date completed: 2026-04-22 +Status: materially improved and complete enough to move to the next workstream, with legacy CRM-contract limitations still open + +### What was changed + +This workstream was explicitly grounded against the current Velocity WebOS bootstrap path, especially: + +- `app/src/hooks/useCrmBootstrap.ts` +- `app/src/lib/api.ts` +- `app/src/lib/velocityPlatformClient.ts` + +The following repo-local changes were completed on the shared WebOS surface: + +- Added a small shared session helper in `app/src/lib/velocitySession.ts` +- Centralized the WebOS bearer-token key and token read/write helpers there so multiple clients stop duplicating session behavior +- Updated the legacy WebOS CRM bootstrap client in `app/src/lib/api.ts` to attach the authenticated Velocity bearer token to `/api/leads`, `/api/chat-logs`, and related shared CRM reads +- Updated legacy WebOS API error handling so protected-route failures surface backend detail strings instead of only generic status codes +- Updated `app/src/lib/crmApi.ts` and `app/src/oracle/lib/oracleApiClient.ts` to use the same shared session helper, keeping canonical CRM, Oracle, and legacy CRM bootstrap on one auth source of truth +- Preserved compatibility for the rest of WebOS by re-exporting the session helpers through `app/src/lib/velocityPlatformClient.ts` + +### What is materially better now + +- WebOS no longer treats the legacy CRM bootstrap path as if it were a public unauthenticated API client +- the backend auth hardening from earlier workstreams now actually lines up with the way the current WebOS app fetches leads and chat logs +- cross-surface auth drift is materially reduced because WebOS legacy CRM, canonical CRM, Oracle, and login/session code now share one token source of truth +- this reduces the risk of breaking the current WebOS production flow while continuing iPad hardening + +### What remains blocked or incomplete + +- WebOS and iPad still both depend on the legacy `/api/leads` family for the current operational bootstrap +- this workstream does not migrate the shared lead bootstrap to canonical CRM tables +- the WebOS frontend bundle still emits a large chunk warning during production build +- lint could not be executed on this machine because the local `node_modules/.bin/eslint` binary is not executable + +### Validation run for this workstream + +The following checks were run after implementation: + +- WebOS production build: + - command: + `npm run build` + - result: success + - note: Vite reported an existing large-chunk warning, but the build completed successfully +- attempted WebOS lint: + - command: + `npm run lint` + - result: blocked by local tool permission error + - detail: `node_modules/.bin/eslint: Permission denied` + +### Current judgment after Workstream 11 + +This workstream was successful. + +It did not solve the broader legacy-versus-canonical CRM architecture problem, but it materially improved production safety by making the current WebOS bootstrap path honor the same authenticated backend contract that the iPad and shared backend now expect. + +It is complete enough to move to the next item once approved. + +## Workstream 12 Status - Dream Weaver Gateway Authentication Hardening + +Date completed: 2026-04-22 +Status: materially improved and complete enough to move to the next workstream, with deployed endpoint validation and infrastructure ownership still open + +### What was changed + +This workstream was focused on the iPad-only Dream Weaver surface and the gateway implementations it depends on. + +The following repo-local changes were completed: + +- Added a shared Dream Weaver gateway auth helper in `comfy_engine/scripts/gateway_auth.py` +- Added optional API-key enforcement support to: + - `dw_gateway_v2_min.py` + - `comfy_engine/scripts/dw_gateway_v2.py` +- Supported common presentation forms for authenticated requests: + - `X-Dream-Weaver-API-Key` + - `X-API-Key` + - `Authorization: Bearer ` +- Kept `/health` available for liveness, but it now reports whether gateway auth is required +- Added a dedicated Dream Weaver gateway API-key path to the iPad runtime configuration and persisted it securely in Keychain +- Updated `ComfyClient` so health probes, route probes, job submission, polling, and result download all send the configured Dream Weaver gateway key when present +- Updated the Settings surface so operators can see whether a Dream Weaver gateway key is configured and manage that key at runtime without rebuilding the app +- Expanded iPad smoke-test scaffolding to cover Dream Weaver gateway-key draft resolution and settings-state behavior + +### What is materially better now + +- Dream Weaver no longer has to be treated as an implicitly open gateway in production +- the iPad app can now authenticate directly to a dedicated Dream Weaver gateway without conflating that secret with core backend auth +- the minimal and richer Dream Weaver gateway variants now share the same auth posture instead of drifting again +- production operators can configure or rotate a Dream Weaver gateway key on-device without rebuilding the app + +### What remains blocked or incomplete + +- this workstream adds an enforceable auth posture, but it does not prove that the deployed public Dream Weaver endpoint is currently running with that auth enabled +- real HTTPS round-trip validation against a live authenticated Dream Weaver deployment is still open +- infrastructure decisions are still unresolved: + - whether Dream Weaver sits behind the same public host and proxy chain as `/api/*` + - or remains a dedicated gateway host with its own ingress and TLS +- `ComfyClient.swift` could not be typechecked on this machine because `UIKit` requires a real iOS SDK / Xcode environment + +### Validation run for this workstream + +The following checks were run after implementation: + +- `python3 -m py_compile` across: + - `comfy_engine/scripts/gateway_auth.py` + - `dw_gateway_v2_min.py` + - `comfy_engine/scripts/dw_gateway_v2.py` + - `backend/tests/test_dream_weaver_gateway_auth.py` + - result: OK +- targeted gateway auth tests: + - command: + `PYTHONPATH="/Users/sayan/Desi Neuron/Project_Velocity:$PYTHONPATH" python3 -m pytest backend/tests/test_dream_weaver_gateway_auth.py -q` + - result: `4 passed in 0.01s` +- Swift configuration-layer validation: + - command: + `swiftc -typecheck iOS/velocity-ipad/velocity/Core/Config/AppConfig.swift iOS/velocity-ipad/velocity/Core/Config/SessionConfiguration.swift` + - result: OK +- attempted `ComfyClient.swift` typecheck: + - blocked because local CLI Swift does not have `UIKit` / iOS SDK access on this machine + +### Current judgment after Workstream 12 + +This workstream was successful. + +It did not prove Dream Weaver production readiness end-to-end, but it materially improved security posture by giving the iPad and both gateway variants a real shared mechanism for gateway authentication instead of leaving Dream Weaver as an implicitly open production service. + +It is complete enough to move to the next item once approved. + +## Workstream 13 Status - Canonical CRM Route Authentication Hardening + +Date completed: 2026-04-22 +Status: materially improved and complete enough to move to the next workstream, with canonical tenant modeling and legacy-to-canonical lead migration still open + +### What was changed + +This workstream was grounded against the shared WebOS production behavior before any backend change was made: + +- WebOS already sends the authenticated Velocity bearer token through `app/src/lib/crmApi.ts` for canonical `/api/crm/*` reads and writes +- the iPad app does not yet depend directly on the canonical CRM route family for its lead bootstrap +- because of that shared contract, it was safe to harden the canonical CRM backend surface without breaking the current WebOS or iPad runtime assumptions + +The following repo-local changes were completed: + +- Hardened the entire canonical CRM router in `backend/api/routes_crm_imports.py` so all `/api/crm/*` endpoints now require the shared Velocity bearer token +- Added focused backend tests covering: + - unauthenticated access rejection for canonical contact routes + - unauthenticated access rejection for canonical task routes + - unauthenticated access rejection for canonical CRM import upload + - successful authenticated access for a representative canonical contact-list read + +### What is materially better now + +- the backend no longer exposes the canonical CRM route family as an implicitly public surface +- WebOS canonical CRM usage is now aligned with the backend auth posture it was already coded to use +- the shared platform is safer to evolve because the canonical CRM surface now follows the same authenticated session contract as legacy CRM, mobile-edge, inventory, and auth +- this removes an important production-readiness blocker that had to be closed before a real legacy-to-canonical CRM migration could be trusted + +### What remains blocked or incomplete + +- this workstream does not add full tenant scoping to canonical CRM tables +- canonical CRM schema still lacks a mature tenant-bound data model across `crm_*`, `intel_*`, and `workflow_*` +- this workstream does not move the iPad or current WebOS lead bootstrap off `/api/leads` +- the broader goal of converging on one canonical CRM contract is still open and should be handled as a follow-on migration workstream + +### Validation run for this workstream + +The following checks were run after implementation: + +- `python3 -m py_compile` across: + - `backend/api/routes_crm_imports.py` + - `backend/tests/test_canonical_crm_auth.py` + - result: OK +- targeted canonical CRM auth tests: + - command: + `PYTHONPATH="/tmp/velocity_pydeps:/Users/sayan/Desi Neuron/Project_Velocity:$PYTHONPATH" python3 -m pytest backend/tests/test_canonical_crm_auth.py -q` + - result: `4 passed in 0.31s` +- shared auth regression check: + - command: + `PYTHONPATH="/tmp/velocity_pydeps:/Users/sayan/Desi Neuron/Project_Velocity:$PYTHONPATH" python3 -m pytest backend/tests/test_auth_tenant_contract.py -q` + - result: `4 passed in 0.20s` +- legacy CRM regression check: + - command: + `PYTHONPATH="/tmp/velocity_pydeps:/Users/sayan/Desi Neuron/Project_Velocity:$PYTHONPATH" python3 -m pytest backend/tests/test_crm_routes.py -q` + - result: `4 passed in 0.40s` + +### Current judgment after Workstream 13 + +This workstream was successful. + +It does not complete the canonical CRM migration, but it closes an important production-safety gap by ensuring the canonical CRM route family now honors the same authenticated Velocity session model already used by WebOS and expected by the wider platform. + +It is complete enough to move to the next item once approved. + +## Workstream 14 Status - Canonical CRM Tenant Scoping Hardening + +Date completed: 2026-04-22 +Status: materially improved and complete enough to move to the next workstream, with the actual shared lead-bootstrap migration still open + +### What was changed + +This workstream was chosen as the next prerequisite after Workstream 13. + +Reason: + +- moving WebOS or iPad reads directly from legacy `/api/leads` onto canonical `/api/crm/*` would still have been unsafe because the canonical CRM route family was not yet tenant-scoped +- the correct architecturally safe order was: + - authenticate canonical CRM first + - then add first-pass tenant scoping to the canonical route family and its backing read/write paths + - only after that start migrating shared lead bootstrap away from legacy tables + +The following repo-local changes were completed: + +- Added canonical CRM schema hardening helper in `backend/crm/canonical_schema.py` +- Added runtime tenant hardening for the currently active canonical CRM tables used by shared route families, including: + - `crm_people` + - `crm_accounts` + - `crm_leads` + - `crm_opportunities` + - `crm_property_interests` + - `intel_interactions` + - `intel_reminders` + - `intel_qd_scores` + - `intel_qd_timeseries` + - `workflow_actions` + - `workflow_approvals` + - `workflow_import_batches` +- Added tenant-aware indexes for the canonical CRM tables currently serving shared WebOS canonical CRM routes +- Updated `backend/api/routes_crm_imports.py` so canonical CRM reads and writes now scope by the authenticated `tenant_id` +- Updated canonical aggregation helpers in `backend/services/client_graph/aggregation_service.py` so Client 360 and contact-list queries now resolve within tenant scope +- Updated CRM import persistence in `backend/services/imports/ingest_service.py` so canonical workflow batches and workflow actions now carry tenant scope +- Updated `backend/db/schema_crm_canonical.sql` so the canonical schema source now records the new tenant-aware direction + +### What is materially better now + +- canonical CRM no longer behaves like a tenant-blind shared data surface for the currently active route families +- WebOS canonical CRM surfaces now align more closely with the same tenant-bound auth model already used by shared auth, mobile-edge, inventory, and legacy CRM +- the platform is now in a much safer state for the next real migration step off legacy `/api/leads` +- canonical CRM imports, contacts, tasks, Client 360 reads, kanban, QD reads, and opportunity reads now have materially better tenant isolation posture than they had at audit start + +### What remains blocked or incomplete + +- this workstream does not yet move the shared WebOS or iPad lead bootstrap off legacy `/api/leads` +- canonical CRM tenant hardening here is a first-pass route-family hardening, not the final enterprise tenant architecture for every canonical table and workflow +- runtime schema mutation and default-tenant backfill are still being used as an interim compatibility strategy +- broader canonical CRM convergence still remains an open workstream + +### Validation run for this workstream + +The following checks were run after implementation: + +- `python3 -m py_compile` across: + - `backend/crm/canonical_schema.py` + - `backend/services/imports/ingest_service.py` + - `backend/services/client_graph/aggregation_service.py` + - `backend/api/routes_crm_imports.py` + - `backend/tests/test_canonical_crm_auth.py` + - `backend/tests/test_canonical_crm_tenant_scoping.py` + - result: OK +- targeted canonical CRM auth regression: + - command: + `PYTHONPATH="/tmp/velocity_pydeps:/Users/sayan/Desi Neuron/Project_Velocity:$PYTHONPATH" python3 -m pytest backend/tests/test_canonical_crm_auth.py -q` + - result: `4 passed in 0.39s` +- targeted canonical CRM tenant-scoping tests: + - command: + `PYTHONPATH="/tmp/velocity_pydeps:/Users/sayan/Desi Neuron/Project_Velocity:$PYTHONPATH" python3 -m pytest backend/tests/test_canonical_crm_tenant_scoping.py -q` + - result: `4 passed in 0.42s` +- shared auth regression check: + - command: + `PYTHONPATH="/tmp/velocity_pydeps:/Users/sayan/Desi Neuron/Project_Velocity:$PYTHONPATH" python3 -m pytest backend/tests/test_auth_tenant_contract.py -q` + - result: `4 passed in 0.22s` +- legacy CRM regression check: + - command: + `PYTHONPATH="/tmp/velocity_pydeps:/Users/sayan/Desi Neuron/Project_Velocity:$PYTHONPATH" python3 -m pytest backend/tests/test_crm_routes.py -q` + - result: `4 passed in 0.31s` + +### Current judgment after Workstream 14 + +This workstream was successful. + +It does not complete the legacy-to-canonical CRM migration, but it removes a serious prerequisite blocker by giving the canonical CRM route family a first-pass tenant-scoped data boundary that matches the authenticated Velocity session contract more honestly. + +It is complete enough to move to the next item once approved. + +## Workstream 15 Status - Shared Legacy CRM Read Bridge Toward Canonical CRM + +Date completed: 2026-04-22 +Status: materially improved and complete enough to move to the next workstream, with write-path convergence and final legacy-table retirement still open + +### What was changed + +This workstream was grounded against the current shared WebOS and iPad dependency shape before any code was changed: + +- WebOS bootstrap still reads: + - `/api/leads` + - `/api/chat-logs` +- iPad still reads: + - `/api/leads` +- WebOS canonical CRM modules already use `/api/crm/*` +- a direct route-path cutover would have broken the current shared client behavior because WebOS still ties `/api/leads` identities to `/api/chat-logs` + +Because of that, the correct next step was not a brittle frontend cutover. + +The following repo-local changes were completed instead: + +- Added a canonical-read compatibility bridge inside `backend/api/routes_crm.py` +- Updated legacy `/api/leads` reads so they now: + - prefer canonical CRM lead data when canonical rows exist for the authenticated tenant + - preserve the existing legacy response shape expected by WebOS and iPad + - merge in legacy-only rows that are not yet represented in canonical CRM + - avoid duplicate rows when canonical records shadow legacy IDs +- Updated legacy `/api/leads/{id}` reads so canonical records can now be resolved by: + - legacy-compatible lead ID + - canonical lead ID + - canonical person ID +- Updated legacy `/api/chat-logs` reads so they now fall back to canonical interaction summaries when no legacy chat-log rows exist for a requested lead +- Expanded canonical-to-legacy status mapping so canonical CRM stages now translate more truthfully into the current shared legacy lead contract +- Added focused backend bridge tests covering: + - canonical-plus-legacy merge behavior + - canonical lead lookup through the legacy route family + - canonical interaction fallback for legacy chat-log reads + - legacy fallback behavior when canonical bridge access is unavailable + +### What is materially better now + +- the shared WebOS and iPad bootstrap path is now meaningfully closer to canonical CRM without requiring an abrupt frontend contract change +- canonical CRM can now progressively become the effective source of truth for shared lead reads while preserving the current production client shape +- tenants that already have canonical CRM data can now benefit from it through the legacy compatibility surface +- the platform has a safer migration path because WebOS bootstrap, iPad bootstrap, and canonical CRM no longer need to be cut over in one risky move + +### What remains blocked or incomplete + +- this workstream improves shared read-path convergence only; it does not migrate legacy CRM writes to canonical tables +- `/api/chat-logs` canonical fallback currently uses canonical interaction summaries, not a full canonical message-thread model +- legacy `leads` and `chat_logs` tables still exist and still back write paths +- final retirement of the legacy CRM tables remains a later workstream + +### Validation run for this workstream + +The following checks were run after implementation: + +- `python3 -m py_compile` across: + - `backend/api/routes_crm.py` + - `backend/tests/test_legacy_crm_canonical_bridge.py` + - result: OK +- targeted shared legacy-to-canonical bridge tests: + - command: + `PYTHONPATH="/tmp/velocity_pydeps:/Users/sayan/Desi Neuron/Project_Velocity:$PYTHONPATH" python3 -m pytest backend/tests/test_legacy_crm_canonical_bridge.py -q` + - result: `4 passed in 0.49s` +- legacy CRM regression check: + - command: + `PYTHONPATH="/tmp/velocity_pydeps:/Users/sayan/Desi Neuron/Project_Velocity:$PYTHONPATH" python3 -m pytest backend/tests/test_crm_routes.py -q` + - result: `4 passed` +- canonical CRM regression checks: + - command: + `PYTHONPATH="/tmp/velocity_pydeps:/Users/sayan/Desi Neuron/Project_Velocity:$PYTHONPATH" python3 -m pytest backend/tests/test_canonical_crm_auth.py -q backend/tests/test_canonical_crm_tenant_scoping.py -q` + - result: `8 passed` + +### Current judgment after Workstream 15 + +This workstream was successful. + +It does not complete the final legacy-to-canonical CRM cutover, but it materially improves production safety and migration discipline by letting the current shared `/api/leads` and `/api/chat-logs` compatibility surface read from canonical CRM where that data already exists, without breaking the current WebOS or iPad client contract. + +It is complete enough to move to the next item once approved. + +## Workstream 16 Status - Shared Legacy CRM Write Bridge Toward Canonical CRM + +Date completed: 2026-04-22 +Status: materially improved and complete enough to move to the next workstream, with final legacy-table retirement and broader canonical workflow depth still open + +### What was changed + +This workstream followed Workstream 15 intentionally. + +Reason: + +- after improving shared read convergence, the next source of drift was legacy write behavior +- WebOS and iPad still keep the legacy CRM route family alive as a compatibility surface +- if legacy writes continued updating only `leads` and `chat_logs`, the platform would keep generating fresh divergence from canonical CRM even after the read bridge was improved + +The following repo-local changes were completed: + +- Added best-effort canonical write bridge helpers in `backend/api/routes_crm.py` +- Updated legacy lead creation so it now also attempts to sync the created lead into canonical CRM +- Updated legacy lead edits so they now also attempt to sync the updated lead into canonical CRM +- Updated legacy kanban moves so they now also attempt to sync canonical lead status and canonical stage history +- Updated legacy chat-log creation so it now also attempts to: + - ensure the corresponding canonical lead binding exists + - write a canonical interaction record + - write a canonical message record +- Updated legacy lead deletion so it now also attempts canonical cleanup for the linked canonical lead path +- Kept all of this as best-effort compatibility behavior so the current legacy route family still works even if canonical CRM schema access is temporarily unavailable in a given environment +- Added focused backend tests verifying that legacy write routes trigger the canonical bridge without changing the current route contract + +### What is materially better now + +- the platform no longer has to accept ongoing guaranteed drift every time the legacy compatibility surface performs a lead mutation +- legacy lead writes, chat-log writes, and kanban moves now push the system meaningfully closer to canonical CRM instead of only widening the gap +- the shared WebOS and iPad compatibility surface is now much closer to a controlled migration layer rather than a permanently divergent second CRM +- this makes the eventual retirement of legacy tables more realistic because both shared reads and shared writes now have a canonical bridge path + +### What remains blocked or incomplete + +- this is still a compatibility bridge, not final legacy-table retirement +- legacy tables still remain part of the active runtime path +- canonical cleanup on delete is best-effort and intentionally conservative +- broader canonical workflow depth such as richer message-thread modeling and full writeback governance remains future work + +### Validation run for this workstream + +The following checks were run after implementation: + +- `python3 -m py_compile` across: + - `backend/api/routes_crm.py` + - `backend/tests/test_legacy_crm_write_bridge.py` + - result: OK +- targeted shared legacy write-bridge tests: + - command: + `PYTHONPATH="/tmp/velocity_pydeps:/Users/sayan/Desi Neuron/Project_Velocity:$PYTHONPATH" python3 -m pytest backend/tests/test_legacy_crm_write_bridge.py -q` + - result: `4 passed in 0.29s` +- legacy CRM regression check: + - command: + `PYTHONPATH="/tmp/velocity_pydeps:/Users/sayan/Desi Neuron/Project_Velocity:$PYTHONPATH" python3 -m pytest backend/tests/test_crm_routes.py -q backend/tests/test_legacy_crm_canonical_bridge.py -q` + - result: `8 passed` +- canonical CRM regression checks: + - command: + `PYTHONPATH="/tmp/velocity_pydeps:/Users/sayan/Desi Neuron/Project_Velocity:$PYTHONPATH" python3 -m pytest backend/tests/test_canonical_crm_auth.py -q backend/tests/test_canonical_crm_tenant_scoping.py -q` + - result: `8 passed` + +### Current judgment after Workstream 16 + +This workstream was successful. + +It does not yet retire the legacy CRM tables, but it materially improves production readiness by ensuring the active shared legacy compatibility surface now attempts to converge both reads and writes toward canonical CRM instead of only bridging reads. + +It is complete enough to move to the next item once approved. + +## Workstream 17 Status - iPad Canonical CRM Bootstrap Alignment + +Date completed: 2026-04-22 +Status: materially improved and complete enough to move to the next workstream, with deeper canonical CRM screen coverage still open + +### What was changed + +Architectural correction received on 2026-04-22 from Sagnik: + +- the canonical CRM source of truth for both WebOS and iPad is the `crm_* + intel_* + workflow_*` model +- the canonical API contract is rooted under `/api/crm/*` +- legacy `leads` / `chat_logs` and `/api/leads*` remain compatibility infrastructure, but they are not the target product contract for iPad work + +Based on that correction, the following repo-local iPad changes were completed: + +- Updated the iPad live CRM bootstrap in `iOS/velocity-ipad/velocity/Core/Networking/VelocityAPIClient.swift` +- The iPad app no longer fetches its live CRM bootstrap from `/api/leads` +- The iPad app now fetches canonical contact summaries from: + - `/api/crm/contacts` +- Added an iPad-side canonical adapter that now: + - filters the canonical contact list down to rows that actually have active lead context + - preserves the current iPad shell DTO shape so the existing Dashboard, Communications, and Oracle views do not break + - normalizes canonical QD score values into the current iPad 0-100 scoring presentation + - maps canonical lead status into the current iPad stage/status presentation +- Updated iPad copy in the active shell so it now speaks more truthfully about canonical CRM contacts and pipeline context instead of implying that the app is directly backed by legacy lead rows +- Added an iPad smoke-test assertion covering: + - canonical contact filtering + - score normalization + - canonical status mapping into the existing shell model + +### What is materially better now + +- the iPad app is now aligned with the same canonical CRM direction already used by WebOS CRM modules, Sentinel, and Oracle backend data access +- the iPad production path no longer depends on `/api/leads` for its live CRM bootstrap +- the current iPad shell now has a stable local adapter contract instead of directly inheriting a legacy route shape +- this reduces the chance that future iPad work accidentally deepens dependence on the old `leads` / `chat_logs` model + +### What remains blocked or incomplete + +- this workstream only aligns the iPad CRM bootstrap source; it does not yet create first-class iPad screens for: + - Contacts list + - Contact detail + - Client 360 + - Opportunities + - Tasks + - Kanban + - Imports +- iPad communications still consume `/api/mobile-edge/*`, and those event routes remain lead-ID keyed today +- because of that, the current iPad adapter intentionally filters canonical contacts down to records that still have active lead context +- a real Xcode build/test run still could not be completed from this machine because full Xcode is not installed + +### Validation run for this workstream + +The following checks were run after implementation: + +- static route verification: + - command: + `rg -n "/api/leads|/api/crm/contacts|activeLeadSummaries" iOS/velocity-ipad/velocity iOS/velocity-ipad/velocityTests` + - result: + - canonical `/api/crm/contacts` adapter usage found in the active iPad networking layer + - no remaining `/api/leads` bootstrap reference found inside the active iPad target +- Swift parse validation: + - command: + `swiftc -frontend -parse iOS/velocity-ipad/velocity/Core/Networking/VelocityAPIClient.swift` + - result: OK +- Swift parse validation for affected UI and smoke-test files: + - command: + `swiftc -frontend -parse iOS/velocity-ipad/velocity/Features/Communications/CommunicationsView.swift iOS/velocity-ipad/velocity/Features/Dashboard/DashboardView.swift iOS/velocity-ipad/velocity/Features/Oracle/OracleView.swift iOS/velocity-ipad/velocity/Features/Settings/SettingsView.swift iOS/velocity-ipad/velocityTests/VelocitySmokeTests.swift` + - result: OK +- attempted Xcode build: + - command: + `xcodebuild -project iOS/velocity-ipad/velocity.xcodeproj -scheme velocity -destination 'generic/platform=iOS Simulator' build` + - result: + - failed due local environment limitation: + `xcode-select: error: tool 'xcodebuild' requires Xcode, but active developer directory '/Library/Developer/CommandLineTools' is a command line tools instance` + +### Current judgment after Workstream 17 + +This workstream was successful. + +It does not yet complete the full canonical iPad CRM product surface, but it corrects an important architecture mistake by moving the live iPad CRM bootstrap onto the canonical `/api/crm/*` contract instead of continuing to deepen the legacy `/api/leads` dependency. + +It is complete enough to move to the next item once approved. + +## Workstream 18 Status - iPad Canonical Task and Reminder Coverage + +Date completed: 2026-04-22 +Status: materially improved and complete enough to move to the next workstream, with task writeback and broader canonical CRM screen depth still open + +### What was changed + +This workstream continued the canonical CRM direction established in Workstream 17. + +Reason: + +- after moving the iPad CRM bootstrap onto canonical `/api/crm/contacts`, the next production gap was follow-up workload truth +- the iPad shell still represented follow-up pressure mostly through derived lead state and calendar-only views +- WebOS and the backend already define canonical task/reminder truth under `/api/crm/tasks` + +The following repo-local changes were completed: + +- Added canonical CRM task DTO support to `iOS/velocity-ipad/velocity/Core/Networking/VelocityAPIClient.swift` +- Added a real iPad client method for: + - `/api/crm/tasks` +- Added deterministic iPad-side task ordering so the operator inbox now prioritizes: + - pending tasks first + - then urgent/high priority + - then earlier due dates +- Extended the shared iPad `AppStore` so it now hydrates canonical CRM tasks together with: + - canonical CRM contacts + - calendar events + - inventory + - alerts + - lead-event timeline context +- Updated Dashboard so it now shows: + - pending task count + - urgent task count + - a real canonical `Follow-Up Load` panel populated from `/api/crm/tasks` +- Updated Calendar so it now blends: + - live mobile-edge calendar events + - canonical CRM reminder tasks + into one operator-facing agenda surface +- Updated Oracle `Calendar & Tasks` so it now truthfully includes both: + - calendar events + - canonical follow-up tasks +- Updated Settings so it now reports how many canonical CRM tasks were loaded +- Added iPad smoke-test coverage for canonical task ordering + +### What is materially better now + +- the iPad app now reads real canonical follow-up workload instead of implying task state only through contact summaries or calendar presence +- Dashboard, Calendar, and Oracle now reflect the same canonical reminder system already exposed by the backend +- the iPad operator shell is more aligned with WebOS and backend source-of-truth behavior for reminder/task load +- this closes an important product-honesty gap because the app no longer claims task awareness while ignoring the canonical `/api/crm/tasks` contract + +### What remains blocked or incomplete + +- the iPad app still does not yet support canonical task write actions such as: + - create + - complete + - snooze + - cancel +- this workstream improves read-path task coverage only +- communications still rely on lead-keyed mobile-edge event routes, so the contact/task/event model is not yet fully unified on iPad +- first-class canonical CRM screens such as Client 360, Opportunities, Kanban, and Imports are still not implemented as dedicated iPad surfaces +- a real Xcode build/test run still could not be completed from this machine because full Xcode is not installed + +### Validation run for this workstream + +The following checks were run after implementation: + +- Swift parse validation: + - command: + `swiftc -frontend -parse iOS/velocity-ipad/velocity/Core/Networking/VelocityAPIClient.swift iOS/velocity-ipad/velocity/Core/State/AppStore.swift iOS/velocity-ipad/velocity/Core/State/AppStoreRefreshPolicy.swift iOS/velocity-ipad/velocity/Features/Dashboard/DashboardView.swift iOS/velocity-ipad/velocity/Features/Calendar/CalendarView.swift iOS/velocity-ipad/velocity/Features/Oracle/OracleView.swift iOS/velocity-ipad/velocity/Features/Settings/SettingsView.swift iOS/velocity-ipad/velocityTests/VelocitySmokeTests.swift` + - result: OK +- canonical route verification: + - command: + `rg -n "/api/crm/tasks|Pending CRM tasks loaded|Follow-Up Load|Pending Follow-Up Tasks" iOS/velocity-ipad/velocity iOS/velocity-ipad/velocityTests` + - result: + - canonical `/api/crm/tasks` usage found in the active iPad networking layer + - canonical task surfacing found in Dashboard, Oracle, and Settings +- canonical backend regression checks: + - command: + `PYTHONPATH="/tmp/velocity_pydeps:/Users/sayan/Desi Neuron/Project_Velocity:$PYTHONPATH" python3 -m pytest backend/tests/test_canonical_crm_auth.py -q backend/tests/test_canonical_crm_tenant_scoping.py -q` + - result: `8 passed` +- attempted Xcode build remains blocked in this local environment for the same reason previously recorded: + - full Xcode is not installed on this machine + +### Current judgment after Workstream 18 + +This workstream was successful. + +It does not yet give the iPad app full task writeback workflows, but it materially improves production readiness by making the iPad operator shell consume and present canonical CRM reminder load instead of relying on partial inferred follow-up state. + +It is complete enough to move to the next item once approved. + +## Workstream 19 Status - iPad Oracle Canonical Kanban Alignment + +Date completed: 2026-04-22 +Status: materially improved and complete enough to move to the next workstream, with deeper canonical opportunity and client-detail surfaces still open + +### What was changed + +This workstream continued the same principle the user emphasized: + +- when the iPad app is opened against the real environment, the developed feature should be actually testable against existing backend data + +Reason: + +- after Workstreams 17 and 18, the iPad app was already reading canonical contacts and canonical tasks +- however, the Oracle `Pipeline` screen still synthesized lanes from adapted contact summaries instead of reading the real canonical board contract +- the backend already exposes the correct testable production contract at: + - `/api/crm/kanban` + +The following repo-local changes were completed: + +- Added canonical kanban DTO support to `iOS/velocity-ipad/velocity/Core/Networking/VelocityAPIClient.swift` +- Added a real iPad client method for: + - `/api/crm/kanban` +- Extended the shared iPad `AppStore` so it now hydrates canonical kanban columns together with the other shared live datasets +- Updated Oracle `Pipeline` so it now reads and renders the canonical kanban board directly instead of deriving lanes from adapted contact summaries +- Preserved the backend lane order and lane counts coming from the canonical route family +- Added iPad-side deterministic card ordering within each lane by canonical intent score so the highest-intent records float to the top in a stable way +- Added iPad smoke-test coverage for canonical kanban ordering and score normalization + +### What is materially better now + +- the Oracle pipeline on iPad is now directly backed by the real canonical `/api/crm/kanban` contract +- this makes the pipeline screen meaningfully more testable against the existing production-like database state +- lane counts, labels, and stage visibility now come from the backend source of truth rather than from local grouping logic +- the iPad app is now more internally consistent: + - canonical contacts for bootstrap + - canonical tasks for follow-up load + - canonical kanban for pipeline truth + +### What remains blocked or incomplete + +- this workstream improves pipeline read-path truth only +- the iPad app still does not expose first-class dedicated screens for: + - Opportunities + - Contact detail + - Client 360 + - Imports +- Oracle still does not expose write actions such as stage movement or opportunity mutation from iPad +- a real Xcode build/test run still could not be completed from this machine because full Xcode is not installed + +### Validation run for this workstream + +The following checks were run after implementation: + +- Swift parse validation: + - command: + `swiftc -frontend -parse iOS/velocity-ipad/velocity/Core/Networking/VelocityAPIClient.swift iOS/velocity-ipad/velocity/Core/State/AppStore.swift iOS/velocity-ipad/velocity/Features/Oracle/OracleView.swift iOS/velocity-ipad/velocityTests/VelocitySmokeTests.swift` + - result: OK +- canonical route verification: + - command: + `rg -n "/api/crm/kanban|operatorDisplayBoard|kanbanColumns|Pipeline Summary" iOS/velocity-ipad/velocity iOS/velocity-ipad/velocityTests` + - result: + - canonical `/api/crm/kanban` usage found in the active iPad networking layer + - shared store kanban hydration found in the active iPad state layer + - Oracle pipeline rendering found to depend on canonical kanban columns +- canonical backend regression checks: + - command: + `PYTHONPATH="/tmp/velocity_pydeps:/Users/sayan/Desi Neuron/Project_Velocity:$PYTHONPATH" python3 -m pytest backend/tests/test_canonical_crm_auth.py -q backend/tests/test_canonical_crm_tenant_scoping.py -q` + - result: `8 passed` + +### Current judgment after Workstream 19 + +This workstream was successful. + +It does not yet provide the full canonical CRM product surface on iPad, but it materially improves production readiness by making the Oracle pipeline screen read the real canonical kanban board, which is far more reliable and testable against existing database data than the earlier local grouping approach. + +It is complete enough to move to the next item once approved. + +## Workstream 20 Status - iPad Canonical Client 360 And Deals Coverage + +Date completed: 2026-04-22 +Status: materially improved and complete enough to move to the next workstream, with dedicated contacts and imports workflows still open + +### What was changed + +This workstream continued the requirement that iPad functionality should be genuinely testable against the real data already present in the database. + +Reason: + +- after Workstreams 17 to 19, the iPad app was already reading canonical contacts, tasks, and kanban +- however, it still lacked a practical way to inspect a real canonical client dossier or a real canonical opportunity list from the running app +- the backend already exposes the correct live contracts at: + - `/api/crm/client-360/{person_id}` + - `/api/crm/opportunities` + +The following repo-local changes were completed: + +- Added canonical Client 360 DTO support to `iOS/velocity-ipad/velocity/Core/Networking/VelocityAPIClient.swift` +- Added canonical opportunities DTO support to the same iPad networking layer +- Added real iPad client methods for: + - `/api/crm/client-360/{person_id}` + - `/api/crm/opportunities` +- Extended the shared iPad `AppStore` so it now hydrates canonical opportunity rows together with the other shared live datasets +- Expanded Oracle production scope to include a new live-backed `Deals` mode +- Added a real Oracle `Deals` surface backed by canonical `/api/crm/opportunities` +- Added a real iPad `Client 360` sheet that can now be opened from: + - canonical pipeline cards + - deal cards + - timeline cards + - follow-up task cards inside Oracle +- Added iPad smoke-test coverage for: + - canonical Client 360 snapshot decoding + - opportunities ordering + +### What is materially better now + +- the iPad app can now open a real canonical Client 360 dossier against live backend data +- the iPad app can now verify canonical opportunities directly instead of treating pipeline-only visibility as sufficient +- Oracle is now a more honest and testable production surface because it exposes: + - canonical kanban + - canonical opportunities + - canonical client dossier + - canonical task context +- this materially improves real-device testability against the existing database instead of only improving architectural correctness on paper + +### What remains blocked or incomplete + +- the iPad app still does not yet expose a dedicated contacts list/detail workspace outside the broader shell +- the iPad app still does not yet expose canonical imports workflows +- the iPad app still does not yet provide canonical CRM write actions for: + - stage movement + - opportunity mutation + - task completion / snooze / cancel +- a real Xcode build/test run still could not be completed from this machine because full Xcode is not installed + +### Validation run for this workstream + +The following checks were run after implementation: + +- Swift parse validation: + - command: + `swiftc -frontend -parse iOS/velocity-ipad/velocity/Core/Networking/VelocityAPIClient.swift iOS/velocity-ipad/velocity/Core/State/AppStore.swift iOS/velocity-ipad/velocity/Features/Oracle/OracleModeAvailability.swift iOS/velocity-ipad/velocity/Features/Oracle/OracleView.swift iOS/velocity-ipad/velocityTests/VelocitySmokeTests.swift` + - result: OK +- canonical route verification: + - command: + `rg -n "/api/crm/client-360|/api/crm/opportunities|Client 360|Deals|productionVisibleModes" iOS/velocity-ipad/velocity iOS/velocity-ipad/velocityTests` + - result: + - canonical Client 360 usage found in the active iPad networking layer + - canonical opportunities usage found in the active iPad networking layer + - Oracle production scope now includes a live-backed `Deals` surface +- canonical backend regression checks: + - command: + `PYTHONPATH="/tmp/velocity_pydeps:/Users/sayan/Desi Neuron/Project_Velocity:$PYTHONPATH" python3 -m pytest backend/tests/test_canonical_crm_auth.py -q backend/tests/test_canonical_crm_tenant_scoping.py -q` + - result: `8 passed` + +### Current judgment after Workstream 20 + +This workstream was successful. + +It does not yet complete the full canonical CRM product surface on iPad, but it materially improves production readiness by making live Client 360 and live opportunities directly testable from the app shell against existing backend data. + +It is complete enough to move to the next item once approved. + +## Workstream 21 Status - iPad Canonical Task Writeback And Lead Stage Movement + +Date completed: 2026-04-22 +Status: materially improved and complete enough to move to the next workstream, with opportunity mutation and dedicated CRM workspaces still open + +### What was changed + +This workstream targeted the first missing canonical CRM write path that blocked meaningful production testing on the iPad app. + +Reason: + +- after Workstreams 17 to 20, the iPad app could already read real canonical tasks, kanban lanes, opportunities, and Client 360 dossiers +- however, the operator still could not act on those rows from the iPad, which meant the app was not honestly testable against the live database even though the data already existed +- the canonical schema already defined the correct writeback targets: + - `intel_reminders.status` for task lifecycle state + - `crm_leads.status` plus `crm_stage_history` for lead funnel transitions + +The following repo-local changes were completed: + +- Added canonical backend write endpoints in `backend/api/routes_crm_imports.py` for: + - `PATCH /api/crm/tasks/{reminder_id}` + - `PATCH /api/crm/leads/{lead_id}/stage` +- Implemented task lifecycle validation so canonical reminders can now be: + - marked `done` + - reopened to `pending` + - `snoozed` with a future due timestamp + - `cancelled` +- Implemented canonical lead stage movement with: + - tenant-scoped lead lookup + - status update on `crm_leads` + - stage audit insertion into `crm_stage_history` + - idempotent response behavior when the requested stage already matches the current one +- Tightened canonical timestamp handling so reminder due dates are parsed as ISO-8601 timestamps instead of being silently ignored +- Added iPad client methods for the new canonical write routes in `VelocityAPIClient` +- Extended the shared iPad `AppStore` with canonical CRM mutation helpers that refresh live store state after a successful write +- Added real iPad write controls to the active production-backed surfaces: + - Oracle `Pipeline` now exposes stage movement actions per live kanban card + - Oracle `Calendar & Tasks` now exposes task completion, snooze, reopen, and cancel actions + - Oracle `Client 360` now exposes both lead stage movement and task lifecycle actions inside the dossier + - the main Calendar screen now exposes task completion, snooze, and cancel actions for canonical reminder rows +- Added backend test coverage for: + - auth rejection on the new canonical write routes + - tenant-scoped task mutation + - tenant-scoped lead stage mutation + - canonical stage-history recording +- Added iPad smoke-test coverage for: + - task snooze-date behavior + - canonical lead-stage mutation DTO decoding + +### What is materially better now + +- the iPad app can now perform real canonical reminder lifecycle changes instead of only reading reminder rows +- the iPad app can now move real canonical leads across funnel stages and persist the audit trail expected by the canonical schema +- a user opening the iPad app against the existing database can now genuinely test: + - task completion + - task snooze flows + - task cancellation + - task reopening + - lead stage movement from Oracle +- the iPad shell is now much closer to WebOS-grounded CRM behavior because the app is no longer limited to passive read-only CRM inspection + +### What remains blocked or incomplete + +- superseded by Workstream 22: canonical opportunity mutation is now exposed on iPad +- the iPad app still does not yet expose dedicated contacts and imports workspaces +- a real Xcode build/test run still could not be completed from this machine because full Xcode is not installed + +### Validation run for this workstream + +The following checks were run after implementation: + +- Swift parse validation: + - command: + `swiftc -frontend -parse iOS/velocity-ipad/velocity/Core/Networking/VelocityAPIClient.swift iOS/velocity-ipad/velocity/Core/State/AppStore.swift iOS/velocity-ipad/velocity/Features/Oracle/OracleView.swift iOS/velocity-ipad/velocity/Features/Calendar/CalendarView.swift iOS/velocity-ipad/velocityTests/VelocitySmokeTests.swift` + - result: OK +- canonical backend regression checks: + - command: + `PYTHONPATH="/tmp/velocity_pydeps:/Users/sayan/Desi Neuron/Project_Velocity:$PYTHONPATH" python3 -m pytest backend/tests/test_canonical_crm_auth.py -q backend/tests/test_canonical_crm_tenant_scoping.py -q` + - result: `14 passed` + +### Current judgment after Workstream 21 + +This workstream was successful. + +It does not yet finish the full canonical CRM product surface on iPad, but it materially improves production readiness by making the first real CRM write actions testable from the running iPad shell against existing backend data. + +It is complete enough to move to the next item once approved. + +## Workstream 22 Status - iPad Canonical Opportunity Mutation + +Date completed: 2026-04-23 +Status: materially improved and complete enough to move to the next workstream, with dedicated contacts/imports workspaces still open + +### What was changed + +This workstream completed the next canonical CRM writeback gap: deal/opportunity mutation. + +Reason: + +- after Workstream 21, the iPad app could write canonical task lifecycle state and lead stage transitions +- however, the `Deals` surface still remained read-only even though it displayed real `/api/crm/opportunities` rows +- the canonical source of truth for deals is `crm_opportunities`, not legacy CRM tables + +The following repo-local changes were completed: + +- Added a tenant-scoped canonical backend write endpoint: + - `PATCH /api/crm/opportunities/{opportunity_id}` +- The endpoint can update canonical opportunity fields: + - `stage` + - `value` + - `probability` + - `expected_close_date` + - `next_action` + - `notes` +- Added validation for canonical opportunity stages: + - `prospect` + - `qualified` + - `proposal` + - `site_visit` + - `negotiation` + - `booking` + - `agreement` + - `closed_won` + - `closed_lost` +- Added tenant isolation by resolving the opportunity through its linked canonical `crm_leads` row and authenticated tenant scope +- Updated the shared WebOS CRM adapter in `app/src/lib/crmApi.ts` so WebOS and iPad share the same canonical write contract +- Updated the shared WebOS CRM types to include opportunity notes returned from the canonical endpoint +- Added iPad client and store write helpers for canonical opportunity mutation +- Added real iPad deal actions in Oracle: + - move opportunity stage + - set probability + - set a follow-up next action + - close won + - close lost +- Added the same opportunity mutation controls inside the `Client 360` opportunity card +- Added backend tests for: + - auth rejection on opportunity write routes + - tenant-scoped opportunity mutation + - cross-tenant opportunity rejection +- Added iPad smoke-test coverage for canonical opportunity mutation DTO decoding and probability label normalization + +### What is materially better now + +- the iPad app can now mutate real canonical deal rows instead of only viewing them +- the Deals surface is now genuinely testable against existing database data +- Client 360 is more production-useful because the operator can update deal posture directly from the dossier +- WebOS and iPad now share a single canonical opportunity write contract under `/api/crm/opportunities/{opportunity_id}` + +### What remains blocked or incomplete + +- superseded by Workstream 23 for contacts: the iPad app now exposes a dedicated canonical Clients workspace +- the iPad app still does not yet expose canonical imports workspaces +- custom free-form opportunity editing is not yet exposed in the iPad UI; this pass added robust preset operator actions that map cleanly to the canonical write contract +- a real Xcode build/test run still could not be completed from this machine because full Xcode is not installed + +### Validation run for this workstream + +The following checks were run after implementation: + +- Swift parse validation: + - command: + `swiftc -frontend -parse iOS/velocity-ipad/velocity/Core/Networking/VelocityAPIClient.swift iOS/velocity-ipad/velocity/Core/State/AppStore.swift iOS/velocity-ipad/velocity/Features/Oracle/OracleView.swift iOS/velocity-ipad/velocity/Features/Calendar/CalendarView.swift iOS/velocity-ipad/velocityTests/VelocitySmokeTests.swift` + - result: OK +- canonical backend regression checks: + - command: + `PYTHONPATH="/tmp/velocity_pydeps:/Users/sayan/Desi Neuron/Project_Velocity:$PYTHONPATH" python3 -m pytest backend/tests/test_canonical_crm_auth.py -q backend/tests/test_canonical_crm_tenant_scoping.py -q` + - result: `17 passed` +- WebOS TypeScript validation: + - command: + `npx tsc --noEmit` + - result: OK + +### Current judgment after Workstream 22 + +This workstream was successful. + +It closes the remaining core canonical CRM writeback gap for the existing iPad Oracle CRM surfaces: tasks, lead stages, and opportunities are now all writable through canonical `/api/crm/*` contracts. + +It is complete enough to move to the next item once approved. + +## Workstream 23 Status - iPad Dedicated Canonical Clients Workspace + +Date completed: 2026-04-23 +Status: materially improved and complete enough to move to the next workstream, with canonical imports workspace still open + +### What was changed + +This workstream added the first dedicated canonical CRM contact workspace to the iPad app. + +Reason: + +- the iPad app already consumed `/api/crm/contacts` internally to build dashboard lead summaries +- however, users did not have a first-class place to inspect canonical contacts directly +- the earlier Oracle Client 360 sheet was useful, but discoverability was limited because it depended on entering Oracle surfaces first + +The following repo-local changes were completed: + +- Added a new top-level iPad navigation section: + - `Clients` +- Added a new SwiftUI screen: + - `iOS/velocity-ipad/velocity/Features/Clients/ClientsView.swift` +- Promoted canonical contact rows into shared iPad `AppStore` state +- Added a dedicated iPad client method for canonical contacts: + - `GET /api/crm/contacts` +- Kept the existing lead-summary adapter intact by deriving `VelocityLeadDTO` values from the same canonical contact rows +- Added deterministic contact sorting by intent score and then client name +- Added a searchable contacts workspace that filters by: + - name + - phone + - buyer type + - lead status + - budget + - primary interest + - urgency +- Added contact cards showing: + - canonical name and phone + - buyer type + - lead status + - budget and primary interest + - pending task count + - interaction count + - intent/engagement/urgency-derived score +- Added a Client 360 sheet from each contact card using: + - `GET /api/crm/client-360/{person_id}` +- Updated iPad navigation smoke expectations so the app shell treats `Clients` as a stable production section + +### What is materially better now + +- users can now open the iPad app and directly test canonical contact data without going through Dashboard or Oracle first +- the iPad app now has a first-class contact workspace backed by `crm_people` plus the canonical contact read model +- Client 360 is easier to discover and test because every contact card can open the dossier +- dashboard lead summaries and the dedicated Clients workspace now share the same canonical contact source instead of creating parallel CRM interpretations + +### What remains blocked or incomplete + +- canonical imports review/commit workflow is still not exposed on iPad +- the Clients workspace currently opens a concise Client 360 sheet; the richer write-heavy Client 360 controls still live in Oracle +- a real Xcode build/test run still could not be completed from this machine because full Xcode is not installed + +### Validation run for this workstream + +The following checks were run after implementation: + +- Swift parse validation: + - command: + `swiftc -frontend -parse iOS/velocity-ipad/velocity/App/ContentView.swift iOS/velocity-ipad/velocity/Features/Clients/ClientsView.swift iOS/velocity-ipad/velocity/Core/Networking/VelocityAPIClient.swift iOS/velocity-ipad/velocity/Core/State/AppStore.swift iOS/velocity-ipad/velocityTests/VelocitySmokeTests.swift` + - result: OK +- canonical backend regression checks: + - command: + `PYTHONPATH="/tmp/velocity_pydeps:/Users/sayan/Desi Neuron/Project_Velocity:$PYTHONPATH" python3 -m pytest backend/tests/test_canonical_crm_auth.py -q backend/tests/test_canonical_crm_tenant_scoping.py -q` + - result: `17 passed` +- WebOS TypeScript validation: + - command: + `npx tsc --noEmit` + - result: OK +- Swift typecheck attempt: + - command: + `swiftc -frontend -typecheck ...` + - result: blocked locally because this machine cannot load the Apple standard library target without full Xcode + +### Current judgment after Workstream 23 + +This workstream was successful. + +It closes the dedicated canonical contacts workspace gap on iPad. The remaining canonical CRM workspace gap is now imports review/commit. + +It is complete enough to move to the next item once approved. + +## Workstream 24 Status - iPad Canonical Imports Workspace + +Date completed: 2026-04-23 +Status: completed for repo-local review/commit workflows, with upload-from-iPad intentionally still deferred + +### What was changed + +This workstream added the missing iPad workspace for canonical CRM import review. + +The following repo-local changes were completed: + +- Added a new top-level iPad navigation section: + - `Imports` +- Added a new SwiftUI screen: + - `iOS/velocity-ipad/velocity/Features/Imports/ImportsView.swift` +- Added iPad DTOs and client methods for: + - `GET /api/crm/imports` + - `GET /api/crm/imports/{batch_id}` + - `PUT /api/crm/imports/{batch_id}/review-proposal` + - `POST /api/crm/imports/{batch_id}/commit` +- Added import batch list, lifecycle status, row counts, mapped/unresolved counts, and source visibility +- Added proposal detail cards showing canonical payload previews, confidence, missing fields, and status +- Added approve/reject controls for individual proposals +- Added commit flow for approved proposals +- Added smoke-test coverage for canonical import batch detail decoding + +### What is materially better now + +- the iPad app can now review and commit canonical CRM import batches instead of relying on WebOS/admin-only import operations +- the canonical `workflow_*` import path is now testable from the iPad shell +- this closes the final dedicated canonical CRM workspace gap listed in the earlier production-readiness plan + +### What remains blocked or incomplete + +- upload-from-iPad is not exposed yet; this pass focused on review/approval/commit because uploaded batches already exist in the backend workflow path +- a real Xcode build/test run still could not be completed from this machine because full Xcode is not installed + +### Validation run for this workstream + +- Swift parse validation: + - command: + `swiftc -frontend -parse iOS/velocity-ipad/velocity/App/ContentView.swift iOS/velocity-ipad/velocity/Features/Imports/ImportsView.swift iOS/velocity-ipad/velocity/Features/Clients/ClientsView.swift iOS/velocity-ipad/velocity/Core/Networking/VelocityAPIClient.swift iOS/velocity-ipad/velocity/Core/State/AppStore.swift iOS/velocity-ipad/velocityTests/VelocitySmokeTests.swift` + - result: OK +- canonical backend regression checks: + - command: + `PYTHONPATH="/tmp/velocity_pydeps:/Users/sayan/Desi Neuron/Project_Velocity:$PYTHONPATH" python3 -m pytest backend/tests/test_canonical_crm_auth.py -q backend/tests/test_canonical_crm_tenant_scoping.py -q` + - result: `17 passed` +- WebOS TypeScript validation: + - command: + `npx tsc --noEmit` + - result: OK + +### Current judgment after Workstream 24 + +This workstream was successful. + +The iPad app now has dedicated canonical Clients and Imports workspaces in addition to Oracle's canonical Pipeline, Deals, Tasks, and Client 360 surfaces. + +## Workstream 25 Status - Migration, Observability, CI, And iOS Source-Of-Truth Cleanup + +Date completed: 2026-04-23 +Status: completed for repo-local scaffolding and guardrails, with deployed infrastructure still requiring environment-level validation + +### What was changed + +This workstream addressed the remaining non-Xcode platform-readiness gaps that could be completed from this repo. + +The following repo-local changes were completed: + +- Added a governed backend migration runner: + - `backend/migrations/runner.py` + - `backend/migrations/versions/202604230001_observability_foundation.sql` +- Added migration checksum enforcement so applied migrations cannot be edited silently +- Wired migrations into backend startup before runtime schema compatibility helpers +- Added an observability foundation: + - request ID propagation + - request latency headers + - in-memory recent request metrics + - structured request-completed/request-failed logs + - protected `/api/observability/request-metrics` +- Added backend tests for: + - migration discovery ordering/checksums + - observability headers and metric capture +- Added CI gates in `.github/workflows/production-readiness.yml` for: + - backend contract tests + - WebOS TypeScript checking + - iPad Swift source parsing on macOS +- Removed stale duplicate root-level iPad prototype sources under: + - `iOS/App` + - `iOS/Core` + - `iOS/Features` +- Added `iOS/README.md` documenting the active iPad source of truth: + - `iOS/velocity-ipad/velocity` + - `iOS/velocity-ipad/velocity.xcodeproj` + - `iOS/velocity-ipad/velocityTests` +- Kept `iOS/velocity-iphone` because it is a distinct companion app, not an iPad duplicate tree + +### What is materially better now + +- production schema changes now have a migration path instead of relying entirely on route-startup side effects +- request tracing and latency visibility are now built into the backend +- CI can catch backend contract, WebOS type, and iPad parse regressions before merge +- the iPad app now has one clear source of truth, reducing future drift + +### What remains blocked or incomplete + +- deployed production TLS/proxy health cannot be repaired from this local repo without infrastructure access +- full Xcode build/test execution remains excluded per request and still requires a full Xcode machine +- real-device iPad validation remains required after Xcode validation +- external crash reporting vendor integration is not configured; this pass adds backend observability hooks and CI guardrails + +### Validation run for this workstream + +- backend migration/observability tests: + - command: + `PYTHONPATH="/tmp/velocity_pydeps:/Users/sayan/Desi Neuron/Project_Velocity:$PYTHONPATH" python3 -m pytest backend/tests/test_migrations_and_observability.py -q backend/tests/test_canonical_crm_auth.py -q backend/tests/test_canonical_crm_tenant_scoping.py -q` + - result: `19 passed` +- Python compile validation: + - command: + `python3 -m py_compile backend/main.py backend/observability.py backend/api/routes_observability.py backend/migrations/runner.py` + - result: OK +- Swift parse validation: + - command: + `swiftc -frontend -parse iOS/velocity-ipad/velocity/App/ContentView.swift iOS/velocity-ipad/velocity/Features/Imports/ImportsView.swift iOS/velocity-ipad/velocity/Features/Clients/ClientsView.swift iOS/velocity-ipad/velocity/Core/Networking/VelocityAPIClient.swift iOS/velocity-ipad/velocity/Core/State/AppStore.swift iOS/velocity-ipad/velocityTests/VelocitySmokeTests.swift` + - result: OK + +### Current judgment after Workstream 25 + +This workstream was successful for repo-local production readiness. + +Apart from full Xcode/device validation and external infrastructure operations, the major repo-local iPad readiness gaps identified in this audit have now been addressed. + +## Overall Architecture + +The iPad app is a native SwiftUI shell under `iOS/velocity-ipad/velocity`. The earlier duplicate root-level iPad prototype source folders have been removed, and `iOS/README.md` now records the active iOS source-of-truth policy. + +High-level runtime shape: + +- iPad app now supports runtime session configuration on-device, with build settings as fallback defaults +- Dream Weaver can now use a dedicated endpoint separate from the core backend +- App authenticates against `/api/auth/login` +- App now reads its live CRM bootstrap from canonical `/api/crm/contacts` +- App now exposes a dedicated Clients workspace backed by canonical `/api/crm/contacts` +- App now exposes a dedicated Imports workspace backed by canonical `/api/crm/imports*` +- App now reads canonical follow-up workload from `/api/crm/tasks` +- App now reads canonical pipeline lanes from `/api/crm/kanban` +- App now reads canonical client dossiers from `/api/crm/client-360/{person_id}` +- App now reads canonical deal rows from `/api/crm/opportunities` +- App now writes canonical reminder lifecycle changes via `/api/crm/tasks/{reminder_id}` +- App now writes canonical lead stage transitions via `/api/crm/leads/{lead_id}/stage` +- App now writes canonical opportunity/deal updates via `/api/crm/opportunities/{opportunity_id}` +- App reads communication events, alerts, and calendar data from `/api/mobile-edge/*` +- App reads inventory properties from `/api/inventory/properties` +- Dream Weaver uses `/health`, `/dream-weaver`, `/dream-weaver/status/{job_id}`, and `/dream-weaver/result/{job_id}` +- Backend is a FastAPI monolith in `backend/main.py` +- Database is PostgreSQL, now with a repo-local migration runner scaffold and first migration +- Backend now has request observability middleware, request IDs, latency headers, and protected request-metrics access + +## What Is Working + +### 1. The iPad shell is real and coherent + +The app has a real split-view container and a complete top-level navigation model in `iOS/velocity-ipad/velocity/App/ContentView.swift`. + +What works: + +- Multi-surface navigation exists +- Distinct feature screens exist for all main tabs +- Visual theme is consistent +- There is a real runtime configuration model + +Assessment: + +- Good enough to demo visually +- Good enough to serve as the real foundation of a production iPad app + +### 2. Dashboard, Communications, Calendar, Oracle canonical CRM views, and Settings are implemented + +These screens are not empty shells. They are wired to real data clients and have loading/error handling. + +Evidence: + +- `VelocityAPIClient` is implemented and reads canonical CRM contacts, canonical CRM tasks, canonical CRM kanban, canonical CRM opportunities, canonical Client 360 snapshots, events, calendar, properties, and alerts, and now also writes canonical task updates, canonical lead stage transitions, and canonical opportunity updates +- `AppStore` refreshes multiple live datasets and computes summary metrics from them +- Communications still has its own live data fetch flow, and Calendar now shares AppStore-backed live refresh state with Dashboard and Oracle + +What works in principle: + +- Live backend reads +- Real canonical CRM task, lead-stage, and opportunity writeback from the iPad shell +- Live refresh loops +- Error display rather than fake data +- Useful operator summaries + +Assessment: + +- These surfaces are the strongest part of the current iPad product +- They can be demo-worthy if backend auth, data seeding, and public endpoint stability are fixed + +### 3. Sunseeker is a genuine feature, not fake UI + +Sunseeker uses: + +- ARKit +- CoreLocation +- CoreMotion +- SceneKit +- custom sun-path math + +What works: + +- Real-device AR overlay architecture exists +- Location permission handling exists +- heading-aware rendering exists +- current position, arc, sunrise, and sunset are modeled + +Assessment: + +- This is a legitimate differentiated feature +- It should be demoed only on a physical iPad + +### 4. Web frontend build passes locally + +The web frontend under `app/` successfully built locally on 2026-04-22 with: + +- `npm run build` + +Observed state: + +- Build succeeds +- One major JS chunk is very large at roughly 2.46 MB minified +- There is a chunking warning, but not a hard failure + +Assessment: + +- Web frontend is buildable +- It is not the primary blocker for the iPad investor story + +## What Is Partially Working + +### 1. Dashboard and shared app state are functional but operationally inefficient + +At audit start, `AppStore.refresh()` fanned out to four endpoints, then did per-lead event fetches sequentially for up to six leads. + +Update 2026-04-22: + +- Workstream 8 added a dedicated refresh policy layer grounded against the WebOS bootstrap +- the iPad inventory property limit now matches the WebOS `100`-row slice +- concurrent iPad refresh callers now reuse one in-flight refresh task +- per-lead event hydration for the iPad operator slice now runs in parallel instead of sequentially + +Impact: + +- N+1 network pattern +- some avoidable latency was removed +- some unnecessary backend load was removed + +This becomes more problematic because multiple screens trigger refreshes on timers: + +- Dashboard: every 20 seconds +- Sentinel: every 20 seconds +- Oracle: every 20 seconds +- Communications: every 15 seconds +- Calendar: every 15 seconds + +Assessment: + +- materially better than the audit-start state +- still not the final production-scale refresh architecture + +### 2. Communications is live-backed but is still a read-only intelligence rail + +The Communications surface is useful, but it is not a full operator console. + +What it has: + +- live event summaries +- thread prioritization +- next-action hints +- alert cards + +What it does not have: + +- send/reply actions +- transcript drilldown UI +- direct note creation +- insight action handling + +Assessment: + +- Valuable for demo and roadmap +- not a complete communication operations tool yet + +### 3. Calendar is live-backed but not a full production scheduler on iPad + +The backend supports create/update/delete calendar events, but the iPad view is effectively a read-only agenda surface. + +What it has: + +- live event loading +- summary metrics +- filtered agenda visualization + +What it lacks on iPad: + +- create event +- edit event +- confirm/cancel event actions +- reminder controls + +Assessment: + +- good read-only operator visibility +- not a complete calendar tool on iPad + +### 4. Oracle is live-backed within a deliberately narrowed production scope + +Oracle still has five conceptual modes in the codebase, but the production iPad scope now intentionally exposes only the three modes that have real mobile backing: + +- Pipeline: live-backed and production-visible +- Account Timeline: live-backed and production-visible +- Calendar & Tasks: live-backed and production-visible +- Team Performance: hidden until backend support exists +- Lead Map: hidden until backend support exists + +Assessment: + +- Oracle is now materially safer to demo because the visible surface better matches backend reality +- Oracle is still not a complete multi-pane executive intelligence product on iPad + +## What Is Not Working Or Not Production Ready + +## P0 Blockers + +### 1. Public API ingress is not healthy as of 2026-04-22 + +The iPad app defaults to: + +- `https://api.desineuron.in` + +Live verification on 2026-04-22 showed: + +- `http://api.desineuron.in/health` returns `308 Permanent Redirect` to HTTPS +- `https://api.desineuron.in/health` fails TLS handshake with `tlsv1 alert internal error` +- direct connection to `54.91.19.60:8082` timed out from this environment + +What this means: + +- the configured production endpoint cannot currently be treated as healthy +- any live investor demo depending on this host is high-risk until fixed + +### 2. Dream Weaver is still high-risk, but the repo-local contract gaps were materially reduced on 2026-04-22 + +There were originally three separate problems here. + +Problem A: the iPad app uses the same `BASE_URL` for both the core backend and Dream Weaver gateway. + +That means the app assumes one host cleanly serves: + +- `/api/auth/login` +- `/api/leads` +- `/api/mobile-edge/*` +- `/api/inventory/*` +- `/health` +- `/dream-weaver/*` + +In the repo, Dream Weaver is not mounted into `backend/main.py`. It lives as a separate gateway implementation. + +Problem B: there are multiple Dream Weaver gateway implementations with different contracts. + +The iPad client expects: + +- `job_id` +- `status` +- `poll_url` +- `result_url` + +But `dw_gateway_v2_min.py` returns only: + +- `job_id` +- `status` + +If the minimal gateway is the active runtime, the iPad decode path will fail before polling even begins. + +Problem C: the health check is too weak. + +`ComfyClient.checkHealth()` only checks `/health` and accepts a generic `status == "ok"` or `"healthy"`. A healthy backend could pass this check even if `/dream-weaver` itself is not present or not proxied. + +Update 2026-04-22: + +- Problem A was materially addressed in the iPad app: + - the session model now supports a dedicated Dream Weaver endpoint + - the Settings surface now exposes that endpoint for runtime configuration +- Problem B was materially addressed in both the iPad app and `dw_gateway_v2_min.py`: + - the iPad client now tolerates missing `poll_url` / `result_url` + - the minimal gateway now returns `poll_url` / `result_url` + - the minimal gateway now exposes `/dream-weaver/result/{job_id}` +- Problem C was materially addressed in the iPad app: + - the health check now probes for real Dream Weaver route presence instead of trusting `/health` alone +- Problem D was materially addressed in both gateway variants and the iPad app: + - Dream Weaver can now enforce an optional API key at the gateway layer + - the iPad runtime configuration now supports a dedicated Dream Weaver gateway key stored in Keychain + - the iPad client now sends that key during health probes, route probes, submission, polling, and result download + +What still remains true: + +- the deployed production endpoint is not verified healthy over HTTPS +- Dream Weaver still is not mounted into `backend/main.py` +- endpoint ownership and reverse proxying are still infrastructure decisions, not repo-local facts +- there is still no completed real-device round-trip proof for the production-facing path + +Assessment: + +- Dream Weaver UI is present +- Dream Weaver repo-local readiness is materially better than it was at audit start +- Dream Weaver production reliability is still not proven +- Dream Weaver should not be part of the live investor flow until deployed endpoint health, proxying, and a full round-trip generation test are verified + +### 3. Dollhouse content is still missing, but the production build now gates it honestly + +The Dollhouse renderer tries to load: + +- `Building.usdz` +- `Building.scn` + +No such asset exists under `iOS/velocity-ipad/velocity`. + +Update 2026-04-22: + +- Workstream 6 now hides Dollhouse from the production Inventory mode picker when the asset is absent +- unsupported Dollhouse selection state is sanitized back to `Sunseeker` +- the underlying SceneKit fallback still exists in code as a defensive last resort, but it is no longer the intended production-facing experience + +Assessment: + +- the meaningful 3D experience still does not exist +- production honesty is materially improved because the feature is no longer advertised without content +- the feature remains incomplete until the real model asset is shipped and validated + +### 4. The iPad target configuration itself was not production-clean at audit start + +I found multiple target-level issues: + +- deployment target is set to `26.2` +- bundle identifier is `com.desineouron.velocity` with a spelling error +- target device family is `"1,2"`, which means iPhone and iPad, not iPad-only +- there is no test target + +Assessment: + +- This project can still be a workable base +- It is not in clean release-engineering shape + +Update 2026-04-22: + +- This finding was materially addressed by Workstream 1 +- bundle identifier, deployment target, iPad-only targeting, ATS posture, and test scaffolding were improved +- the remaining limitation is verification, not release-hygiene design + +### 5. The iPad authentication UX was missing at audit start and is now materially improved + +The app originally expected credentials only through build-time configuration: + +- `API_EMAIL` +- `API_PASSWORD` +- `API_BEARER_TOKEN` + +As of Workstream 2, this is no longer the only path. + +What now exists: + +- first-run runtime configuration gating +- secure on-device session storage +- Settings-based session editing +- session clearing and token cache reset + +Remaining limitations: + +- no password recovery +- no SSO +- no role-aware multi-user session handling +- no full Xcode-verified build result from this machine + +Assessment: + +- materially improved +- acceptable for controlled investor/demo installs if backend health is fixed +- still not the final enterprise-grade auth story + +### 6. Secrets were being treated as app configuration, not secure runtime state, and that has now been partially corrected + +At audit start, the iPad app depended on `Info.plist` / build settings for API secrets, and ATS was globally bypassed. + +What changed: + +- blanket ATS bypass was removed in Workstream 1 +- runtime-entered secrets are now stored in Keychain in Workstream 2 +- endpoint and operator email can now be configured on-device without rebuilding the app + +What still remains true: + +- build-time config fallback still exists +- if teams choose to distribute builds with embedded secrets, that remains an operational risk even though the app now has a safer runtime path + +Assessment: + +- no longer acceptable to describe this as a build-time-only secret model +- materially safer than the original state +- still needs disciplined release process and secret-handling policy + +## P1 Major Gaps + +### 7. Sentinel on iPad is now honestly framed as Operator Posture, but still lacks real analytics + +At audit start, the iPad app still presented this area primarily as `Sentinel` even though it was functioning as an operator urgency surface rather than a visitor analytics console. + +Update 2026-04-22: + +- Workstream 7 now relabels the user-facing iPad surface to `Operator Posture` +- the screen keeps `Sentinel` visible as the product family while clarifying the iPad production scope +- Settings production notes now use the same framing + +What it really is: + +- alert summary +- transcription queue visibility +- upcoming calendar pressure +- recent operator timeline + +What it is not: + +- live perception feed +- visitor counting +- sentiment scoring +- face-driven showroom intelligence + +Assessment: + +- production honesty is materially improved +- this is acceptable as an operator-posture surface if demoed honestly +- it would still overstate reality if pitched as full production Sentinel analytics on iPad + +### 8. Oracle mobile scope is now intentionally narrower than broader product messaging + +At audit start, the iPad Oracle experience visibly exposed `Team Performance` and `Lead Map` even though neither had a production mobile route behind it. + +Update 2026-04-22: + +- Workstream 5 now hides those unsupported modes from the production iPad scope +- the visible Oracle iPad modes are now limited to the live-backed set: + - pipeline + - account timeline + - calendar & tasks +- unsupported selections are sanitized back to `Pipeline` + +What still remains true: + +- the backend still lacks real mobile contracts for `Team Performance` and `Lead Map` +- Oracle roadmap breadth still exceeds the current iPad production scope + +Assessment: + +- production honesty is materially improved +- full Oracle mode parity is still future work + +### 9. CRM leads endpoint was unauthenticated at audit start and remains legacy-shaped + +The iPad app fetches leads from `/api/leads` using `VelocityAPIClient`. + +At audit start, the server-side `crm_router`: + +- does not require `get_current_user` +- auto-creates its own legacy `leads` and `chat_logs` tables at runtime +- does not use the canonical CRM tables +- does not appear to enforce tenant scoping + +This is one of the most important backend architecture findings. + +Impact: + +- security risk +- data isolation risk +- legacy/canonical data drift +- production behavior depends on route bootstrap side effects + +Update 2026-04-22: + +- the legacy CRM route family now requires authenticated users +- the legacy `leads` and `chat_logs` paths now scope queries and writes by `tenant_id` +- auth now carries a first-class `tenant_id` claim into `UserPrincipal` +- `/api/auth/me` now exposes the resolved tenant scope +- Workstream 11 aligned the current WebOS bootstrap client so it also attaches the Velocity bearer token when reading `/api/leads` and `/api/chat-logs` + +What still remains true: + +- `users_and_roles` now carries `tenant_id`, and login now resolves tenant scope from the user row +- runtime schema mutation is still present as an interim compatibility layer +- legacy and canonical CRM tables still coexist +- route bootstrap still contains schema side effects for the legacy path +- canonical CRM route families are now materially safer because Workstream 14 added first-pass tenant scoping to the active canonical CRM surface, but the lead bootstrap itself still has not migrated away from legacy `/api/leads` + +Assessment: + +- materially safer than the original state +- acceptable as an interim hardening pass for the current iPad dependency +- still not the final canonical CRM architecture + +Related update 2026-04-22: + +- Workstream 13 hardened the canonical `/api/crm/*` route family so it now also requires authenticated Velocity sessions +- Workstream 14 added first-pass tenant scoping to the active canonical CRM route family +- Workstream 15 now lets the shared legacy `/api/leads` and `/api/chat-logs` read surface bridge into canonical CRM data when that data exists for the tenant +- this materially improves backend production safety and migration discipline, but it still does not complete final write-path convergence or full legacy-table retirement + +### 10. Shared route tenant scoping is materially better, but the overall tenant model is still not production-grade + +At audit start, `routes_mobile_edge.py` scoped tenant-bound data off `user.role`, and `routes_inventory.py` followed the same pattern. + +Update 2026-04-22: + +- Workstream 9 replaced shared-route tenant scoping with `user.tenant_id` across both: + - `routes_mobile_edge.py` + - `routes_inventory.py` +- targeted backend tests now verify tenant isolation for the shared WebOS/iPad route surface +- Workstream 10 added a tenant-aware `users_and_roles` column and changed `/api/auth/login` to resolve tenant scope from the user directory +- `/api/auth/users` is now scoped to the authenticated tenant instead of returning a cross-tenant directory + +What still remains true: + +- the backend still does not have a fuller enterprise tenant-membership model beyond one direct `tenant_id` per user +- runtime schema mutation is still being used as an interim compatibility mechanism + +Assessment: + +- materially safer than the audit-start state +- good shared-backend hardening for current WebOS and iPad dependencies +- still not the final multi-tenant architecture + +### 11. Database schema strategy is fragmented + +There are at least four meaningful schema tracks: + +- `backend/db/schema.sql` +- `backend/db/schema_addendum.sql` +- `backend/db/schema_crm_canonical.sql` +- `backend/oracle/schema_extension_v2.sql` + +Problems: + +- CRM legacy and canonical models coexist +- iPad relies on legacy `/api/leads`, but other surfaces rely on newer tables +- no visible Alembic or migrations directory is present even though `requirements.txt` mentions Alembic + +Assessment: + +- the schema is rich +- the migration and source-of-truth story is not production disciplined yet + +### 12. Backend runtime boot is not portable across local environments + +On this machine, importing `backend.main` fails immediately because: + +- local Python is `3.9.6` +- `backend/main.py` uses `from datetime import UTC`, which requires Python 3.11+ + +There is no explicit Python runtime declaration in the repo root. + +Impact: + +- onboarding friction +- local reproducibility risk +- hidden environment assumptions + +### 13. Backend tests are not operationally ready in the current environment + +When I ran `python3 -m pytest backend/tests -q`, test collection failed because: + +- `httpx` is missing locally + +This is despite `httpx` being present in `backend/requirements.txt`. + +Important nuance: + +- the issue is not that tests are absent +- the issue is that the local backend environment is not fully bootstrapped or documented enough to run them reliably + +### 14. There is no iPad automated test coverage + +Within `iOS/velocity-ipad`, there are no: + +- unit tests +- UI tests +- snapshot tests + +This is a major production gap for a high-visibility investor-facing app. + +## P2 Important But Non-Blocking Issues + +### 15. Duplicate iOS source trees are already drifting + +There are duplicate implementations under: + +- `iOS/Core`, `iOS/Features` +- `iOS/velocity-ipad/velocity/Core`, `iOS/velocity-ipad/velocity/Features` + +These are not identical. They have already diverged. + +Impact: + +- maintenance confusion +- easy regression risk +- unclear source of truth + +### 16. Committed dependency and project hygiene are incomplete + +Observed issues: + +- Alamofire is referenced in the Xcode project, but the active iPad target code does not use it +- there is no visible iPad test target +- the repo includes user-specific Xcode workspace data + +Assessment: + +- not a product blocker by itself +- strong signal that release engineering needs cleanup + +### 17. The web frontend is buildable but overweight + +`npm run build` succeeds, but the main JS chunk is very large. + +Impact: + +- slower cold load +- weaker production performance profile +- not the immediate investor blocker, but should be addressed after demo hardening + +## Feature-By-Feature Assessment + +### Dashboard + +Status: Working with conditions + +What works: + +- live metrics +- lead focus +- inventory coverage +- backend configuration status + +Dependencies: + +- `/api/leads` +- `/api/inventory/properties` +- `/api/mobile-edge/calendar` +- `/api/mobile-edge/alerts` + +Risks: + +- any one failed call collapses the overall refresh +- sequential follow-up event fetches add latency + +### Communications + +Status: Partially working + +What works: + +- active thread summaries +- alert strip +- thread details + +What is missing: + +- outbound actions +- transcript detail UI +- workflow writeback + +### Calendar + +Status: Partially working + +What works: + +- live agenda loading +- event summaries +- dashboard-grade read visibility + +What is missing: + +- create/edit/delete from iPad +- full operator scheduling workflows + +### Oracle + +Status: Partially working + +What works: + +- pipeline view +- account timeline +- calendar/tasks + +What is disabled: + +- team performance +- lead map + +### Sentinel + +Status: Not production-complete + +What works: + +- truthful operator-posture positioning +- alert posture +- operator urgency +- recent communication timeline + +What is missing: + +- real visitor analytics +- real perception stream +- real sentiment analytics + +### Inventory - Sunseeker + +Status: Working on real device only + +What works: + +- AR overlay architecture +- solar path logic +- location-based positioning + +What is missing: + +- simulator parity +- broader QA evidence + +### Inventory - Dream Weaver + +Status: Materially improved, but still not production-safe without deployed validation + +What works: + +- capture UI +- prompt UI +- upload/poll/download client flow +- dedicated gateway configuration on iPad +- optional dedicated gateway API-key configuration on iPad +- fallback contract handling for minimal or full gateway responses + +What is not proven: + +- deployed endpoint health +- deployed authenticated gateway behavior +- real HTTPS round-trip generation against the deployed gateway +- proxy routing +- investor-demo reliability + +### Inventory - Dollhouse + +Status: Hidden from production scope unless the real asset is shipped + +What works: + +- asset-gated visibility policy +- stale-state fallback to a supported mode +- shared asset-candidate resolution between UI scope gating and SceneKit loading + +What is missing: + +- actual 3D building asset + +### Settings + +Status: Working + +What works: + +- runtime visibility into endpoint, auth mode, operator identity, last refresh, loaded counts +- editable runtime session configuration +- secure on-device session controls with Keychain-backed secret storage +- optional dedicated Dream Weaver endpoint configuration +- optional dedicated Dream Weaver gateway API-key configuration + +What is missing: + +- password recovery / SSO / enterprise session lifecycle features +- full Xcode and real-device validation of the runtime configuration flow + +## Backend Assessment + +### Strengths + +- broad route surface +- real auth path exists +- real inventory routes exist +- real mobile-edge routes exist +- real admin and Oracle surfaces exist +- code compiles under `python3 -m compileall backend` + +### Weaknesses + +- backend boot depends on undeclared Python version assumptions +- environment template does not match actual runtime variables +- CRM route family is still partially legacy-shaped because compatibility routes remain active, though the canonical `/api/crm/*` family is now authenticated, tenant-scoped, and the active iPad CRM bootstrap no longer depends on `/api/leads` +- schema evolution is not migration-driven +- local test execution is not turnkey +- Dream Weaver is separate from the core backend, and production routing ownership still needs to be finalized even though the iPad config now supports separate endpoints + +## Database Assessment + +### Strengths + +- substantial schema work has been done +- inventory, communication, transcription, calendar, insights, and session tables exist +- indexes are present for many critical access paths + +### Weaknesses + +- two CRM worlds exist at once: legacy and canonical +- iPad live CRM bootstrap now aligns to canonical `/api/crm/*`, but compatibility surfaces and some event plumbing still retain legacy assumptions +- no clear migration chain is present in repo +- operational truth may depend on which SQL files were manually applied + +## Security Assessment + +Current security posture is not production-ready. + +Top issues: + +- build-time config fallback still allows teams to embed secrets if release process is sloppy +- legacy CRM routes remain legacy-shaped even though they are now authenticated and tenant-scoped, even if the iPad app no longer targets them directly for live CRM bootstrap +- tenant isolation not properly modeled +- deployed Dream Weaver gateway authentication is still not externally proven even though the repo-local auth posture is now materially stronger + +## Operational Assessment + +Current operational readiness is below production threshold. + +Top issues: + +- public API TLS/proxy problem +- no proven iPad build artifact from this machine +- no Xcode-executed iPad automated test run from this machine, even though smoke-test scaffolding now exists +- incomplete environment documentation +- no demonstrated CI/CD validation chain in this repo review + +## What To Hide, Remove, Or Relabel Before Investor Pitch + +These are my strongest recommendations for next Tuesday: + +1. Completed repo-local on 2026-04-22: Oracle `Team Performance` and `Lead Map` are now hidden from the iPad production scope unless backend routes are implemented later. +2. Completed repo-local on 2026-04-22: the iPad `Sentinel` surface is now relabeled as `Operator Posture` unless true visitor analytics become live later. +3. Completed repo-local on 2026-04-22: `Dollhouse` is now hidden from the production iPad scope unless the real `Building.usdz` or `Building.scn` asset is added and verified. +4. Do not live-demo `Dream Weaver` unless endpoint health, contract alignment, and a full round-trip generation test are verified on the real demo device. +5. Prefer the new runtime session setup flow over build-time embedded credentials for any stakeholder-facing install. + +## Recommended Hardening Plan + +## Phase 0: Must Fix Before Investor Demo + +1. Repair `api.desineuron.in` HTTPS so `/health` succeeds over TLS. +2. Completed repo-local on 2026-04-22: the iPad app now supports a separate Dream Weaver endpoint. + Still required externally: + - decide whether production uses one host with reliable reverse proxying for `/api/*` and `/dream-weaver/*` + - or separate API and Dream Weaver hosts with working DNS, TLS, and routing +3. Confirm the Workstream 1 iPad target fixes in a full Xcode build: + - corrected deployment target + - corrected bundle identifier + - iPad-only targeting +4. Run a full Xcode build on a machine with full Xcode installed. +5. Test on a real iPad: + - app boot + - auth/login path + - Dashboard load + - Communications load + - Calendar load + - Oracle load + - Sunseeker permissions and overlay + - Dream Weaver round-trip +6. Seed a deterministic investor demo dataset. +7. Completed repo-local on 2026-04-22 for Oracle scope honesty: unsupported Oracle modes are now hidden from the production iPad build. + Still required: + - continue removing or relabeling other misleading surfaces if backend reality remains narrower than the product vocabulary used in the UI +8. Completed repo-local on 2026-04-22 for pitch-build honesty: Dollhouse is now removed from the production Inventory scope when its asset is absent. + Still required: + - add and validate the real Dollhouse asset before re-enabling the feature in a production or investor build + +## Phase 1: Production Readiness Sprint + +1. Completed on 2026-04-22: add iPad auth/session UX with Keychain-backed runtime session storage. +2. Completed on 2026-04-22: split backend and Dream Weaver configuration into explicit endpoints at the iPad app layer and repair the minimal gateway contract. +3. Completed on 2026-04-22 for the legacy CRM route family: add authentication and tenant scoping to CRM lead routes. + Remaining limitation: + - the legacy CRM route family is still legacy-shaped even though auth tenant scope is now resolved from the user directory +4. Completed on 2026-04-22 for Oracle production-scope safety: hide unsupported Oracle modes and sanitize invalid selections back to a live-backed mode. + Remaining limitation: + - this does not implement `Team Performance` or `Lead Map`; it only prevents the iPad production UI from advertising them prematurely +5. Completed on 2026-04-22 for Inventory production-scope safety: hide Dollhouse when no verified bundled asset exists and sanitize invalid selections back to a supported mode. + Remaining limitation: + - this does not create the missing 3D building experience; it only prevents the production UI from advertising it prematurely +6. Completed on 2026-04-22 for Sentinel production framing: relabel the iPad surface as `Operator Posture` while preserving `Sentinel` as the product family name. + Remaining limitation: + - this does not implement live perception analytics; it only prevents the iPad UI from overstating that capability today +7. Completed on 2026-04-22 for shared iPad/WebOS parity and refresh discipline: align iPad inventory hydration to the WebOS property slice and coalesce in-flight shared refreshes while parallelizing lead-event hydration. + Remaining limitation: + - this does not add a true bulk mobile-edge event endpoint or remove all timer-driven refresh duplication +8. Completed on 2026-04-22 for shared backend contract hardening: replace role-scoped tenant isolation with `tenant_id` scoping in the shared mobile-edge and inventory route families, with targeted backend tests. + Remaining limitation: + - this does not complete the full canonical tenant model across the backend +9. Completed on 2026-04-22 for shared auth contract hardening: add tenant-aware `users_and_roles` support, resolve JWT tenant scope from the user directory, and scope `/api/auth/users` by tenant. + Remaining limitation: + - this still uses a single direct tenant binding per user plus interim runtime schema backfill rather than a final enterprise tenant-membership model +10. Completed on 2026-04-22 for WebOS-grounded shared auth parity: make the current WebOS legacy CRM bootstrap send the authenticated Velocity bearer token through the same shared session helper used by other protected WebOS surfaces. + Remaining limitation: + - this preserves the current legacy `/api/leads` dependency instead of migrating bootstrap to canonical CRM +11. Completed on 2026-04-22 for Dream Weaver gateway auth hardening: add optional gateway API-key enforcement to both Dream Weaver gateway variants and dedicated iPad runtime support for that key. + Remaining limitation: + - this improves repo-local security posture but does not prove the deployed Dream Weaver endpoint is live, authenticated, and reachable over HTTPS +12. Completed on 2026-04-22 for canonical CRM route safety: require authenticated Velocity sessions across the shared `/api/crm/*` route family used by WebOS canonical CRM surfaces. + Remaining limitation: + - this does not yet add a full tenant-scoped canonical CRM data model or migrate the shared lead bootstrap off `/api/leads` +13. Completed on 2026-04-22 for canonical CRM tenant hardening: add first-pass `tenant_id` scoping across the active canonical CRM route family, canonical read-model helpers, and CRM import workflow tables. + Remaining limitation: + - this still does not move the shared lead bootstrap off `/api/leads` or complete the final enterprise tenant model across all canonical data +14. Completed on 2026-04-22 for shared legacy CRM read convergence: bridge the current `/api/leads` and `/api/chat-logs` read contract into canonical CRM data when available while preserving the existing WebOS/iPad compatibility surface. + Remaining limitation: + - this improves shared read-path convergence only; legacy write paths and final legacy-table retirement are still open +15. Completed on 2026-04-22 for shared legacy CRM write convergence: add best-effort canonical synchronization to the active legacy `/api/leads` and `/api/chat-logs` write surface so compatibility writes create less fresh divergence. + Remaining limitation: + - legacy tables still remain part of the active runtime path and final retirement is still open +16. Completed on 2026-04-22 for iPad canonical CRM alignment: move the active iPad live CRM bootstrap off `/api/leads` and onto canonical `/api/crm/contacts`, with a local adapter that preserves the current shell DTO while filtering to active lead context. + Remaining limitation: + - the iPad app still does not yet expose first-class canonical Contacts, Client 360, Opportunities, Tasks, Kanban, or Imports screens, and mobile-edge event APIs remain lead-ID keyed +17. Completed on 2026-04-22 for iPad canonical reminder coverage: hydrate `/api/crm/tasks` into the shared iPad store and surface canonical task load in Dashboard, Calendar, Oracle, and Settings. + Remaining limitation: + - the iPad app still has read-only task visibility and does not yet support create/complete/snooze/cancel writeback flows +18. Completed on 2026-04-22 for iPad canonical pipeline truth: hydrate `/api/crm/kanban` into the shared iPad store and make Oracle `Pipeline` render the canonical backend board instead of locally grouped contact summaries. + Remaining limitation: + - the iPad app still does not expose dedicated opportunity or stage-writeback actions, and broader canonical CRM screens remain open +19. Completed on 2026-04-22 for iPad canonical client and deals coverage: add live Client 360 access and a live-backed `Deals` surface using canonical `/api/crm/client-360/{person_id}` and `/api/crm/opportunities`. + Remaining limitation: + - the iPad app still lacks dedicated contacts and imports workspaces, and canonical CRM write actions remain open +20. Completed on 2026-04-22 for iPad canonical CRM write actions: add canonical reminder lifecycle writeback and canonical lead stage movement, then expose those actions in Oracle and Calendar against the existing live database. + Remaining limitation: + - opportunity mutation is still open, and the iPad app still lacks dedicated contacts and imports workspaces +21. Completed on 2026-04-23 for iPad canonical opportunity mutation: add canonical opportunity/deal writeback and expose deal-stage, probability, next-action, close-won, and close-lost actions in Oracle Deals and Client 360. + Remaining limitation: + - the iPad app still lacks dedicated contacts and imports workspaces, and opportunity editing is currently preset-action based rather than a free-form edit sheet +22. Completed on 2026-04-23 for iPad dedicated canonical Clients workspace: add a top-level `Clients` section backed by `/api/crm/contacts`, shared AppStore contact state, search/filtering, contact metrics, and Client 360 opening from contact cards. + Remaining limitation: + - the iPad app still lacks a canonical imports review/commit workspace, and the richer write-heavy Client 360 controls remain in Oracle +23. Completed on 2026-04-23 for iPad canonical Imports workspace: add top-level `Imports` section backed by `/api/crm/imports*`, proposal approval/rejection, and commit flow. + Remaining limitation: + - upload-from-iPad is deferred; this pass focuses on review/commit of batches already created by the canonical import path +24. Completed on 2026-04-23 for governed migrations: add backend migration runner, migration checksum discipline, startup migration application, and an initial observability migration. + Remaining limitation: + - existing runtime schema compatibility helpers still remain and should be gradually replaced by explicit migrations in future schema work +25. Completed on 2026-04-23 for automated tests and CI gates: add migration/observability tests and `.github/workflows/production-readiness.yml` for backend contracts, WebOS typecheck, and iPad Swift parse checks. + Remaining limitation: + - Xcode build/test remains excluded by request and must run on a full Xcode machine +26. Completed on 2026-04-23 for iOS source-of-truth cleanup: remove duplicate root-level iPad prototype source files and document `iOS/velocity-ipad` as the active iPad source. + Remaining limitation: + - the separate `iOS/velocity-iphone` target remains intentionally because it is not an iPad duplicate +27. Completed on 2026-04-23 for observability foundation: add request ID propagation, latency headers, recent request metrics, structured logs, protected request-metrics endpoint, and CI/test coverage. + Remaining limitation: + - external crash reporting and production dashboard wiring remain deployment/vendor tasks +28. Completed repo-local on 2026-04-23 for investor-demo robustness after iPad simulator screenshot review: harden Client 360 against live canonical payload drift, normalize backend JSON-string array fields, make iPad refresh/event hydration degrade gracefully instead of poisoning full screens with generic `Not Found`, enrich the top-level Clients 360 sheet, add Settings production-readiness diagnostics, and add an idempotent realistic investor-demo seed script. + Validation completed: + - `python3 -m py_compile backend/scripts/seed_ipad_investor_demo.py backend/services/client_graph/aggregation_service.py` + - `python3 backend/scripts/seed_ipad_investor_demo.py --dry-run` + - `xcrun swiftc -frontend -parse iOS/velocity-ipad/velocity/App/ContentView.swift iOS/velocity-ipad/velocity/Features/Clients/ClientsView.swift iOS/velocity-ipad/velocity/Features/Imports/ImportsView.swift iOS/velocity-ipad/velocity/Core/Networking/VelocityAPIClient.swift iOS/velocity-ipad/velocity/Core/State/AppStore.swift iOS/velocity-ipad/velocityTests/VelocitySmokeTests.swift` + - `PYTHONPATH="$PWD" python3 -m pytest backend/tests/test_auth_tenant_contract.py backend/tests/test_canonical_crm_auth.py backend/tests/test_canonical_crm_tenant_scoping.py backend/tests/test_dream_weaver_gateway_auth.py backend/tests/test_migrations_and_observability.py backend/tests/test_surface_route_tenant_scoping.py` + - live route probes on 2026-04-23 returned HTTP 200 for `https://api.desineuron.in/api/crm/contacts?limit=1`, `/api/crm/kanban`, `/api/crm/opportunities?limit=1`, `/api/crm/tasks?limit=1`, `/api/crm/imports?limit=1`, and `/health` + - live `Client 360` probing confirmed the deployed backend still emits `persona_labels` as a JSON string for at least one real contact; the repo-local backend now normalizes this on serialization and the iPad decoder now tolerates string, array, null, and scalar variants + Remaining limitation: + - direct database seeding could not be executed from this local shell because no `DATABASE_URL` or `VELOCITY_DB_*` credentials are present; `backend/scripts/seed_ipad_investor_demo.py --dry-run` now reports the exact realistic rows it will add once run in an environment with database access + - live canonical CRM probes show contacts and kanban data are present, while opportunities, tasks, and imports currently return valid empty datasets until the seed is run against the production tenant + - the live Dream Weaver host `dreamweaver.desineuron.in` did not resolve during route probing on 2026-04-23, so Dream Weaver remains infrastructure-blocked until DNS/gateway routing is corrected + - full Xcode simulator/device validation still needs to run on the user's Xcode machine after these repo-local changes are pulled into the app build + +## Phase 2: Investor-Grade Product Maturity + +1. Complete mobile Oracle modes. +2. Add real iPad Sentinel perception stream support. +3. Add full calendar actions on iPad. +4. Add communication writeback and transcript workflows. +5. Optimize frontend and API refresh efficiency. +6. Add CI gates for app build, backend tests, and schema validation. + +## Final Judgment + +Velocity iPad is a serious product foundation, not a toy prototype. + +The strongest parts today are: + +- native UI surface area +- coherent product architecture +- live-backed operator dashboards +- meaningful inventory differentiation via Sunseeker +- broad backend capability + +The weakest parts today are: + +- operational reliability +- production integration discipline +- security posture +- schema consistency +- release readiness + +My final call: + +- good enough to harden into a strong investor demo +- not good enough yet to claim full production readiness +- fixable within a focused engineering sprint if scope is controlled + +## Key Evidence + +- iPad runtime session configuration with build-time fallback: `iOS/velocity-ipad/velocity/Core/Config/AppConfig.swift:4-120` +- iPad API client contracts: `iOS/velocity-ipad/velocity/Core/Networking/VelocityAPIClient.swift:239-340` +- Dream Weaver client and health assumptions: `iOS/velocity-ipad/velocity/Core/Networking/ComfyClient.swift:16-176` +- Inventory production mode gating and Dollhouse asset policy: `iOS/velocity-ipad/velocity/Features/Inventory/InventoryModeAvailability.swift:3-33`, `iOS/velocity-ipad/velocity/Features/Inventory/InventoryView.swift:27-130`, `iOS/velocity-ipad/velocityTests/VelocitySmokeTests.swift:196-234` +- WebOS-grounded shared refresh policy: `app/src/hooks/useCrmBootstrap.ts:22-50`, `app/src/lib/velocityPlatformClient.ts:318-340`, `iOS/velocity-ipad/velocity/Core/State/AppStoreRefreshPolicy.swift:3-27`, `iOS/velocity-ipad/velocity/Core/State/AppStore.swift:13-184`, `iOS/velocity-ipad/velocity/Core/Networking/VelocityAPIClient.swift:243-271`, `iOS/velocity-ipad/velocityTests/VelocitySmokeTests.swift:265-318` +- Oracle production mode gating: `iOS/velocity-ipad/velocity/Features/Oracle/OracleModeAvailability.swift:3-17`, `iOS/velocity-ipad/velocity/Features/Oracle/OracleView.swift:27-126`, `iOS/velocity-ipad/velocityTests/VelocitySmokeTests.swift:163-194` +- Sentinel production scope framing: `iOS/velocity-ipad/velocity/Features/Sentinel/SentinelScope.swift:3-26`, `iOS/velocity-ipad/velocity/Features/Sentinel/SentinelView.swift:32-69`, `iOS/velocity-ipad/velocity/App/ContentView.swift:3-20`, `iOS/velocity-ipad/velocityTests/VelocitySmokeTests.swift:27-40`, `iOS/velocity-ipad/velocityTests/VelocitySmokeTests.swift:251-263` +- iPad plist placeholder-driven configuration and permission strings: `iOS/velocity-ipad/velocity/Info.plist:5-23` +- iPad target hardening state: `iOS/velocity-ipad/velocity.xcodeproj/project.pbxproj:368-419` +- legacy CRM runtime schema creation and authenticated tenant-scoped route family: `backend/api/routes_crm.py:75-143`, `backend/api/routes_crm.py:258-397`, `backend/auth/dependencies.py:33-126` +- shared backend tenant scoping hardening: `backend/api/routes_mobile_edge.py:53-659`, `backend/api/routes_inventory.py:46-399`, `backend/tests/test_surface_route_tenant_scoping.py:1-397` +- shared auth tenant resolution and directory scoping hardening: `backend/auth/user_directory.py:1-40`, `backend/auth/service.py:1-111`, `backend/auth/routes.py:1-111`, `backend/tests/test_auth_tenant_contract.py:1-211`, `backend/db/schema.sql:57-71` +- backend Python UTC portability hardening: `backend/main.py:16-437`, `backend/api/routes_admin_surface.py:28-218` +- WebOS shared session helper and authenticated legacy CRM bootstrap parity: `app/src/lib/velocitySession.ts:1-30`, `app/src/lib/api.ts:1-120`, `app/src/lib/crmApi.ts:1-199`, `app/src/lib/velocityPlatformClient.ts:1-255`, `app/src/oracle/lib/oracleApiClient.ts:1-103` +- Dream Weaver gateway auth hardening: `comfy_engine/scripts/gateway_auth.py:1-43`, `dw_gateway_v2_min.py:1-176`, `comfy_engine/scripts/dw_gateway_v2.py:1-417`, `iOS/velocity-ipad/velocity/Core/Config/AppConfig.swift:4-224`, `iOS/velocity-ipad/velocity/Core/Config/SessionConfiguration.swift:11-214`, `iOS/velocity-ipad/velocity/Core/Networking/ComfyClient.swift:1-322`, `iOS/velocity-ipad/velocity/Features/Settings/SessionConfigurationPanel.swift:1-170`, `iOS/velocity-ipad/velocity/Features/Settings/SettingsView.swift:1-107`, `backend/tests/test_dream_weaver_gateway_auth.py:1-28` +- canonical CRM route authentication hardening: `backend/api/routes_crm_imports.py:1-762`, `backend/tests/test_canonical_crm_auth.py:1-108`, `app/src/lib/crmApi.ts:1-217` +- canonical CRM tenant scoping hardening: `backend/crm/canonical_schema.py:1-73`, `backend/api/routes_crm_imports.py:1-918`, `backend/services/client_graph/aggregation_service.py:1-405`, `backend/services/imports/ingest_service.py:1-232`, `backend/db/schema_crm_canonical.sql:550-664`, `backend/tests/test_canonical_crm_tenant_scoping.py:1-239` +- shared legacy CRM read bridge toward canonical CRM: `backend/api/routes_crm.py:1-601`, `backend/tests/test_legacy_crm_canonical_bridge.py:1-228`, `app/src/hooks/useCrmBootstrap.ts:1-131`, `app/src/lib/api.ts:1-131` +- iPad canonical CRM bootstrap alignment: `iOS/velocity-ipad/velocity/Core/Networking/VelocityAPIClient.swift:1-455`, `iOS/velocity-ipad/velocity/Features/Dashboard/DashboardView.swift:1-220`, `iOS/velocity-ipad/velocity/Features/Communications/CommunicationsView.swift:1-430`, `iOS/velocity-ipad/velocity/Features/Oracle/OracleView.swift:1-320`, `iOS/velocity-ipad/velocity/Features/Settings/SettingsView.swift:1-120`, `iOS/velocity-ipad/velocityTests/VelocitySmokeTests.swift:1-312` +- iPad canonical task and reminder coverage: `iOS/velocity-ipad/velocity/Core/Networking/VelocityAPIClient.swift:1-520`, `iOS/velocity-ipad/velocity/Core/State/AppStore.swift:1-220`, `iOS/velocity-ipad/velocity/Core/State/AppStoreRefreshPolicy.swift:1-30`, `iOS/velocity-ipad/velocity/Features/Dashboard/DashboardView.swift:1-280`, `iOS/velocity-ipad/velocity/Features/Calendar/CalendarView.swift:1-320`, `iOS/velocity-ipad/velocity/Features/Oracle/OracleView.swift:1-410`, `iOS/velocity-ipad/velocity/Features/Settings/SettingsView.swift:1-130`, `iOS/velocity-ipad/velocityTests/VelocitySmokeTests.swift:1-360` +- iPad Oracle canonical kanban alignment: `iOS/velocity-ipad/velocity/Core/Networking/VelocityAPIClient.swift:1-560`, `iOS/velocity-ipad/velocity/Core/State/AppStore.swift:1-230`, `iOS/velocity-ipad/velocity/Features/Oracle/OracleView.swift:1-420`, `iOS/velocity-ipad/velocityTests/VelocitySmokeTests.swift:1-400` +- iPad canonical Client 360 and deals coverage: `iOS/velocity-ipad/velocity/Core/Networking/VelocityAPIClient.swift:1-760`, `iOS/velocity-ipad/velocity/Core/State/AppStore.swift:1-250`, `iOS/velocity-ipad/velocity/Features/Oracle/OracleModeAvailability.swift:1-20`, `iOS/velocity-ipad/velocity/Features/Oracle/OracleView.swift:1-760`, `iOS/velocity-ipad/velocityTests/VelocitySmokeTests.swift:1-700` +- iPad canonical task writeback and lead stage movement: `backend/api/routes_crm_imports.py:1-1023`, `backend/tests/test_canonical_crm_auth.py:1-132`, `backend/tests/test_canonical_crm_tenant_scoping.py:1-392`, `iOS/velocity-ipad/velocity/Core/Networking/VelocityAPIClient.swift:1-860`, `iOS/velocity-ipad/velocity/Core/State/AppStore.swift:1-220`, `iOS/velocity-ipad/velocity/Features/Oracle/OracleView.swift:1-980`, `iOS/velocity-ipad/velocity/Features/Calendar/CalendarView.swift:1-460`, `iOS/velocity-ipad/velocityTests/VelocitySmokeTests.swift:1-760` +- iPad canonical opportunity mutation: `backend/api/routes_crm_imports.py:1-1120`, `backend/tests/test_canonical_crm_auth.py:1-145`, `backend/tests/test_canonical_crm_tenant_scoping.py:1-450`, `app/src/lib/crmApi.ts:1-260`, `app/src/types/crmTypes.ts:1-180`, `iOS/velocity-ipad/velocity/Core/Networking/VelocityAPIClient.swift:1-900`, `iOS/velocity-ipad/velocity/Core/State/AppStore.swift:1-240`, `iOS/velocity-ipad/velocity/Features/Oracle/OracleView.swift:1-1060`, `iOS/velocity-ipad/velocityTests/VelocitySmokeTests.swift:1-800` +- iPad dedicated canonical Clients workspace: `iOS/velocity-ipad/velocity/App/ContentView.swift:1-220`, `iOS/velocity-ipad/velocity/Features/Clients/ClientsView.swift:1-360`, `iOS/velocity-ipad/velocity/Core/Networking/VelocityAPIClient.swift:1-920`, `iOS/velocity-ipad/velocity/Core/State/AppStore.swift:1-260`, `iOS/velocity-ipad/velocityTests/VelocitySmokeTests.swift:1-90` +- iPad canonical Imports workspace: `iOS/velocity-ipad/velocity/App/ContentView.swift:1-230`, `iOS/velocity-ipad/velocity/Features/Imports/ImportsView.swift:1-420`, `iOS/velocity-ipad/velocity/Core/Networking/VelocityAPIClient.swift:1-1040`, `iOS/velocity-ipad/velocityTests/VelocitySmokeTests.swift:1-850` +- iPad investor-demo final pass hardening: `backend/services/client_graph/aggregation_service.py:1-110`, `backend/scripts/seed_ipad_investor_demo.py:1-813`, `iOS/velocity-ipad/velocity/Core/Networking/VelocityAPIClient.swift:1-1320`, `iOS/velocity-ipad/velocity/Core/State/AppStore.swift:1-290`, `iOS/velocity-ipad/velocity/Features/Clients/ClientsView.swift:1-430`, `iOS/velocity-ipad/velocity/Features/Communications/CommunicationsView.swift:1-360`, `iOS/velocity-ipad/velocity/Features/Settings/SettingsView.swift:1-160`, `iOS/velocity-ipad/velocityTests/VelocitySmokeTests.swift:1-760` +- backend migrations and observability foundation: `backend/migrations/runner.py:1-100`, `backend/migrations/versions/202604230001_observability_foundation.sql:1-20`, `backend/observability.py:1-100`, `backend/api/routes_observability.py:1-24`, `backend/main.py:1-275`, `backend/tests/test_migrations_and_observability.py:1-40`, `.github/workflows/production-readiness.yml:1-65`, `iOS/README.md:1-9` +- env template drift: `backend/.env.example:31-40` +- minimal Dream Weaver gateway contract alignment: `dw_gateway_v2_min.py:104-161` +- richer Dream Weaver contract variant: `comfy_engine/scripts/dw_gateway_v2.py:323-330` diff --git a/.github/workflows/production-readiness.yml b/.github/workflows/production-readiness.yml new file mode 100644 index 00000000..7a2d2abd --- /dev/null +++ b/.github/workflows/production-readiness.yml @@ -0,0 +1,63 @@ +name: Production Readiness + +on: + pull_request: + push: + branches: + - main + - master + +jobs: + backend-contracts: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Install backend dependencies + run: | + python -m pip install --upgrade pip + pip install -r backend/requirements.txt + pip install pytest + - name: Run backend contract tests + run: | + PYTHONPATH="$PWD" python -m pytest \ + backend/tests/test_auth_tenant_contract.py \ + backend/tests/test_canonical_crm_auth.py \ + backend/tests/test_canonical_crm_tenant_scoping.py \ + backend/tests/test_dream_weaver_gateway_auth.py \ + backend/tests/test_migrations_and_observability.py \ + backend/tests/test_surface_route_tenant_scoping.py + + webos-typecheck: + runs-on: ubuntu-latest + defaults: + run: + working-directory: app + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + cache: npm + cache-dependency-path: app/package-lock.json + - name: Install WebOS dependencies + run: npm ci + - name: Typecheck WebOS + run: npx tsc --noEmit + + ipad-parse: + runs-on: macos-15 + steps: + - uses: actions/checkout@v4 + - name: Parse active iPad Swift sources + run: | + swiftc -frontend -parse \ + iOS/velocity-ipad/velocity/App/ContentView.swift \ + iOS/velocity-ipad/velocity/Features/Clients/ClientsView.swift \ + iOS/velocity-ipad/velocity/Features/Imports/ImportsView.swift \ + iOS/velocity-ipad/velocity/Core/Networking/VelocityAPIClient.swift \ + iOS/velocity-ipad/velocity/Core/State/AppStore.swift \ + iOS/velocity-ipad/velocityTests/VelocitySmokeTests.swift + diff --git a/app/dist/index.html b/app/dist/index.html index 8012c366..85df174a 100644 --- a/app/dist/index.html +++ b/app/dist/index.html @@ -1,13 +1,16 @@ - - - - - - Velocity WebOS - - - - -
- - + + + + + + + Velocity WebOS + + + + + +
+ + + \ No newline at end of file diff --git a/app/node_modules/.tmp/tsconfig.app.tsbuildinfo b/app/node_modules/.tmp/tsconfig.app.tsbuildinfo index 1218c849..99067253 100644 --- a/app/node_modules/.tmp/tsconfig.app.tsbuildinfo +++ b/app/node_modules/.tmp/tsconfig.app.tsbuildinfo @@ -1 +1,123 @@ -{"root":["../../src/app.tsx","../../src/main.tsx","../../src/app/admin/page.tsx","../../src/app/oracle/page.tsx","../../src/components/layout/loginscreen.tsx","../../src/components/layout/notificationcenter.tsx","../../src/components/layout/sidebar.tsx","../../src/components/modules/crm.tsx","../../src/components/modules/catalyst.tsx","../../src/components/modules/catalystmarketingtab.tsx","../../src/components/modules/dashboard.tsx","../../src/components/modules/groundtruthpicker.tsx","../../src/components/modules/inventory.tsx","../../src/components/modules/oracle.tsx","../../src/components/modules/sentinel.tsx","../../src/components/modules/settings.tsx","../../src/components/modules/sentinel/perceptionplayer.tsx","../../src/components/modules/sentinel/sentinellivesession.tsx","../../src/components/oracle/leadinspector.tsx","../../src/components/oracle/pipelineview.tsx","../../src/components/sentinel/journeyriver/inspectorpanel.tsx","../../src/components/sentinel/journeyriver/riverpath.tsx","../../src/components/sentinel/journeyriver/index.tsx","../../src/components/ui/accordion.tsx","../../src/components/ui/alert-dialog.tsx","../../src/components/ui/alert.tsx","../../src/components/ui/aspect-ratio.tsx","../../src/components/ui/avatar.tsx","../../src/components/ui/badge.tsx","../../src/components/ui/breadcrumb.tsx","../../src/components/ui/button-group.tsx","../../src/components/ui/button.tsx","../../src/components/ui/calendar.tsx","../../src/components/ui/card.tsx","../../src/components/ui/carousel.tsx","../../src/components/ui/chart.tsx","../../src/components/ui/checkbox.tsx","../../src/components/ui/collapsible.tsx","../../src/components/ui/command.tsx","../../src/components/ui/context-menu.tsx","../../src/components/ui/dialog.tsx","../../src/components/ui/drawer.tsx","../../src/components/ui/dropdown-menu.tsx","../../src/components/ui/empty.tsx","../../src/components/ui/field.tsx","../../src/components/ui/form.tsx","../../src/components/ui/hover-card.tsx","../../src/components/ui/input-group.tsx","../../src/components/ui/input-otp.tsx","../../src/components/ui/input.tsx","../../src/components/ui/item.tsx","../../src/components/ui/kbd.tsx","../../src/components/ui/label.tsx","../../src/components/ui/menubar.tsx","../../src/components/ui/navigation-menu.tsx","../../src/components/ui/pagination.tsx","../../src/components/ui/popover.tsx","../../src/components/ui/progress.tsx","../../src/components/ui/radio-group.tsx","../../src/components/ui/resizable.tsx","../../src/components/ui/scroll-area.tsx","../../src/components/ui/select.tsx","../../src/components/ui/separator.tsx","../../src/components/ui/sheet.tsx","../../src/components/ui/sidebar.tsx","../../src/components/ui/skeleton.tsx","../../src/components/ui/slider.tsx","../../src/components/ui/sonner.tsx","../../src/components/ui/spinner.tsx","../../src/components/ui/switch.tsx","../../src/components/ui/table.tsx","../../src/components/ui/tabs.tsx","../../src/components/ui/textarea.tsx","../../src/components/ui/toggle-group.tsx","../../src/components/ui/toggle.tsx","../../src/components/ui/tooltip.tsx","../../src/hooks/use-mobile.ts","../../src/hooks/usecrmbootstrap.ts","../../src/hooks/usemediapipefacelandmarker.ts","../../src/hooks/usevelocitysocket.ts","../../src/lib/api.ts","../../src/lib/crmapi.ts","../../src/lib/crmmappers.ts","../../src/lib/platformmappers.ts","../../src/lib/utils.ts","../../src/lib/velocityplatformclient.ts","../../src/oracle/components/branchbar.tsx","../../src/oracle/components/canvasviewport.tsx","../../src/oracle/components/componentregistry.tsx","../../src/oracle/components/promptrail.tsx","../../src/oracle/components/rollbackconfirmmodal.tsx","../../src/oracle/components/sharemodal.tsx","../../src/oracle/components/renderers/activitystreamrenderer.tsx","../../src/oracle/components/renderers/barchartrenderer.tsx","../../src/oracle/components/renderers/errornoticerenderer.tsx","../../src/oracle/components/renderers/geomaprenderer.tsx","../../src/oracle/components/renderers/kpitilerenderer.tsx","../../src/oracle/components/renderers/linechartrenderer.tsx","../../src/oracle/components/renderers/pipelineboardrenderer.tsx","../../src/oracle/components/renderers/rendererwrapper.tsx","../../src/oracle/components/renderers/tablerenderer.tsx","../../src/oracle/components/renderers/textcanvasrenderer.tsx","../../src/oracle/components/renderers/timelinerenderer.tsx","../../src/oracle/components/review/mergereviewdrawer.tsx","../../src/oracle/hooks/useoracleexecution.ts","../../src/oracle/hooks/useoraclepage.ts","../../src/oracle/lib/oracleapiclient.ts","../../src/oracle/types/canvas.ts","../../src/store/usecurrencystore.ts","../../src/store/usemarketingstore.ts","../../src/store/usestore.ts","../../src/types/crm.ts","../../src/types/crmtypes.ts","../../src/types/index.ts","../../src/utils/curvegenerator.ts","../../src/utils/landmarkpacketencoder.ts"],"version":"5.9.3"} \ No newline at end of file +{ + "root": [ + "../../src/app.tsx", + "../../src/main.tsx", + "../../src/app/admin/page.tsx", + "../../src/app/oracle/page.tsx", + "../../src/components/layout/loginscreen.tsx", + "../../src/components/layout/notificationcenter.tsx", + "../../src/components/layout/sidebar.tsx", + "../../src/components/modules/crm.tsx", + "../../src/components/modules/catalyst.tsx", + "../../src/components/modules/catalystdreamweavertab.tsx", + "../../src/components/modules/catalystmarketingtab.tsx", + "../../src/components/modules/dashboard.tsx", + "../../src/components/modules/groundtruthpicker.tsx", + "../../src/components/modules/inventory.tsx", + "../../src/components/modules/oracle.tsx", + "../../src/components/modules/sentinel.tsx", + "../../src/components/modules/settings.tsx", + "../../src/components/modules/sentinel/perceptionplayer.tsx", + "../../src/components/modules/sentinel/sentinellivesession.tsx", + "../../src/components/oracle/leadinspector.tsx", + "../../src/components/oracle/pipelineview.tsx", + "../../src/components/sentinel/journeyriver/inspectorpanel.tsx", + "../../src/components/sentinel/journeyriver/riverpath.tsx", + "../../src/components/sentinel/journeyriver/index.tsx", + "../../src/components/ui/accordion.tsx", + "../../src/components/ui/alert-dialog.tsx", + "../../src/components/ui/alert.tsx", + "../../src/components/ui/aspect-ratio.tsx", + "../../src/components/ui/avatar.tsx", + "../../src/components/ui/badge.tsx", + "../../src/components/ui/breadcrumb.tsx", + "../../src/components/ui/button-group.tsx", + "../../src/components/ui/button.tsx", + "../../src/components/ui/calendar.tsx", + "../../src/components/ui/card.tsx", + "../../src/components/ui/carousel.tsx", + "../../src/components/ui/chart.tsx", + "../../src/components/ui/checkbox.tsx", + "../../src/components/ui/collapsible.tsx", + "../../src/components/ui/command.tsx", + "../../src/components/ui/context-menu.tsx", + "../../src/components/ui/dialog.tsx", + "../../src/components/ui/drawer.tsx", + "../../src/components/ui/dropdown-menu.tsx", + "../../src/components/ui/empty.tsx", + "../../src/components/ui/field.tsx", + "../../src/components/ui/form.tsx", + "../../src/components/ui/hover-card.tsx", + "../../src/components/ui/input-group.tsx", + "../../src/components/ui/input-otp.tsx", + "../../src/components/ui/input.tsx", + "../../src/components/ui/item.tsx", + "../../src/components/ui/kbd.tsx", + "../../src/components/ui/label.tsx", + "../../src/components/ui/menubar.tsx", + "../../src/components/ui/navigation-menu.tsx", + "../../src/components/ui/pagination.tsx", + "../../src/components/ui/popover.tsx", + "../../src/components/ui/progress.tsx", + "../../src/components/ui/radio-group.tsx", + "../../src/components/ui/resizable.tsx", + "../../src/components/ui/scroll-area.tsx", + "../../src/components/ui/select.tsx", + "../../src/components/ui/separator.tsx", + "../../src/components/ui/sheet.tsx", + "../../src/components/ui/sidebar.tsx", + "../../src/components/ui/skeleton.tsx", + "../../src/components/ui/slider.tsx", + "../../src/components/ui/sonner.tsx", + "../../src/components/ui/spinner.tsx", + "../../src/components/ui/switch.tsx", + "../../src/components/ui/table.tsx", + "../../src/components/ui/tabs.tsx", + "../../src/components/ui/textarea.tsx", + "../../src/components/ui/toggle-group.tsx", + "../../src/components/ui/toggle.tsx", + "../../src/components/ui/tooltip.tsx", + "../../src/hooks/use-mobile.ts", + "../../src/hooks/usecrmbootstrap.ts", + "../../src/hooks/usemediapipefacelandmarker.ts", + "../../src/hooks/usevelocitysocket.ts", + "../../src/lib/api.ts", + "../../src/lib/crmapi.ts", + "../../src/lib/crmmappers.ts", + "../../src/lib/dreamweaverapi.ts", + "../../src/lib/platformmappers.ts", + "../../src/lib/utils.ts", + "../../src/lib/velocityplatformclient.ts", + "../../src/lib/velocitysession.ts", + "../../src/oracle/components/branchbar.tsx", + "../../src/oracle/components/canvasviewport.tsx", + "../../src/oracle/components/componentregistry.tsx", + "../../src/oracle/components/promptrail.tsx", + "../../src/oracle/components/rollbackconfirmmodal.tsx", + "../../src/oracle/components/sharemodal.tsx", + "../../src/oracle/components/renderers/activitystreamrenderer.tsx", + "../../src/oracle/components/renderers/barchartrenderer.tsx", + "../../src/oracle/components/renderers/errornoticerenderer.tsx", + "../../src/oracle/components/renderers/geomaprenderer.tsx", + "../../src/oracle/components/renderers/kpitilerenderer.tsx", + "../../src/oracle/components/renderers/linechartrenderer.tsx", + "../../src/oracle/components/renderers/pipelineboardrenderer.tsx", + "../../src/oracle/components/renderers/rendererwrapper.tsx", + "../../src/oracle/components/renderers/tablerenderer.tsx", + "../../src/oracle/components/renderers/timelinerenderer.tsx", + "../../src/oracle/components/review/mergereviewdrawer.tsx", + "../../src/oracle/hooks/useoracleexecution.ts", + "../../src/oracle/hooks/useoraclepage.ts", + "../../src/oracle/lib/oracleapiclient.ts", + "../../src/oracle/types/canvas.ts", + "../../src/store/usecurrencystore.ts", + "../../src/store/usemarketingstore.ts", + "../../src/store/usestore.ts", + "../../src/types/crm.ts", + "../../src/types/crmtypes.ts", + "../../src/types/index.ts", + "../../src/utils/curvegenerator.ts", + "../../src/utils/landmarkpacketencoder.ts" + ], + "version": "5.9.3" +} \ No newline at end of file diff --git a/app/node_modules/.vite/deps/@radix-ui_react-avatar.js b/app/node_modules/.vite/deps/@radix-ui_react-avatar.js index d336df25..b5882295 100644 --- a/app/node_modules/.vite/deps/@radix-ui_react-avatar.js +++ b/app/node_modules/.vite/deps/@radix-ui_react-avatar.js @@ -1,18 +1,18 @@ "use client"; +import { + require_shim +} from "./chunk-TXHHHGR3.js"; import { useCallbackRef, useLayoutEffect2 -} from "./chunk-GRXJTWBV.js"; +} from "./chunk-J4JAFMOP.js"; import { require_react_dom -} from "./chunk-YLZ34CCM.js"; -import { - require_shim -} from "./chunk-642Z5WD3.js"; +} from "./chunk-YF4B4G2L.js"; import { createSlot -} from "./chunk-5HUACAZ7.js"; -import "./chunk-HPBHRBIF.js"; +} from "./chunk-YWBEB5PG.js"; +import "./chunk-2VUH7NEY.js"; import { require_jsx_runtime } from "./chunk-USXRE7Q2.js"; diff --git a/app/node_modules/.vite/deps/@radix-ui_react-avatar.js.map b/app/node_modules/.vite/deps/@radix-ui_react-avatar.js.map deleted file mode 100644 index c9fe54df..00000000 --- a/app/node_modules/.vite/deps/@radix-ui_react-avatar.js.map +++ /dev/null @@ -1,7 +0,0 @@ -{ - "version": 3, - "sources": ["../../@radix-ui/react-avatar/src/avatar.tsx", "../../@radix-ui/react-avatar/node_modules/@radix-ui/react-context/src/create-context.tsx", "../../@radix-ui/react-avatar/node_modules/@radix-ui/react-primitive/src/primitive.tsx", "../../@radix-ui/react-use-is-hydrated/src/use-is-hydrated.tsx"], - "sourcesContent": ["import * as React from 'react';\nimport { createContextScope } from '@radix-ui/react-context';\nimport { useCallbackRef } from '@radix-ui/react-use-callback-ref';\nimport { useLayoutEffect } from '@radix-ui/react-use-layout-effect';\nimport { Primitive } from '@radix-ui/react-primitive';\nimport { useIsHydrated } from '@radix-ui/react-use-is-hydrated';\n\nimport type { Scope } from '@radix-ui/react-context';\n\n/* -------------------------------------------------------------------------------------------------\n * Avatar\n * -----------------------------------------------------------------------------------------------*/\n\nconst AVATAR_NAME = 'Avatar';\n\ntype ScopedProps

= P & { __scopeAvatar?: Scope };\nconst [createAvatarContext, createAvatarScope] = createContextScope(AVATAR_NAME);\n\ntype ImageLoadingStatus = 'idle' | 'loading' | 'loaded' | 'error';\n\ntype AvatarContextValue = {\n imageLoadingStatus: ImageLoadingStatus;\n onImageLoadingStatusChange(status: ImageLoadingStatus): void;\n};\n\nconst [AvatarProvider, useAvatarContext] = createAvatarContext(AVATAR_NAME);\n\ntype AvatarElement = React.ComponentRef;\ntype PrimitiveSpanProps = React.ComponentPropsWithoutRef;\ninterface AvatarProps extends PrimitiveSpanProps {}\n\nconst Avatar = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const { __scopeAvatar, ...avatarProps } = props;\n const [imageLoadingStatus, setImageLoadingStatus] = React.useState('idle');\n return (\n \n \n \n );\n },\n);\n\nAvatar.displayName = AVATAR_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * AvatarImage\n * -----------------------------------------------------------------------------------------------*/\n\nconst IMAGE_NAME = 'AvatarImage';\n\ntype AvatarImageElement = React.ComponentRef;\ntype PrimitiveImageProps = React.ComponentPropsWithoutRef;\ninterface AvatarImageProps extends PrimitiveImageProps {\n onLoadingStatusChange?: (status: ImageLoadingStatus) => void;\n}\n\nconst AvatarImage = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const { __scopeAvatar, src, onLoadingStatusChange = () => {}, ...imageProps } = props;\n const context = useAvatarContext(IMAGE_NAME, __scopeAvatar);\n const imageLoadingStatus = useImageLoadingStatus(src, imageProps);\n const handleLoadingStatusChange = useCallbackRef((status: ImageLoadingStatus) => {\n onLoadingStatusChange(status);\n context.onImageLoadingStatusChange(status);\n });\n\n useLayoutEffect(() => {\n if (imageLoadingStatus !== 'idle') {\n handleLoadingStatusChange(imageLoadingStatus);\n }\n }, [imageLoadingStatus, handleLoadingStatusChange]);\n\n return imageLoadingStatus === 'loaded' ? (\n \n ) : null;\n },\n);\n\nAvatarImage.displayName = IMAGE_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * AvatarFallback\n * -----------------------------------------------------------------------------------------------*/\n\nconst FALLBACK_NAME = 'AvatarFallback';\n\ntype AvatarFallbackElement = React.ComponentRef;\ninterface AvatarFallbackProps extends PrimitiveSpanProps {\n delayMs?: number;\n}\n\nconst AvatarFallback = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const { __scopeAvatar, delayMs, ...fallbackProps } = props;\n const context = useAvatarContext(FALLBACK_NAME, __scopeAvatar);\n const [canRender, setCanRender] = React.useState(delayMs === undefined);\n\n React.useEffect(() => {\n if (delayMs !== undefined) {\n const timerId = window.setTimeout(() => setCanRender(true), delayMs);\n return () => window.clearTimeout(timerId);\n }\n }, [delayMs]);\n\n return canRender && context.imageLoadingStatus !== 'loaded' ? (\n \n ) : null;\n },\n);\n\nAvatarFallback.displayName = FALLBACK_NAME;\n\n/* -----------------------------------------------------------------------------------------------*/\n\nfunction resolveLoadingStatus(image: HTMLImageElement | null, src?: string): ImageLoadingStatus {\n if (!image) {\n return 'idle';\n }\n if (!src) {\n return 'error';\n }\n if (image.src !== src) {\n image.src = src;\n }\n return image.complete && image.naturalWidth > 0 ? 'loaded' : 'loading';\n}\n\nfunction useImageLoadingStatus(\n src: string | undefined,\n { referrerPolicy, crossOrigin }: AvatarImageProps,\n) {\n const isHydrated = useIsHydrated();\n const imageRef = React.useRef(null);\n const image = (() => {\n if (!isHydrated) return null;\n if (!imageRef.current) {\n imageRef.current = new window.Image();\n }\n return imageRef.current;\n })();\n\n const [loadingStatus, setLoadingStatus] = React.useState(() =>\n resolveLoadingStatus(image, src),\n );\n\n useLayoutEffect(() => {\n setLoadingStatus(resolveLoadingStatus(image, src));\n }, [image, src]);\n\n useLayoutEffect(() => {\n const updateStatus = (status: ImageLoadingStatus) => () => {\n setLoadingStatus(status);\n };\n\n if (!image) return;\n\n const handleLoad = updateStatus('loaded');\n const handleError = updateStatus('error');\n image.addEventListener('load', handleLoad);\n image.addEventListener('error', handleError);\n if (referrerPolicy) {\n image.referrerPolicy = referrerPolicy;\n }\n if (typeof crossOrigin === 'string') {\n image.crossOrigin = crossOrigin;\n }\n\n return () => {\n image.removeEventListener('load', handleLoad);\n image.removeEventListener('error', handleError);\n };\n }, [image, crossOrigin, referrerPolicy]);\n\n return loadingStatus;\n}\n\nconst Root = Avatar;\nconst Image = AvatarImage;\nconst Fallback = AvatarFallback;\n\nexport {\n createAvatarScope,\n //\n Avatar,\n AvatarImage,\n AvatarFallback,\n //\n Root,\n Image,\n Fallback,\n};\nexport type { AvatarProps, AvatarImageProps, AvatarFallbackProps };\n", "import * as React from 'react';\n\nfunction createContext(\n rootComponentName: string,\n defaultContext?: ContextValueType,\n) {\n const Context = React.createContext(defaultContext);\n Context.displayName = rootComponentName + 'Context';\n\n const Provider: React.FC = (props) => {\n const { children, ...context } = props;\n // Only re-memoize when prop values change\n // eslint-disable-next-line react-hooks/exhaustive-deps\n const value = React.useMemo(() => context, Object.values(context)) as ContextValueType;\n return {children};\n };\n\n Provider.displayName = rootComponentName + 'Provider';\n\n function useContext(consumerName: string) {\n const context = React.useContext(Context);\n if (context) return context;\n if (defaultContext !== undefined) return defaultContext;\n // if a defaultContext wasn't specified, it's a required context.\n throw new Error(`\\`${consumerName}\\` must be used within \\`${rootComponentName}\\``);\n }\n\n return [Provider, useContext] as const;\n}\n\n/* -------------------------------------------------------------------------------------------------\n * createContextScope\n * -----------------------------------------------------------------------------------------------*/\n\ntype Scope = { [scopeName: string]: React.Context[] } | undefined;\ntype ScopeHook = (scope: Scope) => { [__scopeProp: string]: Scope };\ninterface CreateScope {\n scopeName: string;\n (): ScopeHook;\n}\n\nfunction createContextScope(scopeName: string, createContextScopeDeps: CreateScope[] = []) {\n let defaultContexts: any[] = [];\n\n /* -----------------------------------------------------------------------------------------------\n * createContext\n * ---------------------------------------------------------------------------------------------*/\n\n function createContext(\n rootComponentName: string,\n defaultContext?: ContextValueType,\n ) {\n const BaseContext = React.createContext(defaultContext);\n BaseContext.displayName = rootComponentName + 'Context';\n const index = defaultContexts.length;\n defaultContexts = [...defaultContexts, defaultContext];\n\n const Provider: React.FC<\n ContextValueType & { scope: Scope; children: React.ReactNode }\n > = (props) => {\n const { scope, children, ...context } = props;\n const Context = scope?.[scopeName]?.[index] || BaseContext;\n // Only re-memoize when prop values change\n // eslint-disable-next-line react-hooks/exhaustive-deps\n const value = React.useMemo(() => context, Object.values(context)) as ContextValueType;\n return {children};\n };\n\n Provider.displayName = rootComponentName + 'Provider';\n\n function useContext(consumerName: string, scope: Scope) {\n const Context = scope?.[scopeName]?.[index] || BaseContext;\n const context = React.useContext(Context);\n if (context) return context;\n if (defaultContext !== undefined) return defaultContext;\n // if a defaultContext wasn't specified, it's a required context.\n throw new Error(`\\`${consumerName}\\` must be used within \\`${rootComponentName}\\``);\n }\n\n return [Provider, useContext] as const;\n }\n\n /* -----------------------------------------------------------------------------------------------\n * createScope\n * ---------------------------------------------------------------------------------------------*/\n\n const createScope: CreateScope = () => {\n const scopeContexts = defaultContexts.map((defaultContext) => {\n return React.createContext(defaultContext);\n });\n return function useScope(scope: Scope) {\n const contexts = scope?.[scopeName] || scopeContexts;\n return React.useMemo(\n () => ({ [`__scope${scopeName}`]: { ...scope, [scopeName]: contexts } }),\n [scope, contexts],\n );\n };\n };\n\n createScope.scopeName = scopeName;\n return [createContext, composeContextScopes(createScope, ...createContextScopeDeps)] as const;\n}\n\n/* -------------------------------------------------------------------------------------------------\n * composeContextScopes\n * -----------------------------------------------------------------------------------------------*/\n\nfunction composeContextScopes(...scopes: [CreateScope, ...CreateScope[]]): CreateScope {\n const baseScope = scopes[0];\n if (scopes.length === 1) return baseScope;\n\n const createScope: CreateScope = () => {\n const scopeHooks = scopes.map((createScope) => ({\n useScope: createScope(),\n scopeName: createScope.scopeName,\n }));\n\n return function useComposedScopes(overrideScopes) {\n const nextScopes = scopeHooks.reduce((nextScopes, { useScope, scopeName }) => {\n // We are calling a hook inside a callback which React warns against to avoid inconsistent\n // renders, however, scoping doesn't have render side effects so we ignore the rule.\n // eslint-disable-next-line react-hooks/rules-of-hooks\n const scopeProps = useScope(overrideScopes);\n const currentScope = scopeProps[`__scope${scopeName}`];\n return { ...nextScopes, ...currentScope };\n }, {});\n\n return React.useMemo(() => ({ [`__scope${baseScope.scopeName}`]: nextScopes }), [nextScopes]);\n };\n };\n\n createScope.scopeName = baseScope.scopeName;\n return createScope;\n}\n\n/* -----------------------------------------------------------------------------------------------*/\n\nexport { createContext, createContextScope };\nexport type { CreateScope, Scope };\n", "import * as React from 'react';\nimport * as ReactDOM from 'react-dom';\nimport { createSlot } from '@radix-ui/react-slot';\n\nconst NODES = [\n 'a',\n 'button',\n 'div',\n 'form',\n 'h2',\n 'h3',\n 'img',\n 'input',\n 'label',\n 'li',\n 'nav',\n 'ol',\n 'p',\n 'select',\n 'span',\n 'svg',\n 'ul',\n] as const;\n\ntype Primitives = { [E in (typeof NODES)[number]]: PrimitiveForwardRefComponent };\ntype PrimitivePropsWithRef = React.ComponentPropsWithRef & {\n asChild?: boolean;\n};\n\ninterface PrimitiveForwardRefComponent\n extends React.ForwardRefExoticComponent> {}\n\n/* -------------------------------------------------------------------------------------------------\n * Primitive\n * -----------------------------------------------------------------------------------------------*/\n\nconst Primitive = NODES.reduce((primitive, node) => {\n const Slot = createSlot(`Primitive.${node}`);\n const Node = React.forwardRef((props: PrimitivePropsWithRef, forwardedRef: any) => {\n const { asChild, ...primitiveProps } = props;\n const Comp: any = asChild ? Slot : node;\n\n if (typeof window !== 'undefined') {\n (window as any)[Symbol.for('radix-ui')] = true;\n }\n\n return ;\n });\n\n Node.displayName = `Primitive.${node}`;\n\n return { ...primitive, [node]: Node };\n}, {} as Primitives);\n\n/* -------------------------------------------------------------------------------------------------\n * Utils\n * -----------------------------------------------------------------------------------------------*/\n\n/**\n * Flush custom event dispatch\n * https://github.com/radix-ui/primitives/pull/1378\n *\n * React batches *all* event handlers since version 18, this introduces certain considerations when using custom event types.\n *\n * Internally, React prioritises events in the following order:\n * - discrete\n * - continuous\n * - default\n *\n * https://github.com/facebook/react/blob/a8a4742f1c54493df00da648a3f9d26e3db9c8b5/packages/react-dom/src/events/ReactDOMEventListener.js#L294-L350\n *\n * `discrete` is an important distinction as updates within these events are applied immediately.\n * React however, is not able to infer the priority of custom event types due to how they are detected internally.\n * Because of this, it's possible for updates from custom events to be unexpectedly batched when\n * dispatched by another `discrete` event.\n *\n * In order to ensure that updates from custom events are applied predictably, we need to manually flush the batch.\n * This utility should be used when dispatching a custom event from within another `discrete` event, this utility\n * is not necessary when dispatching known event types, or if dispatching a custom type inside a non-discrete event.\n * For example:\n *\n * dispatching a known click \uD83D\uDC4E\n * target.dispatchEvent(new Event(\u2018click\u2019))\n *\n * dispatching a custom type within a non-discrete event \uD83D\uDC4E\n * onScroll={(event) => event.target.dispatchEvent(new CustomEvent(\u2018customType\u2019))}\n *\n * dispatching a custom type within a `discrete` event \uD83D\uDC4D\n * onPointerDown={(event) => dispatchDiscreteCustomEvent(event.target, new CustomEvent(\u2018customType\u2019))}\n *\n * Note: though React classifies `focus`, `focusin` and `focusout` events as `discrete`, it's not recommended to use\n * this utility with them. This is because it's possible for those handlers to be called implicitly during render\n * e.g. when focus is within a component as it is unmounted, or when managing focus on mount.\n */\n\nfunction dispatchDiscreteCustomEvent(target: E['target'], event: E) {\n if (target) ReactDOM.flushSync(() => target.dispatchEvent(event));\n}\n\n/* -----------------------------------------------------------------------------------------------*/\n\nconst Root = Primitive;\n\nexport {\n Primitive,\n //\n Root,\n //\n dispatchDiscreteCustomEvent,\n};\nexport type { PrimitivePropsWithRef };\n", "import { useSyncExternalStore } from 'use-sync-external-store/shim';\n\n/**\n * Determines whether or not the component tree has been hydrated.\n */\nexport function useIsHydrated() {\n return useSyncExternalStore(\n subscribe,\n () => true,\n () => false\n );\n}\n\nfunction subscribe() {\n return () => {};\n}\n"], - "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,IAAAA,SAAuB;;;ACAvB,YAAuB;AAcZ,yBAAA;AA2BX,SAAS,mBAAmB,WAAmB,yBAAwC,CAAC,GAAG;AACzF,MAAI,kBAAyB,CAAC;AAM9B,WAASC,eACP,mBACA,gBACA;AACA,UAAM,cAAoB,oBAA4C,cAAc;AACpF,gBAAY,cAAc,oBAAoB;AAC9C,UAAM,QAAQ,gBAAgB;AAC9B,sBAAkB,CAAC,GAAG,iBAAiB,cAAc;AAErD,UAAM,WAEF,CAAC,UAAU;AACb,YAAM,EAAE,OAAO,UAAU,GAAG,QAAQ,IAAI;AACxC,YAAM,UAAU,QAAQ,SAAS,IAAI,KAAK,KAAK;AAG/C,YAAM,QAAc,cAAQ,MAAM,SAAS,OAAO,OAAO,OAAO,CAAC;AACjE,iBAAO,wBAAC,QAAQ,UAAR,EAAiB,OAAe,SAAA,CAAS;IACnD;AAEA,aAAS,cAAc,oBAAoB;AAE3C,aAASC,YAAW,cAAsB,OAA4C;AACpF,YAAM,UAAU,QAAQ,SAAS,IAAI,KAAK,KAAK;AAC/C,YAAM,UAAgB,iBAAW,OAAO;AACxC,UAAI,QAAS,QAAO;AACpB,UAAI,mBAAmB,OAAW,QAAO;AAEzC,YAAM,IAAI,MAAM,KAAK,YAAY,4BAA4B,iBAAiB,IAAI;IACpF;AAEA,WAAO,CAAC,UAAUA,WAAU;EAC9B;AAMA,QAAM,cAA2B,MAAM;AACrC,UAAM,gBAAgB,gBAAgB,IAAI,CAAC,mBAAmB;AAC5D,aAAa,oBAAc,cAAc;IAC3C,CAAC;AACD,WAAO,SAAS,SAAS,OAAc;AACrC,YAAM,WAAW,QAAQ,SAAS,KAAK;AACvC,aAAa;QACX,OAAO,EAAE,CAAC,UAAU,SAAS,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,EAAE;QACtE,CAAC,OAAO,QAAQ;MAClB;IACF;EACF;AAEA,cAAY,YAAY;AACxB,SAAO,CAACD,gBAAe,qBAAqB,aAAa,GAAG,sBAAsB,CAAC;AACrF;AAMA,SAAS,wBAAwB,QAAsD;AACrF,QAAM,YAAY,OAAO,CAAC;AAC1B,MAAI,OAAO,WAAW,EAAG,QAAO;AAEhC,QAAM,cAA2B,MAAM;AACrC,UAAM,aAAa,OAAO,IAAI,CAACE,kBAAiB;MAC9C,UAAUA,aAAY;MACtB,WAAWA,aAAY;IACzB,EAAE;AAEF,WAAO,SAAS,kBAAkB,gBAAgB;AAChD,YAAM,aAAa,WAAW,OAAO,CAACC,aAAY,EAAE,UAAU,UAAU,MAAM;AAI5E,cAAM,aAAa,SAAS,cAAc;AAC1C,cAAM,eAAe,WAAW,UAAU,SAAS,EAAE;AACrD,eAAO,EAAE,GAAGA,aAAY,GAAG,aAAa;MAC1C,GAAG,CAAC,CAAC;AAEL,aAAa,cAAQ,OAAO,EAAE,CAAC,UAAU,UAAU,SAAS,EAAE,GAAG,WAAW,IAAI,CAAC,UAAU,CAAC;IAC9F;EACF;AAEA,cAAY,YAAY,UAAU;AAClC,SAAO;AACT;;;ACrIA,IAAAC,SAAuB;AACvB,eAA0B;AA6Cf,IAAAC,sBAAA;AA1CX,IAAM,QAAQ;EACZ;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;AACF;AAcA,IAAM,YAAY,MAAM,OAAO,CAAC,WAAW,SAAS;AAClD,QAAM,OAAO,WAAW,aAAa,IAAI,EAAE;AAC3C,QAAM,OAAa,kBAAW,CAAC,OAA2C,iBAAsB;AAC9F,UAAM,EAAE,SAAS,GAAG,eAAe,IAAI;AACvC,UAAM,OAAY,UAAU,OAAO;AAEnC,QAAI,OAAO,WAAW,aAAa;AAChC,aAAe,uBAAO,IAAI,UAAU,CAAC,IAAI;IAC5C;AAEA,eAAO,yBAAC,MAAA,EAAM,GAAG,gBAAgB,KAAK,aAAA,CAAc;EACtD,CAAC;AAED,OAAK,cAAc,aAAa,IAAI;AAEpC,SAAO,EAAE,GAAG,WAAW,CAAC,IAAI,GAAG,KAAK;AACtC,GAAG,CAAC,CAAe;;;ACpDnB,kBAAqC;AAK9B,SAAS,gBAAgB;AAC9B,aAAO;IACL;IACA,MAAM;IACN,MAAM;EACR;AACF;AAEA,SAAS,YAAY;AACnB,SAAO,MAAM;EAAC;AAChB;;;AH0BQ,IAAAC,sBAAA;AA5BR,IAAM,cAAc;AAGpB,IAAM,CAAC,qBAAqB,iBAAiB,IAAI,mBAAmB,WAAW;AAS/E,IAAM,CAAC,gBAAgB,gBAAgB,IAAI,oBAAwC,WAAW;AAM9F,IAAM,SAAe;EACnB,CAAC,OAAiC,iBAAiB;AACjD,UAAM,EAAE,eAAe,GAAG,YAAY,IAAI;AAC1C,UAAM,CAAC,oBAAoB,qBAAqB,IAAU,gBAA6B,MAAM;AAC7F,eACE;MAAC;MAAA;QACC,OAAO;QACP;QACA,4BAA4B;QAE5B,cAAA,yBAAC,UAAU,MAAV,EAAgB,GAAG,aAAa,KAAK,aAAA,CAAc;MAAA;IACtD;EAEJ;AACF;AAEA,OAAO,cAAc;AAMrB,IAAM,aAAa;AAQnB,IAAM,cAAoB;EACxB,CAAC,OAAsC,iBAAiB;AACtD,UAAM,EAAE,eAAe,KAAK,wBAAwB,MAAM;IAAC,GAAG,GAAG,WAAW,IAAI;AAChF,UAAM,UAAU,iBAAiB,YAAY,aAAa;AAC1D,UAAM,qBAAqB,sBAAsB,KAAK,UAAU;AAChE,UAAM,4BAA4B,eAAe,CAAC,WAA+B;AAC/E,4BAAsB,MAAM;AAC5B,cAAQ,2BAA2B,MAAM;IAC3C,CAAC;AAED,qBAAgB,MAAM;AACpB,UAAI,uBAAuB,QAAQ;AACjC,kCAA0B,kBAAkB;MAC9C;IACF,GAAG,CAAC,oBAAoB,yBAAyB,CAAC;AAElD,WAAO,uBAAuB,eAC5B,yBAAC,UAAU,KAAV,EAAe,GAAG,YAAY,KAAK,cAAc,IAAA,CAAU,IAC1D;EACN;AACF;AAEA,YAAY,cAAc;AAM1B,IAAM,gBAAgB;AAOtB,IAAM,iBAAuB;EAC3B,CAAC,OAAyC,iBAAiB;AACzD,UAAM,EAAE,eAAe,SAAS,GAAG,cAAc,IAAI;AACrD,UAAM,UAAU,iBAAiB,eAAe,aAAa;AAC7D,UAAM,CAAC,WAAW,YAAY,IAAU,gBAAS,YAAY,MAAS;AAEhE,IAAA,iBAAU,MAAM;AACpB,UAAI,YAAY,QAAW;AACzB,cAAM,UAAU,OAAO,WAAW,MAAM,aAAa,IAAI,GAAG,OAAO;AACnE,eAAO,MAAM,OAAO,aAAa,OAAO;MAC1C;IACF,GAAG,CAAC,OAAO,CAAC;AAEZ,WAAO,aAAa,QAAQ,uBAAuB,eACjD,yBAAC,UAAU,MAAV,EAAgB,GAAG,eAAe,KAAK,aAAA,CAAc,IACpD;EACN;AACF;AAEA,eAAe,cAAc;AAI7B,SAAS,qBAAqB,OAAgC,KAAkC;AAC9F,MAAI,CAAC,OAAO;AACV,WAAO;EACT;AACA,MAAI,CAAC,KAAK;AACR,WAAO;EACT;AACA,MAAI,MAAM,QAAQ,KAAK;AACrB,UAAM,MAAM;EACd;AACA,SAAO,MAAM,YAAY,MAAM,eAAe,IAAI,WAAW;AAC/D;AAEA,SAAS,sBACP,KACA,EAAE,gBAAgB,YAAY,GAC9B;AACA,QAAM,aAAa,cAAc;AACjC,QAAM,WAAiB,cAAgC,IAAI;AAC3D,QAAM,SAAS,MAAM;AACnB,QAAI,CAAC,WAAY,QAAO;AACxB,QAAI,CAAC,SAAS,SAAS;AACrB,eAAS,UAAU,IAAI,OAAO,MAAM;IACtC;AACA,WAAO,SAAS;EAClB,GAAG;AAEH,QAAM,CAAC,eAAe,gBAAgB,IAAU;IAA6B,MAC3E,qBAAqB,OAAO,GAAG;EACjC;AAEA,mBAAgB,MAAM;AACpB,qBAAiB,qBAAqB,OAAO,GAAG,CAAC;EACnD,GAAG,CAAC,OAAO,GAAG,CAAC;AAEf,mBAAgB,MAAM;AACpB,UAAM,eAAe,CAAC,WAA+B,MAAM;AACzD,uBAAiB,MAAM;IACzB;AAEA,QAAI,CAAC,MAAO;AAEZ,UAAM,aAAa,aAAa,QAAQ;AACxC,UAAM,cAAc,aAAa,OAAO;AACxC,UAAM,iBAAiB,QAAQ,UAAU;AACzC,UAAM,iBAAiB,SAAS,WAAW;AAC3C,QAAI,gBAAgB;AAClB,YAAM,iBAAiB;IACzB;AACA,QAAI,OAAO,gBAAgB,UAAU;AACnC,YAAM,cAAc;IACtB;AAEA,WAAO,MAAM;AACX,YAAM,oBAAoB,QAAQ,UAAU;AAC5C,YAAM,oBAAoB,SAAS,WAAW;IAChD;EACF,GAAG,CAAC,OAAO,aAAa,cAAc,CAAC;AAEvC,SAAO;AACT;AAEA,IAAM,OAAO;AACb,IAAM,QAAQ;AACd,IAAM,WAAW;", - "names": ["React", "createContext", "useContext", "createScope", "nextScopes", "React", "import_jsx_runtime", "import_jsx_runtime"] -} diff --git a/app/node_modules/.vite/deps/@radix-ui_react-dropdown-menu.js b/app/node_modules/.vite/deps/@radix-ui_react-dropdown-menu.js index da57abf3..3c748ce6 100644 --- a/app/node_modules/.vite/deps/@radix-ui_react-dropdown-menu.js +++ b/app/node_modules/.vite/deps/@radix-ui_react-dropdown-menu.js @@ -2,14 +2,14 @@ import { useCallbackRef, useLayoutEffect2 -} from "./chunk-GRXJTWBV.js"; +} from "./chunk-J4JAFMOP.js"; import { require_react_dom -} from "./chunk-YLZ34CCM.js"; +} from "./chunk-YF4B4G2L.js"; import { composeRefs, useComposedRefs -} from "./chunk-HPBHRBIF.js"; +} from "./chunk-2VUH7NEY.js"; import { require_jsx_runtime } from "./chunk-USXRE7Q2.js"; @@ -634,7 +634,7 @@ function usePointerDownOutside(onPointerDownOutside, ownerDocument = globalThis? React10.useEffect(() => { const handlePointerDown = (event) => { if (event.target && !isPointerInsideReactTreeRef.current) { - let handleAndDispatchPointerDownOutsideEvent2 = function() { + let handleAndDispatchPointerDownOutsideEvent2 = function () { handleAndDispatchCustomEvent( POINTER_DOWN_OUTSIDE, handlePointerDownOutside, @@ -764,7 +764,7 @@ var FocusScope = React12.forwardRef((props, forwardedRef) => { }).current; React12.useEffect(() => { if (trapped) { - let handleFocusIn2 = function(event) { + let handleFocusIn2 = function (event) { if (focusScope.paused || !container) return; const target = event.target; if (container.contains(target)) { @@ -772,14 +772,14 @@ var FocusScope = React12.forwardRef((props, forwardedRef) => { } else { focus(lastFocusedElementRef.current, { select: true }); } - }, handleFocusOut2 = function(event) { + }, handleFocusOut2 = function (event) { if (focusScope.paused || !container) return; const relatedTarget = event.relatedTarget; if (relatedTarget === null) return; if (!container.contains(relatedTarget)) { focus(lastFocusedElementRef.current, { select: true }); } - }, handleMutations2 = function(mutations) { + }, handleMutations2 = function (mutations) { const focusedElement = document.activeElement; if (focusedElement !== document.body) return; for (const mutation of mutations) { @@ -1343,7 +1343,7 @@ var arrow = (options) => ({ }; } }); -var flip = function(options) { +var flip = function (options) { if (options === void 0) { options = {}; } @@ -1403,8 +1403,8 @@ var flip = function(options) { if (nextPlacement) { const ignoreCrossAxisOverflow = checkCrossAxis === "alignment" ? initialSideAxis !== getSideAxis(nextPlacement) : false; if (!ignoreCrossAxisOverflow || // We leave the current main axis only if every placement on that axis - // overflows the main axis. - overflowsData.every((d) => getSideAxis(d.placement) === initialSideAxis ? d.overflows[0] > 0 : true)) { + // overflows the main axis. + overflowsData.every((d) => getSideAxis(d.placement) === initialSideAxis ? d.overflows[0] > 0 : true)) { return { data: { index: nextIndex, @@ -1425,8 +1425,8 @@ var flip = function(options) { if (hasFallbackAxisSideDirection) { const currentSideAxis = getSideAxis(d.placement); return currentSideAxis === initialSideAxis || // Create a bias to the `y` side axis due to horizontal - // reading directions favoring greater width. - currentSideAxis === "y"; + // reading directions favoring greater width. + currentSideAxis === "y"; } return true; }).map((d) => [d.placement, d.overflows.filter((overflow2) => overflow2 > 0).reduce((acc, overflow2) => acc + overflow2, 0)]).sort((a, b) => a[1] - b[1])[0]) == null ? void 0 : _overflowsData$filter2[0]; @@ -1463,7 +1463,7 @@ function getSideOffsets(overflow, rect) { function isAnySideFullyClipped(overflow) { return sides.some((side) => overflow[side] >= 0); } -var hide = function(options) { +var hide = function (options) { if (options === void 0) { options = {}; } @@ -1535,10 +1535,10 @@ async function convertValueToCoords(state, options) { crossAxis: 0, alignmentAxis: null } : { - mainAxis: rawValue.mainAxis || 0, - crossAxis: rawValue.crossAxis || 0, - alignmentAxis: rawValue.alignmentAxis - }; + mainAxis: rawValue.mainAxis || 0, + crossAxis: rawValue.crossAxis || 0, + alignmentAxis: rawValue.alignmentAxis + }; if (alignment && typeof alignmentAxis === "number") { crossAxis = alignment === "end" ? alignmentAxis * -1 : alignmentAxis; } @@ -1550,7 +1550,7 @@ async function convertValueToCoords(state, options) { y: crossAxis * crossAxisMulti }; } -var offset = function(options) { +var offset = function (options) { if (options === void 0) { options = 0; } @@ -1580,7 +1580,7 @@ var offset = function(options) { } }; }; -var shift = function(options) { +var shift = function (options) { if (options === void 0) { options = {}; } @@ -1652,7 +1652,7 @@ var shift = function(options) { } }; }; -var limitShift = function(options) { +var limitShift = function (options) { if (options === void 0) { options = {}; } @@ -1717,7 +1717,7 @@ var limitShift = function(options) { } }; }; -var size = function(options) { +var size = function (options) { if (options === void 0) { options = {}; } @@ -2363,7 +2363,7 @@ function getOffsetParent(element, polyfill) { } return offsetParent || getContainingBlock(element) || win; } -var getElementRects = async function(data) { +var getElementRects = async function (data) { const getOffsetParentFn = this.getOffsetParent || getOffsetParent; const getDimensionsFn = this.getDimensions; const floatingDimensions = await getDimensionsFn(data.floating); @@ -2585,7 +2585,7 @@ function deepEqual(a, b) { if (Array.isArray(a)) { length = a.length; if (length !== b.length) return false; - for (i = length; i-- !== 0; ) { + for (i = length; i-- !== 0;) { if (!deepEqual(a[i], b[i])) { return false; } @@ -2597,12 +2597,12 @@ function deepEqual(a, b) { if (length !== Object.keys(b).length) { return false; } - for (i = length; i-- !== 0; ) { + for (i = length; i-- !== 0;) { if (!{}.hasOwnProperty.call(b, keys[i])) { return false; } } - for (i = length; i-- !== 0; ) { + for (i = length; i-- !== 0;) { const key = keys[i]; if (key === "_owner" && a.$$typeof) { continue; @@ -3623,7 +3623,7 @@ function getElementRef4(element) { } // node_modules/aria-hidden/dist/es2015/index.js -var getDefaultParent = function(originalTarget) { +var getDefaultParent = function (originalTarget) { if (typeof document === "undefined") { return null; } @@ -3634,11 +3634,11 @@ var counterMap = /* @__PURE__ */ new WeakMap(); var uncontrolledNodes = /* @__PURE__ */ new WeakMap(); var markerMap = {}; var lockCount = 0; -var unwrapHost = function(node) { +var unwrapHost = function (node) { return node && (node.host || unwrapHost(node.parentNode)); }; -var correctTargets = function(parent, targets) { - return targets.map(function(target) { +var correctTargets = function (parent, targets) { + return targets.map(function (target) { if (parent.contains(target)) { return target; } @@ -3648,11 +3648,11 @@ var correctTargets = function(parent, targets) { } console.error("aria-hidden", target, "in not contained inside", parent, ". Doing nothing"); return null; - }).filter(function(x) { + }).filter(function (x) { return Boolean(x); }); }; -var applyAttributeToOthers = function(originalTarget, parentNode, markerName, controlAttribute) { +var applyAttributeToOthers = function (originalTarget, parentNode, markerName, controlAttribute) { var targets = correctTargets(parentNode, Array.isArray(originalTarget) ? originalTarget : [originalTarget]); if (!markerMap[markerName]) { markerMap[markerName] = /* @__PURE__ */ new WeakMap(); @@ -3661,7 +3661,7 @@ var applyAttributeToOthers = function(originalTarget, parentNode, markerName, co var hiddenNodes = []; var elementsToKeep = /* @__PURE__ */ new Set(); var elementsToStop = new Set(targets); - var keep = function(el) { + var keep = function (el) { if (!el || elementsToKeep.has(el)) { return; } @@ -3669,11 +3669,11 @@ var applyAttributeToOthers = function(originalTarget, parentNode, markerName, co keep(el.parentNode); }; targets.forEach(keep); - var deep = function(parent) { + var deep = function (parent) { if (!parent || elementsToStop.has(parent)) { return; } - Array.prototype.forEach.call(parent.children, function(node) { + Array.prototype.forEach.call(parent.children, function (node) { if (elementsToKeep.has(node)) { deep(node); } else { @@ -3703,8 +3703,8 @@ var applyAttributeToOthers = function(originalTarget, parentNode, markerName, co deep(parentNode); elementsToKeep.clear(); lockCount++; - return function() { - hiddenNodes.forEach(function(node) { + return function () { + hiddenNodes.forEach(function (node) { var counterValue = counterMap.get(node) - 1; var markerValue = markerCounter.get(node) - 1; counterMap.set(node, counterValue); @@ -3728,14 +3728,14 @@ var applyAttributeToOthers = function(originalTarget, parentNode, markerName, co } }; }; -var hideOthers = function(originalTarget, parentNode, markerName) { +var hideOthers = function (originalTarget, parentNode, markerName) { if (markerName === void 0) { markerName = "data-aria-hidden"; } var targets = Array.from(Array.isArray(originalTarget) ? originalTarget : [originalTarget]); var activeParentNode = parentNode || getDefaultParent(originalTarget); if (!activeParentNode) { - return function() { + return function () { return null; }; } @@ -3744,7 +3744,7 @@ var hideOthers = function(originalTarget, parentNode, markerName) { }; // node_modules/tslib/tslib.es6.mjs -var __assign = function() { +var __assign = function () { __assign = Object.assign || function __assign2(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; @@ -3800,7 +3800,7 @@ function assignRef(ref, value) { // node_modules/use-callback-ref/dist/es2015/useRef.js var import_react4 = __toESM(require_react()); function useCallbackRef2(initialValue, callback) { - var ref = (0, import_react4.useState)(function() { + var ref = (0, import_react4.useState)(function () { return { // value value: initialValue, @@ -3830,23 +3830,23 @@ var React25 = __toESM(require_react()); var useIsomorphicLayoutEffect = typeof window !== "undefined" ? React25.useLayoutEffect : React25.useEffect; var currentValues = /* @__PURE__ */ new WeakMap(); function useMergeRefs(refs, defaultValue) { - var callbackRef = useCallbackRef2(defaultValue || null, function(newValue) { - return refs.forEach(function(ref) { + var callbackRef = useCallbackRef2(defaultValue || null, function (newValue) { + return refs.forEach(function (ref) { return assignRef(ref, newValue); }); }); - useIsomorphicLayoutEffect(function() { + useIsomorphicLayoutEffect(function () { var oldValue = currentValues.get(callbackRef); if (oldValue) { var prevRefs_1 = new Set(oldValue); var nextRefs_1 = new Set(refs); var current_1 = callbackRef.current; - prevRefs_1.forEach(function(ref) { + prevRefs_1.forEach(function (ref) { if (!nextRefs_1.has(ref)) { assignRef(ref, null); } }); - nextRefs_1.forEach(function(ref) { + nextRefs_1.forEach(function (ref) { if (!prevRefs_1.has(ref)) { assignRef(ref, current_1); } @@ -3874,7 +3874,7 @@ function innerCreateMedium(defaults, middleware) { var buffer = []; var assigned = false; var medium = { - read: function() { + read: function () { if (assigned) { throw new Error("Sidecar: could not `read` from an `assigned` medium. `read` could be used only with `useMedium`."); } @@ -3883,16 +3883,16 @@ function innerCreateMedium(defaults, middleware) { } return defaults; }, - useMedium: function(data) { + useMedium: function (data) { var item = middleware(data, assigned); buffer.push(item); - return function() { - buffer = buffer.filter(function(x) { + return function () { + buffer = buffer.filter(function (x) { return x !== item; }); }; }, - assignSyncMedium: function(cb) { + assignSyncMedium: function (cb) { assigned = true; while (buffer.length) { var cbs = buffer; @@ -3900,15 +3900,15 @@ function innerCreateMedium(defaults, middleware) { cbs.forEach(cb); } buffer = { - push: function(x) { + push: function (x) { return cb(x); }, - filter: function() { + filter: function () { return buffer; } }; }, - assignMedium: function(cb) { + assignMedium: function (cb) { assigned = true; var pendingQueue = []; if (buffer.length) { @@ -3917,21 +3917,21 @@ function innerCreateMedium(defaults, middleware) { cbs.forEach(cb); pendingQueue = buffer; } - var executeQueue = function() { + var executeQueue = function () { var cbs2 = pendingQueue; pendingQueue = []; cbs2.forEach(cb); }; - var cycle = function() { + var cycle = function () { return Promise.resolve().then(executeQueue); }; cycle(); buffer = { - push: function(x) { + push: function (x) { pendingQueue.push(x); cycle(); }, - filter: function(filter) { + filter: function (filter) { pendingQueue = pendingQueue.filter(filter); return buffer; } @@ -3955,7 +3955,7 @@ var import_react6 = __toESM(require_react()); // node_modules/use-sidecar/dist/es2015/exports.js var React28 = __toESM(require_react()); -var SideCar = function(_a) { +var SideCar = function (_a) { var sideCar = _a.sideCar, rest = __rest(_a, ["sideCar"]); if (!sideCar) { throw new Error("Sidecar: please provide `sideCar` property to import the right car"); @@ -3976,10 +3976,10 @@ function exportSidecar(medium, exported) { var effectCar = createSidecarMedium(); // node_modules/react-remove-scroll/dist/es2015/UI.js -var nothing = function() { +var nothing = function () { return; }; -var RemoveScroll = React29.forwardRef(function(props, parentRef) { +var RemoveScroll = React29.forwardRef(function (props, parentRef) { var ref = React29.useRef(null); var _a = React29.useState({ onScrollCapture: nothing, @@ -4018,7 +4018,7 @@ var React30 = __toESM(require_react()); // node_modules/get-nonce/dist/es2015/index.js var currentNonce; -var getNonce = function() { +var getNonce = function () { if (currentNonce) { return currentNonce; } @@ -4051,11 +4051,11 @@ function insertStyleTag(tag) { var head = document.head || document.getElementsByTagName("head")[0]; head.appendChild(tag); } -var stylesheetSingleton = function() { +var stylesheetSingleton = function () { var counter = 0; var stylesheet = null; return { - add: function(style) { + add: function (style) { if (counter == 0) { if (stylesheet = makeStyleTag()) { injectStyles(stylesheet, style); @@ -4064,7 +4064,7 @@ var stylesheetSingleton = function() { } counter++; }, - remove: function() { + remove: function () { counter--; if (!counter && stylesheet) { stylesheet.parentNode && stylesheet.parentNode.removeChild(stylesheet); @@ -4075,12 +4075,12 @@ var stylesheetSingleton = function() { }; // node_modules/react-style-singleton/dist/es2015/hook.js -var styleHookSingleton = function() { +var styleHookSingleton = function () { var sheet = stylesheetSingleton(); - return function(styles, isDynamic) { - React30.useEffect(function() { + return function (styles, isDynamic) { + React30.useEffect(function () { sheet.add(styles); - return function() { + return function () { sheet.remove(); }; }, [styles && isDynamic]); @@ -4088,9 +4088,9 @@ var styleHookSingleton = function() { }; // node_modules/react-style-singleton/dist/es2015/component.js -var styleSingleton = function() { +var styleSingleton = function () { var useStyle = styleHookSingleton(); - var Sheet = function(_a) { + var Sheet = function (_a) { var styles = _a.styles, dynamic = _a.dynamic; useStyle(styles, dynamic); return null; @@ -4105,17 +4105,17 @@ var zeroGap = { right: 0, gap: 0 }; -var parse = function(x) { +var parse = function (x) { return parseInt(x || "", 10) || 0; }; -var getOffset = function(gapMode) { +var getOffset = function (gapMode) { var cs = window.getComputedStyle(document.body); var left = cs[gapMode === "padding" ? "paddingLeft" : "marginLeft"]; var top = cs[gapMode === "padding" ? "paddingTop" : "marginTop"]; var right = cs[gapMode === "padding" ? "paddingRight" : "marginRight"]; return [parse(left), parse(top), parse(right)]; }; -var getGapWidth = function(gapMode) { +var getGapWidth = function (gapMode) { if (gapMode === void 0) { gapMode = "margin"; } @@ -4136,7 +4136,7 @@ var getGapWidth = function(gapMode) { // node_modules/react-remove-scroll-bar/dist/es2015/component.js var Style = styleSingleton(); var lockAttribute = "data-scroll-locked"; -var getStyles = function(_a, allowRelative, gapMode, important) { +var getStyles = function (_a, allowRelative, gapMode, important) { var left = _a.left, top = _a.top, right = _a.right, gap = _a.gap; if (gapMode === void 0) { gapMode = "margin"; @@ -4147,14 +4147,14 @@ var getStyles = function(_a, allowRelative, gapMode, important) { gapMode === "padding" && "padding-right: ".concat(gap, "px ").concat(important, ";") ].filter(Boolean).join(""), "\n }\n \n .").concat(zeroRightClassName, " {\n right: ").concat(gap, "px ").concat(important, ";\n }\n \n .").concat(fullWidthClassName, " {\n margin-right: ").concat(gap, "px ").concat(important, ";\n }\n \n .").concat(zeroRightClassName, " .").concat(zeroRightClassName, " {\n right: 0 ").concat(important, ";\n }\n \n .").concat(fullWidthClassName, " .").concat(fullWidthClassName, " {\n margin-right: 0 ").concat(important, ";\n }\n \n body[").concat(lockAttribute, "] {\n ").concat(removedBarSizeVariable, ": ").concat(gap, "px;\n }\n"); }; -var getCurrentUseCounter = function() { +var getCurrentUseCounter = function () { var counter = parseInt(document.body.getAttribute(lockAttribute) || "0", 10); return isFinite(counter) ? counter : 0; }; -var useLockAttribute = function() { - React31.useEffect(function() { +var useLockAttribute = function () { + React31.useEffect(function () { document.body.setAttribute(lockAttribute, (getCurrentUseCounter() + 1).toString()); - return function() { + return function () { var newCounter = getCurrentUseCounter() - 1; if (newCounter <= 0) { document.body.removeAttribute(lockAttribute); @@ -4164,10 +4164,10 @@ var useLockAttribute = function() { }; }, []); }; -var RemoveScrollBar = function(_a) { +var RemoveScrollBar = function (_a) { var noRelative = _a.noRelative, noImportant = _a.noImportant, _b = _a.gapMode, gapMode = _b === void 0 ? "margin" : _b; useLockAttribute(); - var gap = React31.useMemo(function() { + var gap = React31.useMemo(function () { return getGapWidth(gapMode); }, [gapMode]); return React31.createElement(Style, { styles: getStyles(gap, !noRelative, gapMode, !noImportant ? "!important" : "") }); @@ -4178,7 +4178,7 @@ var passiveSupported = false; if (typeof window !== "undefined") { try { options = Object.defineProperty({}, "passive", { - get: function() { + get: function () { passiveSupported = true; return true; } @@ -4193,10 +4193,10 @@ var options; var nonPassive = passiveSupported ? { passive: false } : false; // node_modules/react-remove-scroll/dist/es2015/handleScroll.js -var alwaysContainsScroll = function(node) { +var alwaysContainsScroll = function (node) { return node.tagName === "TEXTAREA"; }; -var elementCanBeScrolled = function(node, overflow) { +var elementCanBeScrolled = function (node, overflow) { if (!(node instanceof Element)) { return false; } @@ -4207,13 +4207,13 @@ var elementCanBeScrolled = function(node, overflow) { !(styles.overflowY === styles.overflowX && !alwaysContainsScroll(node) && styles[overflow] === "visible") ); }; -var elementCouldBeVScrolled = function(node) { +var elementCouldBeVScrolled = function (node) { return elementCanBeScrolled(node, "overflowY"); }; -var elementCouldBeHScrolled = function(node) { +var elementCouldBeHScrolled = function (node) { return elementCanBeScrolled(node, "overflowX"); }; -var locationCouldBeScrolled = function(axis, node) { +var locationCouldBeScrolled = function (axis, node) { var ownerDocument = node.ownerDocument; var current = node; do { @@ -4231,7 +4231,7 @@ var locationCouldBeScrolled = function(axis, node) { } while (current && current !== ownerDocument.body); return false; }; -var getVScrollVariables = function(_a) { +var getVScrollVariables = function (_a) { var scrollTop = _a.scrollTop, scrollHeight = _a.scrollHeight, clientHeight = _a.clientHeight; return [ scrollTop, @@ -4239,7 +4239,7 @@ var getVScrollVariables = function(_a) { clientHeight ]; }; -var getHScrollVariables = function(_a) { +var getHScrollVariables = function (_a) { var scrollLeft = _a.scrollLeft, scrollWidth = _a.scrollWidth, clientWidth = _a.clientWidth; return [ scrollLeft, @@ -4247,16 +4247,16 @@ var getHScrollVariables = function(_a) { clientWidth ]; }; -var elementCouldBeScrolled = function(axis, node) { +var elementCouldBeScrolled = function (axis, node) { return axis === "v" ? elementCouldBeVScrolled(node) : elementCouldBeHScrolled(node); }; -var getScrollVariables = function(axis, node) { +var getScrollVariables = function (axis, node) { return axis === "v" ? getVScrollVariables(node) : getHScrollVariables(node); }; -var getDirectionFactor = function(axis, direction) { +var getDirectionFactor = function (axis, direction) { return axis === "h" && direction === "rtl" ? -1 : 1; }; -var handleScroll = function(axis, endTarget, event, sourceDelta, noOverscroll) { +var handleScroll = function (axis, endTarget, event, sourceDelta, noOverscroll) { var directionFactor = getDirectionFactor(axis, window.getComputedStyle(endTarget).direction); var delta = directionFactor * sourceDelta; var target = event.target; @@ -4293,19 +4293,19 @@ var handleScroll = function(axis, endTarget, event, sourceDelta, noOverscroll) { }; // node_modules/react-remove-scroll/dist/es2015/SideEffect.js -var getTouchXY = function(event) { +var getTouchXY = function (event) { return "changedTouches" in event ? [event.changedTouches[0].clientX, event.changedTouches[0].clientY] : [0, 0]; }; -var getDeltaXY = function(event) { +var getDeltaXY = function (event) { return [event.deltaX, event.deltaY]; }; -var extractRef = function(ref) { +var extractRef = function (ref) { return ref && "current" in ref ? ref.current : ref; }; -var deltaCompare = function(x, y) { +var deltaCompare = function (x, y) { return x[0] === y[0] && x[1] === y[1]; }; -var generateStyle = function(id) { +var generateStyle = function (id) { return "\n .block-interactivity-".concat(id, " {pointer-events: none;}\n .allow-interactivity-").concat(id, " {pointer-events: all;}\n"); }; var idCounter = 0; @@ -4317,26 +4317,26 @@ function RemoveScrollSideCar(props) { var id = React32.useState(idCounter++)[0]; var Style2 = React32.useState(styleSingleton)[0]; var lastProps = React32.useRef(props); - React32.useEffect(function() { + React32.useEffect(function () { lastProps.current = props; }, [props]); - React32.useEffect(function() { + React32.useEffect(function () { if (props.inert) { document.body.classList.add("block-interactivity-".concat(id)); var allow_1 = __spreadArray([props.lockRef.current], (props.shards || []).map(extractRef), true).filter(Boolean); - allow_1.forEach(function(el) { + allow_1.forEach(function (el) { return el.classList.add("allow-interactivity-".concat(id)); }); - return function() { + return function () { document.body.classList.remove("block-interactivity-".concat(id)); - allow_1.forEach(function(el) { + allow_1.forEach(function (el) { return el.classList.remove("allow-interactivity-".concat(id)); }); }; } return; }, [props.inert, props.lockRef.current, props.shards]); - var shouldCancelEvent = React32.useCallback(function(event, parent) { + var shouldCancelEvent = React32.useCallback(function (event, parent) { if ("touches" in event && event.touches.length === 2 || event.type === "wheel" && event.ctrlKey) { return !lastProps.current.allowPinchZoom; } @@ -4378,13 +4378,13 @@ function RemoveScrollSideCar(props) { var cancelingAxis = activeAxis.current || currentAxis; return handleScroll(cancelingAxis, parent, event, cancelingAxis === "h" ? deltaX : deltaY, true); }, []); - var shouldPrevent = React32.useCallback(function(_event) { + var shouldPrevent = React32.useCallback(function (_event) { var event = _event; if (!lockStack.length || lockStack[lockStack.length - 1] !== Style2) { return; } var delta = "deltaY" in event ? getDeltaXY(event) : getTouchXY(event); - var sourceEvent = shouldPreventQueue.current.filter(function(e) { + var sourceEvent = shouldPreventQueue.current.filter(function (e) { return e.name === event.type && (e.target === event.target || event.target === e.shadowParent) && deltaCompare(e.delta, delta); })[0]; if (sourceEvent && sourceEvent.should) { @@ -4394,7 +4394,7 @@ function RemoveScrollSideCar(props) { return; } if (!sourceEvent) { - var shardNodes = (lastProps.current.shards || []).map(extractRef).filter(Boolean).filter(function(node) { + var shardNodes = (lastProps.current.shards || []).map(extractRef).filter(Boolean).filter(function (node) { return node.contains(event.target); }); var shouldStop = shardNodes.length > 0 ? shouldCancelEvent(event, shardNodes[0]) : !lastProps.current.noIsolation; @@ -4405,26 +4405,26 @@ function RemoveScrollSideCar(props) { } } }, []); - var shouldCancel = React32.useCallback(function(name, delta, target, should) { + var shouldCancel = React32.useCallback(function (name, delta, target, should) { var event = { name, delta, target, should, shadowParent: getOutermostShadowParent(target) }; shouldPreventQueue.current.push(event); - setTimeout(function() { - shouldPreventQueue.current = shouldPreventQueue.current.filter(function(e) { + setTimeout(function () { + shouldPreventQueue.current = shouldPreventQueue.current.filter(function (e) { return e !== event; }); }, 1); }, []); - var scrollTouchStart = React32.useCallback(function(event) { + var scrollTouchStart = React32.useCallback(function (event) { touchStartRef.current = getTouchXY(event); activeAxis.current = void 0; }, []); - var scrollWheel = React32.useCallback(function(event) { + var scrollWheel = React32.useCallback(function (event) { shouldCancel(event.type, getDeltaXY(event), event.target, shouldCancelEvent(event, props.lockRef.current)); }, []); - var scrollTouchMove = React32.useCallback(function(event) { + var scrollTouchMove = React32.useCallback(function (event) { shouldCancel(event.type, getTouchXY(event), event.target, shouldCancelEvent(event, props.lockRef.current)); }, []); - React32.useEffect(function() { + React32.useEffect(function () { lockStack.push(Style2); props.setCallbacks({ onScrollCapture: scrollWheel, @@ -4434,8 +4434,8 @@ function RemoveScrollSideCar(props) { document.addEventListener("wheel", shouldPrevent, nonPassive); document.addEventListener("touchmove", shouldPrevent, nonPassive); document.addEventListener("touchstart", scrollTouchStart, nonPassive); - return function() { - lockStack = lockStack.filter(function(inst) { + return function () { + lockStack = lockStack.filter(function (inst) { return inst !== Style2; }); document.removeEventListener("wheel", shouldPrevent, nonPassive); @@ -4467,7 +4467,7 @@ function getOutermostShadowParent(node) { var sidecar_default = exportSidecar(effectCar, RemoveScrollSideCar); // node_modules/react-remove-scroll/dist/es2015/Combination.js -var ReactRemoveScroll = React33.forwardRef(function(props, ref) { +var ReactRemoveScroll = React33.forwardRef(function (props, ref) { return React33.createElement(RemoveScroll, __assign({}, props, { ref, sideCar: sidecar_default })); }); ReactRemoveScroll.classNames = RemoveScroll.classNames; @@ -4519,27 +4519,29 @@ var Menu = (props) => { document.removeEventListener("pointermove", handlePointer, { capture: true }); }; }, []); - return (0, import_jsx_runtime15.jsx)(Root2, { ...popperScope, children: (0, import_jsx_runtime15.jsx)( - MenuProvider, - { - scope: __scopeMenu, - open, - onOpenChange: handleOpenChange, - content, - onContentChange: setContent, - children: (0, import_jsx_runtime15.jsx)( - MenuRootProvider, - { - scope: __scopeMenu, - onClose: React34.useCallback(() => handleOpenChange(false), [handleOpenChange]), - isUsingKeyboardRef, - dir: direction, - modal, - children - } - ) - } - ) }); + return (0, import_jsx_runtime15.jsx)(Root2, { + ...popperScope, children: (0, import_jsx_runtime15.jsx)( + MenuProvider, + { + scope: __scopeMenu, + open, + onOpenChange: handleOpenChange, + content, + onContentChange: setContent, + children: (0, import_jsx_runtime15.jsx)( + MenuRootProvider, + { + scope: __scopeMenu, + onClose: React34.useCallback(() => handleOpenChange(false), [handleOpenChange]), + isUsingKeyboardRef, + dir: direction, + modal, + children + } + ) + } + ) + }); }; Menu.displayName = MENU_NAME; var ANCHOR_NAME2 = "MenuAnchor"; @@ -4702,96 +4704,98 @@ var MenuContentImpl = React34.forwardRef( onPointerGraceIntentChange: React34.useCallback((intent) => { pointerGraceIntentRef.current = intent; }, []), - children: (0, import_jsx_runtime15.jsx)(ScrollLockWrapper, { ...scrollLockWrapperProps, children: (0, import_jsx_runtime15.jsx)( - FocusScope, - { - asChild: true, - trapped: trapFocus, - onMountAutoFocus: composeEventHandlers(onOpenAutoFocus, (event) => { - event.preventDefault(); - contentRef.current?.focus({ preventScroll: true }); - }), - onUnmountAutoFocus: onCloseAutoFocus, - children: (0, import_jsx_runtime15.jsx)( - DismissableLayer, - { - asChild: true, - disableOutsidePointerEvents, - onEscapeKeyDown, - onPointerDownOutside, - onFocusOutside, - onInteractOutside, - onDismiss, - children: (0, import_jsx_runtime15.jsx)( - Root3, - { - asChild: true, - ...rovingFocusGroupScope, - dir: rootContext.dir, - orientation: "vertical", - loop, - currentTabStopId: currentItemId, - onCurrentTabStopIdChange: setCurrentItemId, - onEntryFocus: composeEventHandlers(onEntryFocus, (event) => { - if (!rootContext.isUsingKeyboardRef.current) event.preventDefault(); - }), - preventScrollOnEntryFocus: true, - children: (0, import_jsx_runtime15.jsx)( - Content, - { - role: "menu", - "aria-orientation": "vertical", - "data-state": getOpenState(context.open), - "data-radix-menu-content": "", - dir: rootContext.dir, - ...popperScope, - ...contentProps, - ref: composedRefs, - style: { outline: "none", ...contentProps.style }, - onKeyDown: composeEventHandlers(contentProps.onKeyDown, (event) => { - const target = event.target; - const isKeyDownInside = target.closest("[data-radix-menu-content]") === event.currentTarget; - const isModifierKey = event.ctrlKey || event.altKey || event.metaKey; - const isCharacterKey = event.key.length === 1; - if (isKeyDownInside) { - if (event.key === "Tab") event.preventDefault(); - if (!isModifierKey && isCharacterKey) handleTypeaheadSearch(event.key); - } - const content = contentRef.current; - if (event.target !== content) return; - if (!FIRST_LAST_KEYS.includes(event.key)) return; - event.preventDefault(); - const items = getItems().filter((item) => !item.disabled); - const candidateNodes = items.map((item) => item.ref.current); - if (LAST_KEYS.includes(event.key)) candidateNodes.reverse(); - focusFirst3(candidateNodes); - }), - onBlur: composeEventHandlers(props.onBlur, (event) => { - if (!event.currentTarget.contains(event.target)) { - window.clearTimeout(timerRef.current); - searchRef.current = ""; - } - }), - onPointerMove: composeEventHandlers( - props.onPointerMove, - whenMouse((event) => { + children: (0, import_jsx_runtime15.jsx)(ScrollLockWrapper, { + ...scrollLockWrapperProps, children: (0, import_jsx_runtime15.jsx)( + FocusScope, + { + asChild: true, + trapped: trapFocus, + onMountAutoFocus: composeEventHandlers(onOpenAutoFocus, (event) => { + event.preventDefault(); + contentRef.current?.focus({ preventScroll: true }); + }), + onUnmountAutoFocus: onCloseAutoFocus, + children: (0, import_jsx_runtime15.jsx)( + DismissableLayer, + { + asChild: true, + disableOutsidePointerEvents, + onEscapeKeyDown, + onPointerDownOutside, + onFocusOutside, + onInteractOutside, + onDismiss, + children: (0, import_jsx_runtime15.jsx)( + Root3, + { + asChild: true, + ...rovingFocusGroupScope, + dir: rootContext.dir, + orientation: "vertical", + loop, + currentTabStopId: currentItemId, + onCurrentTabStopIdChange: setCurrentItemId, + onEntryFocus: composeEventHandlers(onEntryFocus, (event) => { + if (!rootContext.isUsingKeyboardRef.current) event.preventDefault(); + }), + preventScrollOnEntryFocus: true, + children: (0, import_jsx_runtime15.jsx)( + Content, + { + role: "menu", + "aria-orientation": "vertical", + "data-state": getOpenState(context.open), + "data-radix-menu-content": "", + dir: rootContext.dir, + ...popperScope, + ...contentProps, + ref: composedRefs, + style: { outline: "none", ...contentProps.style }, + onKeyDown: composeEventHandlers(contentProps.onKeyDown, (event) => { const target = event.target; - const pointerXHasChanged = lastPointerXRef.current !== event.clientX; - if (event.currentTarget.contains(target) && pointerXHasChanged) { - const newDir = event.clientX > lastPointerXRef.current ? "right" : "left"; - pointerDirRef.current = newDir; - lastPointerXRef.current = event.clientX; + const isKeyDownInside = target.closest("[data-radix-menu-content]") === event.currentTarget; + const isModifierKey = event.ctrlKey || event.altKey || event.metaKey; + const isCharacterKey = event.key.length === 1; + if (isKeyDownInside) { + if (event.key === "Tab") event.preventDefault(); + if (!isModifierKey && isCharacterKey) handleTypeaheadSearch(event.key); } - }) - ) - } - ) - } - ) - } - ) - } - ) }) + const content = contentRef.current; + if (event.target !== content) return; + if (!FIRST_LAST_KEYS.includes(event.key)) return; + event.preventDefault(); + const items = getItems().filter((item) => !item.disabled); + const candidateNodes = items.map((item) => item.ref.current); + if (LAST_KEYS.includes(event.key)) candidateNodes.reverse(); + focusFirst3(candidateNodes); + }), + onBlur: composeEventHandlers(props.onBlur, (event) => { + if (!event.currentTarget.contains(event.target)) { + window.clearTimeout(timerRef.current); + searchRef.current = ""; + } + }), + onPointerMove: composeEventHandlers( + props.onPointerMove, + whenMouse((event) => { + const target = event.target; + const pointerXHasChanged = lastPointerXRef.current !== event.clientX; + if (event.currentTarget.contains(target) && pointerXHasChanged) { + const newDir = event.clientX > lastPointerXRef.current ? "right" : "left"; + pointerDirRef.current = newDir; + lastPointerXRef.current = event.clientX; + } + }) + ) + } + ) + } + ) + } + ) + } + ) + }) } ); } @@ -4884,37 +4888,39 @@ var MenuItemImpl = React34.forwardRef( scope: __scopeMenu, disabled, textValue: textValue ?? textContent, - children: (0, import_jsx_runtime15.jsx)(Item, { asChild: true, ...rovingFocusGroupScope, focusable: !disabled, children: (0, import_jsx_runtime15.jsx)( - Primitive.div, - { - role: "menuitem", - "data-highlighted": isFocused ? "" : void 0, - "aria-disabled": disabled || void 0, - "data-disabled": disabled ? "" : void 0, - ...itemProps, - ref: composedRefs, - onPointerMove: composeEventHandlers( - props.onPointerMove, - whenMouse((event) => { - if (disabled) { - contentContext.onItemLeave(event); - } else { - contentContext.onItemEnter(event); - if (!event.defaultPrevented) { - const item = event.currentTarget; - item.focus({ preventScroll: true }); + children: (0, import_jsx_runtime15.jsx)(Item, { + asChild: true, ...rovingFocusGroupScope, focusable: !disabled, children: (0, import_jsx_runtime15.jsx)( + Primitive.div, + { + role: "menuitem", + "data-highlighted": isFocused ? "" : void 0, + "aria-disabled": disabled || void 0, + "data-disabled": disabled ? "" : void 0, + ...itemProps, + ref: composedRefs, + onPointerMove: composeEventHandlers( + props.onPointerMove, + whenMouse((event) => { + if (disabled) { + contentContext.onItemLeave(event); + } else { + contentContext.onItemEnter(event); + if (!event.defaultPrevented) { + const item = event.currentTarget; + item.focus({ preventScroll: true }); + } } - } - }) - ), - onPointerLeave: composeEventHandlers( - props.onPointerLeave, - whenMouse((event) => contentContext.onItemLeave(event)) - ), - onFocus: composeEventHandlers(props.onFocus, () => setIsFocused(true)), - onBlur: composeEventHandlers(props.onBlur, () => setIsFocused(false)) - } - ) }) + }) + ), + onPointerLeave: composeEventHandlers( + props.onPointerLeave, + whenMouse((event) => contentContext.onItemLeave(event)) + ), + onFocus: composeEventHandlers(props.onFocus, () => setIsFocused(true)), + onBlur: composeEventHandlers(props.onBlur, () => setIsFocused(false)) + } + ) + }) } ); } @@ -4923,29 +4929,33 @@ var CHECKBOX_ITEM_NAME = "MenuCheckboxItem"; var MenuCheckboxItem = React34.forwardRef( (props, forwardedRef) => { const { checked = false, onCheckedChange, ...checkboxItemProps } = props; - return (0, import_jsx_runtime15.jsx)(ItemIndicatorProvider, { scope: props.__scopeMenu, checked, children: (0, import_jsx_runtime15.jsx)( - MenuItem, - { - role: "menuitemcheckbox", - "aria-checked": isIndeterminate(checked) ? "mixed" : checked, - ...checkboxItemProps, - ref: forwardedRef, - "data-state": getCheckedState(checked), - onSelect: composeEventHandlers( - checkboxItemProps.onSelect, - () => onCheckedChange?.(isIndeterminate(checked) ? true : !checked), - { checkForDefaultPrevented: false } - ) - } - ) }); + return (0, import_jsx_runtime15.jsx)(ItemIndicatorProvider, { + scope: props.__scopeMenu, checked, children: (0, import_jsx_runtime15.jsx)( + MenuItem, + { + role: "menuitemcheckbox", + "aria-checked": isIndeterminate(checked) ? "mixed" : checked, + ...checkboxItemProps, + ref: forwardedRef, + "data-state": getCheckedState(checked), + onSelect: composeEventHandlers( + checkboxItemProps.onSelect, + () => onCheckedChange?.(isIndeterminate(checked) ? true : !checked), + { checkForDefaultPrevented: false } + ) + } + ) + }); } ); MenuCheckboxItem.displayName = CHECKBOX_ITEM_NAME; var RADIO_GROUP_NAME = "MenuRadioGroup"; var [RadioGroupProvider, useRadioGroupContext] = createMenuContext( RADIO_GROUP_NAME, - { value: void 0, onValueChange: () => { - } } + { + value: void 0, onValueChange: () => { + } + } ); var MenuRadioGroup = React34.forwardRef( (props, forwardedRef) => { @@ -4961,21 +4971,23 @@ var MenuRadioItem = React34.forwardRef( const { value, ...radioItemProps } = props; const context = useRadioGroupContext(RADIO_ITEM_NAME, props.__scopeMenu); const checked = value === context.value; - return (0, import_jsx_runtime15.jsx)(ItemIndicatorProvider, { scope: props.__scopeMenu, checked, children: (0, import_jsx_runtime15.jsx)( - MenuItem, - { - role: "menuitemradio", - "aria-checked": checked, - ...radioItemProps, - ref: forwardedRef, - "data-state": getCheckedState(checked), - onSelect: composeEventHandlers( - radioItemProps.onSelect, - () => context.onValueChange?.(value), - { checkForDefaultPrevented: false } - ) - } - ) }); + return (0, import_jsx_runtime15.jsx)(ItemIndicatorProvider, { + scope: props.__scopeMenu, checked, children: (0, import_jsx_runtime15.jsx)( + MenuItem, + { + role: "menuitemradio", + "aria-checked": checked, + ...radioItemProps, + ref: forwardedRef, + "data-state": getCheckedState(checked), + onSelect: composeEventHandlers( + radioItemProps.onSelect, + () => context.onValueChange?.(value), + { checkForDefaultPrevented: false } + ) + } + ) + }); } ); MenuRadioItem.displayName = RADIO_ITEM_NAME; @@ -5043,27 +5055,29 @@ var MenuSub = (props) => { if (parentMenuContext.open === false) handleOpenChange(false); return () => handleOpenChange(false); }, [parentMenuContext.open, handleOpenChange]); - return (0, import_jsx_runtime15.jsx)(Root2, { ...popperScope, children: (0, import_jsx_runtime15.jsx)( - MenuProvider, - { - scope: __scopeMenu, - open, - onOpenChange: handleOpenChange, - content, - onContentChange: setContent, - children: (0, import_jsx_runtime15.jsx)( - MenuSubProvider, - { - scope: __scopeMenu, - contentId: useId(), - triggerId: useId(), - trigger, - onTriggerChange: setTrigger, - children - } - ) - } - ) }); + return (0, import_jsx_runtime15.jsx)(Root2, { + ...popperScope, children: (0, import_jsx_runtime15.jsx)( + MenuProvider, + { + scope: __scopeMenu, + open, + onOpenChange: handleOpenChange, + content, + onContentChange: setContent, + children: (0, import_jsx_runtime15.jsx)( + MenuSubProvider, + { + scope: __scopeMenu, + contentId: useId(), + triggerId: useId(), + trigger, + onTriggerChange: setTrigger, + children + } + ) + } + ) + }); }; MenuSub.displayName = SUB_NAME; var SUB_TRIGGER_NAME = "MenuSubTrigger"; @@ -5088,82 +5102,84 @@ var MenuSubTrigger = React34.forwardRef( onPointerGraceIntentChange(null); }; }, [pointerGraceTimerRef, onPointerGraceIntentChange]); - return (0, import_jsx_runtime15.jsx)(MenuAnchor, { asChild: true, ...scope, children: (0, import_jsx_runtime15.jsx)( - MenuItemImpl, - { - id: subContext.triggerId, - "aria-haspopup": "menu", - "aria-expanded": context.open, - "aria-controls": subContext.contentId, - "data-state": getOpenState(context.open), - ...props, - ref: composeRefs(forwardedRef, subContext.onTriggerChange), - onClick: (event) => { - props.onClick?.(event); - if (props.disabled || event.defaultPrevented) return; - event.currentTarget.focus(); - if (!context.open) context.onOpenChange(true); - }, - onPointerMove: composeEventHandlers( - props.onPointerMove, - whenMouse((event) => { - contentContext.onItemEnter(event); - if (event.defaultPrevented) return; - if (!props.disabled && !context.open && !openTimerRef.current) { - contentContext.onPointerGraceIntentChange(null); - openTimerRef.current = window.setTimeout(() => { - context.onOpenChange(true); - clearOpenTimer(); - }, 100); - } - }) - ), - onPointerLeave: composeEventHandlers( - props.onPointerLeave, - whenMouse((event) => { - clearOpenTimer(); - const contentRect = context.content?.getBoundingClientRect(); - if (contentRect) { - const side = context.content?.dataset.side; - const rightSide = side === "right"; - const bleed = rightSide ? -5 : 5; - const contentNearEdge = contentRect[rightSide ? "left" : "right"]; - const contentFarEdge = contentRect[rightSide ? "right" : "left"]; - contentContext.onPointerGraceIntentChange({ - area: [ - // Apply a bleed on clientX to ensure that our exit point is - // consistently within polygon bounds - { x: event.clientX + bleed, y: event.clientY }, - { x: contentNearEdge, y: contentRect.top }, - { x: contentFarEdge, y: contentRect.top }, - { x: contentFarEdge, y: contentRect.bottom }, - { x: contentNearEdge, y: contentRect.bottom } - ], - side - }); - window.clearTimeout(pointerGraceTimerRef.current); - pointerGraceTimerRef.current = window.setTimeout( - () => contentContext.onPointerGraceIntentChange(null), - 300 - ); - } else { - contentContext.onTriggerLeave(event); + return (0, import_jsx_runtime15.jsx)(MenuAnchor, { + asChild: true, ...scope, children: (0, import_jsx_runtime15.jsx)( + MenuItemImpl, + { + id: subContext.triggerId, + "aria-haspopup": "menu", + "aria-expanded": context.open, + "aria-controls": subContext.contentId, + "data-state": getOpenState(context.open), + ...props, + ref: composeRefs(forwardedRef, subContext.onTriggerChange), + onClick: (event) => { + props.onClick?.(event); + if (props.disabled || event.defaultPrevented) return; + event.currentTarget.focus(); + if (!context.open) context.onOpenChange(true); + }, + onPointerMove: composeEventHandlers( + props.onPointerMove, + whenMouse((event) => { + contentContext.onItemEnter(event); if (event.defaultPrevented) return; - contentContext.onPointerGraceIntentChange(null); + if (!props.disabled && !context.open && !openTimerRef.current) { + contentContext.onPointerGraceIntentChange(null); + openTimerRef.current = window.setTimeout(() => { + context.onOpenChange(true); + clearOpenTimer(); + }, 100); + } + }) + ), + onPointerLeave: composeEventHandlers( + props.onPointerLeave, + whenMouse((event) => { + clearOpenTimer(); + const contentRect = context.content?.getBoundingClientRect(); + if (contentRect) { + const side = context.content?.dataset.side; + const rightSide = side === "right"; + const bleed = rightSide ? -5 : 5; + const contentNearEdge = contentRect[rightSide ? "left" : "right"]; + const contentFarEdge = contentRect[rightSide ? "right" : "left"]; + contentContext.onPointerGraceIntentChange({ + area: [ + // Apply a bleed on clientX to ensure that our exit point is + // consistently within polygon bounds + { x: event.clientX + bleed, y: event.clientY }, + { x: contentNearEdge, y: contentRect.top }, + { x: contentFarEdge, y: contentRect.top }, + { x: contentFarEdge, y: contentRect.bottom }, + { x: contentNearEdge, y: contentRect.bottom } + ], + side + }); + window.clearTimeout(pointerGraceTimerRef.current); + pointerGraceTimerRef.current = window.setTimeout( + () => contentContext.onPointerGraceIntentChange(null), + 300 + ); + } else { + contentContext.onTriggerLeave(event); + if (event.defaultPrevented) return; + contentContext.onPointerGraceIntentChange(null); + } + }) + ), + onKeyDown: composeEventHandlers(props.onKeyDown, (event) => { + const isTypingAhead = contentContext.searchRef.current !== ""; + if (props.disabled || isTypingAhead && event.key === " ") return; + if (SUB_OPEN_KEYS[rootContext.dir].includes(event.key)) { + context.onOpenChange(true); + context.content?.focus(); + event.preventDefault(); } }) - ), - onKeyDown: composeEventHandlers(props.onKeyDown, (event) => { - const isTypingAhead = contentContext.searchRef.current !== ""; - if (props.disabled || isTypingAhead && event.key === " ") return; - if (SUB_OPEN_KEYS[rootContext.dir].includes(event.key)) { - context.onOpenChange(true); - context.content?.focus(); - event.preventDefault(); - } - }) - } - ) }); + } + ) + }); } ); MenuSubTrigger.displayName = SUB_TRIGGER_NAME; @@ -5177,41 +5193,47 @@ var MenuSubContent = React34.forwardRef( const subContext = useMenuSubContext(SUB_CONTENT_NAME, props.__scopeMenu); const ref = React34.useRef(null); const composedRefs = useComposedRefs(forwardedRef, ref); - return (0, import_jsx_runtime15.jsx)(Collection2.Provider, { scope: props.__scopeMenu, children: (0, import_jsx_runtime15.jsx)(Presence, { present: forceMount || context.open, children: (0, import_jsx_runtime15.jsx)(Collection2.Slot, { scope: props.__scopeMenu, children: (0, import_jsx_runtime15.jsx)( - MenuContentImpl, - { - id: subContext.contentId, - "aria-labelledby": subContext.triggerId, - ...subContentProps, - ref: composedRefs, - align: "start", - side: rootContext.dir === "rtl" ? "left" : "right", - disableOutsidePointerEvents: false, - disableOutsideScroll: false, - trapFocus: false, - onOpenAutoFocus: (event) => { - if (rootContext.isUsingKeyboardRef.current) ref.current?.focus(); - event.preventDefault(); - }, - onCloseAutoFocus: (event) => event.preventDefault(), - onFocusOutside: composeEventHandlers(props.onFocusOutside, (event) => { - if (event.target !== subContext.trigger) context.onOpenChange(false); - }), - onEscapeKeyDown: composeEventHandlers(props.onEscapeKeyDown, (event) => { - rootContext.onClose(); - event.preventDefault(); - }), - onKeyDown: composeEventHandlers(props.onKeyDown, (event) => { - const isKeyDownInside = event.currentTarget.contains(event.target); - const isCloseKey = SUB_CLOSE_KEYS[rootContext.dir].includes(event.key); - if (isKeyDownInside && isCloseKey) { - context.onOpenChange(false); - subContext.trigger?.focus(); - event.preventDefault(); - } + return (0, import_jsx_runtime15.jsx)(Collection2.Provider, { + scope: props.__scopeMenu, children: (0, import_jsx_runtime15.jsx)(Presence, { + present: forceMount || context.open, children: (0, import_jsx_runtime15.jsx)(Collection2.Slot, { + scope: props.__scopeMenu, children: (0, import_jsx_runtime15.jsx)( + MenuContentImpl, + { + id: subContext.contentId, + "aria-labelledby": subContext.triggerId, + ...subContentProps, + ref: composedRefs, + align: "start", + side: rootContext.dir === "rtl" ? "left" : "right", + disableOutsidePointerEvents: false, + disableOutsideScroll: false, + trapFocus: false, + onOpenAutoFocus: (event) => { + if (rootContext.isUsingKeyboardRef.current) ref.current?.focus(); + event.preventDefault(); + }, + onCloseAutoFocus: (event) => event.preventDefault(), + onFocusOutside: composeEventHandlers(props.onFocusOutside, (event) => { + if (event.target !== subContext.trigger) context.onOpenChange(false); + }), + onEscapeKeyDown: composeEventHandlers(props.onEscapeKeyDown, (event) => { + rootContext.onClose(); + event.preventDefault(); + }), + onKeyDown: composeEventHandlers(props.onKeyDown, (event) => { + const isKeyDownInside = event.currentTarget.contains(event.target); + const isCloseKey = SUB_CLOSE_KEYS[rootContext.dir].includes(event.key); + if (isKeyDownInside && isCloseKey) { + context.onOpenChange(false); + subContext.trigger?.focus(); + event.preventDefault(); + } + }) + } + ) }) - } - ) }) }) }); + }) + }); } ); MenuSubContent.displayName = SUB_CONTENT_NAME; @@ -5336,33 +5358,35 @@ var DropdownMenuTrigger = React35.forwardRef( const { __scopeDropdownMenu, disabled = false, ...triggerProps } = props; const context = useDropdownMenuContext(TRIGGER_NAME, __scopeDropdownMenu); const menuScope = useMenuScope(__scopeDropdownMenu); - return (0, import_jsx_runtime16.jsx)(Anchor2, { asChild: true, ...menuScope, children: (0, import_jsx_runtime16.jsx)( - Primitive.button, - { - type: "button", - id: context.triggerId, - "aria-haspopup": "menu", - "aria-expanded": context.open, - "aria-controls": context.open ? context.contentId : void 0, - "data-state": context.open ? "open" : "closed", - "data-disabled": disabled ? "" : void 0, - disabled, - ...triggerProps, - ref: composeRefs(forwardedRef, context.triggerRef), - onPointerDown: composeEventHandlers(props.onPointerDown, (event) => { - if (!disabled && event.button === 0 && event.ctrlKey === false) { - context.onOpenToggle(); - if (!context.open) event.preventDefault(); - } - }), - onKeyDown: composeEventHandlers(props.onKeyDown, (event) => { - if (disabled) return; - if (["Enter", " "].includes(event.key)) context.onOpenToggle(); - if (event.key === "ArrowDown") context.onOpenChange(true); - if (["Enter", " ", "ArrowDown"].includes(event.key)) event.preventDefault(); - }) - } - ) }); + return (0, import_jsx_runtime16.jsx)(Anchor2, { + asChild: true, ...menuScope, children: (0, import_jsx_runtime16.jsx)( + Primitive.button, + { + type: "button", + id: context.triggerId, + "aria-haspopup": "menu", + "aria-expanded": context.open, + "aria-controls": context.open ? context.contentId : void 0, + "data-state": context.open ? "open" : "closed", + "data-disabled": disabled ? "" : void 0, + disabled, + ...triggerProps, + ref: composeRefs(forwardedRef, context.triggerRef), + onPointerDown: composeEventHandlers(props.onPointerDown, (event) => { + if (!disabled && event.button === 0 && event.ctrlKey === false) { + context.onOpenToggle(); + if (!context.open) event.preventDefault(); + } + }), + onKeyDown: composeEventHandlers(props.onKeyDown, (event) => { + if (disabled) return; + if (["Enter", " "].includes(event.key)) context.onOpenToggle(); + if (event.key === "ArrowDown") context.onOpenChange(true); + if (["Enter", " ", "ArrowDown"].includes(event.key)) event.preventDefault(); + }) + } + ) + }); } ); DropdownMenuTrigger.displayName = TRIGGER_NAME; diff --git a/app/node_modules/.vite/deps/@radix-ui_react-dropdown-menu.js.map b/app/node_modules/.vite/deps/@radix-ui_react-dropdown-menu.js.map deleted file mode 100644 index 6158e38c..00000000 --- a/app/node_modules/.vite/deps/@radix-ui_react-dropdown-menu.js.map +++ /dev/null @@ -1,7 +0,0 @@ -{ - "version": 3, - "sources": ["../../@radix-ui/react-dropdown-menu/src/dropdown-menu.tsx", "../../@radix-ui/primitive/src/primitive.tsx", "../../@radix-ui/react-context/src/create-context.tsx", "../../@radix-ui/react-use-controllable-state/src/use-controllable-state.tsx", "../../@radix-ui/react-use-controllable-state/src/use-controllable-state-reducer.tsx", "../../@radix-ui/react-use-effect-event/src/use-effect-event.tsx", "../../@radix-ui/react-primitive/src/primitive.tsx", "../../@radix-ui/react-primitive/node_modules/@radix-ui/react-slot/src/slot.tsx", "../../@radix-ui/react-menu/src/menu.tsx", "../../@radix-ui/react-collection/src/collection-legacy.tsx", "../../@radix-ui/react-collection/src/collection.tsx", "../../@radix-ui/react-collection/src/ordered-dictionary.ts", "../../@radix-ui/react-collection/node_modules/@radix-ui/react-slot/src/slot.tsx", "../../@radix-ui/react-direction/src/direction.tsx", "../../@radix-ui/react-dismissable-layer/src/dismissable-layer.tsx", "../../@radix-ui/react-use-escape-keydown/src/use-escape-keydown.tsx", "../../@radix-ui/react-focus-guards/src/focus-guards.tsx", "../../@radix-ui/react-focus-scope/src/focus-scope.tsx", "../../@radix-ui/react-id/src/id.tsx", "../../@radix-ui/react-popper/src/popper.tsx", "../../@floating-ui/utils/dist/floating-ui.utils.mjs", "../../@floating-ui/core/dist/floating-ui.core.mjs", "../../@floating-ui/utils/dist/floating-ui.utils.dom.mjs", "../../@floating-ui/dom/dist/floating-ui.dom.mjs", "../../@floating-ui/react-dom/dist/floating-ui.react-dom.mjs", "../../@radix-ui/react-arrow/src/arrow.tsx", "../../@radix-ui/react-use-size/src/use-size.tsx", "../../@radix-ui/react-portal/src/portal.tsx", "../../@radix-ui/react-presence/src/presence.tsx", "../../@radix-ui/react-presence/src/use-state-machine.tsx", "../../@radix-ui/react-roving-focus/src/roving-focus-group.tsx", "../../@radix-ui/react-menu/node_modules/@radix-ui/react-slot/src/slot.tsx", "../../aria-hidden/dist/es2015/index.js", "../../tslib/tslib.es6.mjs", "../../react-remove-scroll/dist/es2015/Combination.js", "../../react-remove-scroll/dist/es2015/UI.js", "../../react-remove-scroll-bar/dist/es2015/constants.js", "../../use-callback-ref/dist/es2015/assignRef.js", "../../use-callback-ref/dist/es2015/useRef.js", "../../use-callback-ref/dist/es2015/useMergeRef.js", "../../use-sidecar/dist/es2015/hoc.js", "../../use-sidecar/dist/es2015/hook.js", "../../use-sidecar/dist/es2015/medium.js", "../../use-sidecar/dist/es2015/renderProp.js", "../../use-sidecar/dist/es2015/exports.js", "../../react-remove-scroll/dist/es2015/medium.js", "../../react-remove-scroll/dist/es2015/SideEffect.js", "../../react-remove-scroll-bar/dist/es2015/component.js", "../../react-style-singleton/dist/es2015/hook.js", "../../get-nonce/dist/es2015/index.js", "../../react-style-singleton/dist/es2015/singleton.js", "../../react-style-singleton/dist/es2015/component.js", "../../react-remove-scroll-bar/dist/es2015/utils.js", "../../react-remove-scroll/dist/es2015/aggresiveCapture.js", "../../react-remove-scroll/dist/es2015/handleScroll.js", "../../react-remove-scroll/dist/es2015/sidecar.js"], - "sourcesContent": ["import * as React from 'react';\nimport { composeEventHandlers } from '@radix-ui/primitive';\nimport { composeRefs } from '@radix-ui/react-compose-refs';\nimport { createContextScope } from '@radix-ui/react-context';\nimport { useControllableState } from '@radix-ui/react-use-controllable-state';\nimport { Primitive } from '@radix-ui/react-primitive';\nimport * as MenuPrimitive from '@radix-ui/react-menu';\nimport { createMenuScope } from '@radix-ui/react-menu';\nimport { useId } from '@radix-ui/react-id';\n\nimport type { Scope } from '@radix-ui/react-context';\n\ntype Direction = 'ltr' | 'rtl';\n\n/* -------------------------------------------------------------------------------------------------\n * DropdownMenu\n * -----------------------------------------------------------------------------------------------*/\n\nconst DROPDOWN_MENU_NAME = 'DropdownMenu';\n\ntype ScopedProps

= P & { __scopeDropdownMenu?: Scope };\nconst [createDropdownMenuContext, createDropdownMenuScope] = createContextScope(\n DROPDOWN_MENU_NAME,\n [createMenuScope]\n);\nconst useMenuScope = createMenuScope();\n\ntype DropdownMenuContextValue = {\n triggerId: string;\n triggerRef: React.RefObject;\n contentId: string;\n open: boolean;\n onOpenChange(open: boolean): void;\n onOpenToggle(): void;\n modal: boolean;\n};\n\nconst [DropdownMenuProvider, useDropdownMenuContext] =\n createDropdownMenuContext(DROPDOWN_MENU_NAME);\n\ninterface DropdownMenuProps {\n children?: React.ReactNode;\n dir?: Direction;\n open?: boolean;\n defaultOpen?: boolean;\n onOpenChange?(open: boolean): void;\n modal?: boolean;\n}\n\nconst DropdownMenu: React.FC = (props: ScopedProps) => {\n const {\n __scopeDropdownMenu,\n children,\n dir,\n open: openProp,\n defaultOpen,\n onOpenChange,\n modal = true,\n } = props;\n const menuScope = useMenuScope(__scopeDropdownMenu);\n const triggerRef = React.useRef(null);\n const [open, setOpen] = useControllableState({\n prop: openProp,\n defaultProp: defaultOpen ?? false,\n onChange: onOpenChange,\n caller: DROPDOWN_MENU_NAME,\n });\n\n return (\n setOpen((prevOpen) => !prevOpen), [setOpen])}\n modal={modal}\n >\n \n {children}\n \n \n );\n};\n\nDropdownMenu.displayName = DROPDOWN_MENU_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * DropdownMenuTrigger\n * -----------------------------------------------------------------------------------------------*/\n\nconst TRIGGER_NAME = 'DropdownMenuTrigger';\n\ntype DropdownMenuTriggerElement = React.ComponentRef;\ntype PrimitiveButtonProps = React.ComponentPropsWithoutRef;\ninterface DropdownMenuTriggerProps extends PrimitiveButtonProps {}\n\nconst DropdownMenuTrigger = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const { __scopeDropdownMenu, disabled = false, ...triggerProps } = props;\n const context = useDropdownMenuContext(TRIGGER_NAME, __scopeDropdownMenu);\n const menuScope = useMenuScope(__scopeDropdownMenu);\n return (\n \n {\n // only call handler if it's the left button (mousedown gets triggered by all mouse buttons)\n // but not when the control key is pressed (avoiding MacOS right click)\n if (!disabled && event.button === 0 && event.ctrlKey === false) {\n context.onOpenToggle();\n // prevent trigger focusing when opening\n // this allows the content to be given focus without competition\n if (!context.open) event.preventDefault();\n }\n })}\n onKeyDown={composeEventHandlers(props.onKeyDown, (event) => {\n if (disabled) return;\n if (['Enter', ' '].includes(event.key)) context.onOpenToggle();\n if (event.key === 'ArrowDown') context.onOpenChange(true);\n // prevent keydown from scrolling window / first focused item to execute\n // that keydown (inadvertently closing the menu)\n if (['Enter', ' ', 'ArrowDown'].includes(event.key)) event.preventDefault();\n })}\n />\n \n );\n }\n);\n\nDropdownMenuTrigger.displayName = TRIGGER_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * DropdownMenuPortal\n * -----------------------------------------------------------------------------------------------*/\n\nconst PORTAL_NAME = 'DropdownMenuPortal';\n\ntype MenuPortalProps = React.ComponentPropsWithoutRef;\ninterface DropdownMenuPortalProps extends MenuPortalProps {}\n\nconst DropdownMenuPortal: React.FC = (\n props: ScopedProps\n) => {\n const { __scopeDropdownMenu, ...portalProps } = props;\n const menuScope = useMenuScope(__scopeDropdownMenu);\n return ;\n};\n\nDropdownMenuPortal.displayName = PORTAL_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * DropdownMenuContent\n * -----------------------------------------------------------------------------------------------*/\n\nconst CONTENT_NAME = 'DropdownMenuContent';\n\ntype DropdownMenuContentElement = React.ComponentRef;\ntype MenuContentProps = React.ComponentPropsWithoutRef;\ninterface DropdownMenuContentProps extends Omit {}\n\nconst DropdownMenuContent = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const { __scopeDropdownMenu, ...contentProps } = props;\n const context = useDropdownMenuContext(CONTENT_NAME, __scopeDropdownMenu);\n const menuScope = useMenuScope(__scopeDropdownMenu);\n const hasInteractedOutsideRef = React.useRef(false);\n\n return (\n {\n if (!hasInteractedOutsideRef.current) context.triggerRef.current?.focus();\n hasInteractedOutsideRef.current = false;\n // Always prevent auto focus because we either focus manually or want user agent focus\n event.preventDefault();\n })}\n onInteractOutside={composeEventHandlers(props.onInteractOutside, (event) => {\n const originalEvent = event.detail.originalEvent as PointerEvent;\n const ctrlLeftClick = originalEvent.button === 0 && originalEvent.ctrlKey === true;\n const isRightClick = originalEvent.button === 2 || ctrlLeftClick;\n if (!context.modal || isRightClick) hasInteractedOutsideRef.current = true;\n })}\n style={{\n ...props.style,\n // re-namespace exposed content custom properties\n ...{\n '--radix-dropdown-menu-content-transform-origin':\n 'var(--radix-popper-transform-origin)',\n '--radix-dropdown-menu-content-available-width': 'var(--radix-popper-available-width)',\n '--radix-dropdown-menu-content-available-height':\n 'var(--radix-popper-available-height)',\n '--radix-dropdown-menu-trigger-width': 'var(--radix-popper-anchor-width)',\n '--radix-dropdown-menu-trigger-height': 'var(--radix-popper-anchor-height)',\n },\n }}\n />\n );\n }\n);\n\nDropdownMenuContent.displayName = CONTENT_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * DropdownMenuGroup\n * -----------------------------------------------------------------------------------------------*/\n\nconst GROUP_NAME = 'DropdownMenuGroup';\n\ntype DropdownMenuGroupElement = React.ComponentRef;\ntype MenuGroupProps = React.ComponentPropsWithoutRef;\ninterface DropdownMenuGroupProps extends MenuGroupProps {}\n\nconst DropdownMenuGroup = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const { __scopeDropdownMenu, ...groupProps } = props;\n const menuScope = useMenuScope(__scopeDropdownMenu);\n return ;\n }\n);\n\nDropdownMenuGroup.displayName = GROUP_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * DropdownMenuLabel\n * -----------------------------------------------------------------------------------------------*/\n\nconst LABEL_NAME = 'DropdownMenuLabel';\n\ntype DropdownMenuLabelElement = React.ComponentRef;\ntype MenuLabelProps = React.ComponentPropsWithoutRef;\ninterface DropdownMenuLabelProps extends MenuLabelProps {}\n\nconst DropdownMenuLabel = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const { __scopeDropdownMenu, ...labelProps } = props;\n const menuScope = useMenuScope(__scopeDropdownMenu);\n return ;\n }\n);\n\nDropdownMenuLabel.displayName = LABEL_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * DropdownMenuItem\n * -----------------------------------------------------------------------------------------------*/\n\nconst ITEM_NAME = 'DropdownMenuItem';\n\ntype DropdownMenuItemElement = React.ComponentRef;\ntype MenuItemProps = React.ComponentPropsWithoutRef;\ninterface DropdownMenuItemProps extends MenuItemProps {}\n\nconst DropdownMenuItem = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const { __scopeDropdownMenu, ...itemProps } = props;\n const menuScope = useMenuScope(__scopeDropdownMenu);\n return ;\n }\n);\n\nDropdownMenuItem.displayName = ITEM_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * DropdownMenuCheckboxItem\n * -----------------------------------------------------------------------------------------------*/\n\nconst CHECKBOX_ITEM_NAME = 'DropdownMenuCheckboxItem';\n\ntype DropdownMenuCheckboxItemElement = React.ComponentRef;\ntype MenuCheckboxItemProps = React.ComponentPropsWithoutRef;\ninterface DropdownMenuCheckboxItemProps extends MenuCheckboxItemProps {}\n\nconst DropdownMenuCheckboxItem = React.forwardRef<\n DropdownMenuCheckboxItemElement,\n DropdownMenuCheckboxItemProps\n>((props: ScopedProps, forwardedRef) => {\n const { __scopeDropdownMenu, ...checkboxItemProps } = props;\n const menuScope = useMenuScope(__scopeDropdownMenu);\n return ;\n});\n\nDropdownMenuCheckboxItem.displayName = CHECKBOX_ITEM_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * DropdownMenuRadioGroup\n * -----------------------------------------------------------------------------------------------*/\n\nconst RADIO_GROUP_NAME = 'DropdownMenuRadioGroup';\n\ntype DropdownMenuRadioGroupElement = React.ComponentRef;\ntype MenuRadioGroupProps = React.ComponentPropsWithoutRef;\ninterface DropdownMenuRadioGroupProps extends MenuRadioGroupProps {}\n\nconst DropdownMenuRadioGroup = React.forwardRef<\n DropdownMenuRadioGroupElement,\n DropdownMenuRadioGroupProps\n>((props: ScopedProps, forwardedRef) => {\n const { __scopeDropdownMenu, ...radioGroupProps } = props;\n const menuScope = useMenuScope(__scopeDropdownMenu);\n return ;\n});\n\nDropdownMenuRadioGroup.displayName = RADIO_GROUP_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * DropdownMenuRadioItem\n * -----------------------------------------------------------------------------------------------*/\n\nconst RADIO_ITEM_NAME = 'DropdownMenuRadioItem';\n\ntype DropdownMenuRadioItemElement = React.ComponentRef;\ntype MenuRadioItemProps = React.ComponentPropsWithoutRef;\ninterface DropdownMenuRadioItemProps extends MenuRadioItemProps {}\n\nconst DropdownMenuRadioItem = React.forwardRef<\n DropdownMenuRadioItemElement,\n DropdownMenuRadioItemProps\n>((props: ScopedProps, forwardedRef) => {\n const { __scopeDropdownMenu, ...radioItemProps } = props;\n const menuScope = useMenuScope(__scopeDropdownMenu);\n return ;\n});\n\nDropdownMenuRadioItem.displayName = RADIO_ITEM_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * DropdownMenuItemIndicator\n * -----------------------------------------------------------------------------------------------*/\n\nconst INDICATOR_NAME = 'DropdownMenuItemIndicator';\n\ntype DropdownMenuItemIndicatorElement = React.ComponentRef;\ntype MenuItemIndicatorProps = React.ComponentPropsWithoutRef;\ninterface DropdownMenuItemIndicatorProps extends MenuItemIndicatorProps {}\n\nconst DropdownMenuItemIndicator = React.forwardRef<\n DropdownMenuItemIndicatorElement,\n DropdownMenuItemIndicatorProps\n>((props: ScopedProps, forwardedRef) => {\n const { __scopeDropdownMenu, ...itemIndicatorProps } = props;\n const menuScope = useMenuScope(__scopeDropdownMenu);\n return ;\n});\n\nDropdownMenuItemIndicator.displayName = INDICATOR_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * DropdownMenuSeparator\n * -----------------------------------------------------------------------------------------------*/\n\nconst SEPARATOR_NAME = 'DropdownMenuSeparator';\n\ntype DropdownMenuSeparatorElement = React.ComponentRef;\ntype MenuSeparatorProps = React.ComponentPropsWithoutRef;\ninterface DropdownMenuSeparatorProps extends MenuSeparatorProps {}\n\nconst DropdownMenuSeparator = React.forwardRef<\n DropdownMenuSeparatorElement,\n DropdownMenuSeparatorProps\n>((props: ScopedProps, forwardedRef) => {\n const { __scopeDropdownMenu, ...separatorProps } = props;\n const menuScope = useMenuScope(__scopeDropdownMenu);\n return ;\n});\n\nDropdownMenuSeparator.displayName = SEPARATOR_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * DropdownMenuArrow\n * -----------------------------------------------------------------------------------------------*/\n\nconst ARROW_NAME = 'DropdownMenuArrow';\n\ntype DropdownMenuArrowElement = React.ComponentRef;\ntype MenuArrowProps = React.ComponentPropsWithoutRef;\ninterface DropdownMenuArrowProps extends MenuArrowProps {}\n\nconst DropdownMenuArrow = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const { __scopeDropdownMenu, ...arrowProps } = props;\n const menuScope = useMenuScope(__scopeDropdownMenu);\n return ;\n }\n);\n\nDropdownMenuArrow.displayName = ARROW_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * DropdownMenuSub\n * -----------------------------------------------------------------------------------------------*/\n\ninterface DropdownMenuSubProps {\n children?: React.ReactNode;\n open?: boolean;\n defaultOpen?: boolean;\n onOpenChange?(open: boolean): void;\n}\n\nconst DropdownMenuSub: React.FC = (\n props: ScopedProps\n) => {\n const { __scopeDropdownMenu, children, open: openProp, onOpenChange, defaultOpen } = props;\n const menuScope = useMenuScope(__scopeDropdownMenu);\n const [open, setOpen] = useControllableState({\n prop: openProp,\n defaultProp: defaultOpen ?? false,\n onChange: onOpenChange,\n caller: 'DropdownMenuSub',\n });\n\n return (\n \n {children}\n \n );\n};\n\n/* -------------------------------------------------------------------------------------------------\n * DropdownMenuSubTrigger\n * -----------------------------------------------------------------------------------------------*/\n\nconst SUB_TRIGGER_NAME = 'DropdownMenuSubTrigger';\n\ntype DropdownMenuSubTriggerElement = React.ComponentRef;\ntype MenuSubTriggerProps = React.ComponentPropsWithoutRef;\ninterface DropdownMenuSubTriggerProps extends MenuSubTriggerProps {}\n\nconst DropdownMenuSubTrigger = React.forwardRef<\n DropdownMenuSubTriggerElement,\n DropdownMenuSubTriggerProps\n>((props: ScopedProps, forwardedRef) => {\n const { __scopeDropdownMenu, ...subTriggerProps } = props;\n const menuScope = useMenuScope(__scopeDropdownMenu);\n return ;\n});\n\nDropdownMenuSubTrigger.displayName = SUB_TRIGGER_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * DropdownMenuSubContent\n * -----------------------------------------------------------------------------------------------*/\n\nconst SUB_CONTENT_NAME = 'DropdownMenuSubContent';\n\ntype DropdownMenuSubContentElement = React.ComponentRef;\ntype MenuSubContentProps = React.ComponentPropsWithoutRef;\ninterface DropdownMenuSubContentProps extends MenuSubContentProps {}\n\nconst DropdownMenuSubContent = React.forwardRef<\n DropdownMenuSubContentElement,\n DropdownMenuSubContentProps\n>((props: ScopedProps, forwardedRef) => {\n const { __scopeDropdownMenu, ...subContentProps } = props;\n const menuScope = useMenuScope(__scopeDropdownMenu);\n\n return (\n \n );\n});\n\nDropdownMenuSubContent.displayName = SUB_CONTENT_NAME;\n\n/* -----------------------------------------------------------------------------------------------*/\n\nconst Root = DropdownMenu;\nconst Trigger = DropdownMenuTrigger;\nconst Portal = DropdownMenuPortal;\nconst Content = DropdownMenuContent;\nconst Group = DropdownMenuGroup;\nconst Label = DropdownMenuLabel;\nconst Item = DropdownMenuItem;\nconst CheckboxItem = DropdownMenuCheckboxItem;\nconst RadioGroup = DropdownMenuRadioGroup;\nconst RadioItem = DropdownMenuRadioItem;\nconst ItemIndicator = DropdownMenuItemIndicator;\nconst Separator = DropdownMenuSeparator;\nconst Arrow = DropdownMenuArrow;\nconst Sub = DropdownMenuSub;\nconst SubTrigger = DropdownMenuSubTrigger;\nconst SubContent = DropdownMenuSubContent;\n\nexport {\n createDropdownMenuScope,\n //\n DropdownMenu,\n DropdownMenuTrigger,\n DropdownMenuPortal,\n DropdownMenuContent,\n DropdownMenuGroup,\n DropdownMenuLabel,\n DropdownMenuItem,\n DropdownMenuCheckboxItem,\n DropdownMenuRadioGroup,\n DropdownMenuRadioItem,\n DropdownMenuItemIndicator,\n DropdownMenuSeparator,\n DropdownMenuArrow,\n DropdownMenuSub,\n DropdownMenuSubTrigger,\n DropdownMenuSubContent,\n //\n Root,\n Trigger,\n Portal,\n Content,\n Group,\n Label,\n Item,\n CheckboxItem,\n RadioGroup,\n RadioItem,\n ItemIndicator,\n Separator,\n Arrow,\n Sub,\n SubTrigger,\n SubContent,\n};\nexport type {\n DropdownMenuProps,\n DropdownMenuTriggerProps,\n DropdownMenuPortalProps,\n DropdownMenuContentProps,\n DropdownMenuGroupProps,\n DropdownMenuLabelProps,\n DropdownMenuItemProps,\n DropdownMenuCheckboxItemProps,\n DropdownMenuRadioGroupProps,\n DropdownMenuRadioItemProps,\n DropdownMenuItemIndicatorProps,\n DropdownMenuSeparatorProps,\n DropdownMenuArrowProps,\n DropdownMenuSubProps,\n DropdownMenuSubTriggerProps,\n DropdownMenuSubContentProps,\n};\n", "/* eslint-disable no-restricted-properties */\n\n/* eslint-disable no-restricted-globals */\nexport const canUseDOM = !!(\n typeof window !== 'undefined' &&\n window.document &&\n window.document.createElement\n);\n/* eslint-enable no-restricted-globals */\n\nexport function composeEventHandlers(\n originalEventHandler?: (event: E) => void,\n ourEventHandler?: (event: E) => void,\n { checkForDefaultPrevented = true } = {}\n) {\n return function handleEvent(event: E) {\n originalEventHandler?.(event);\n\n if (checkForDefaultPrevented === false || !event.defaultPrevented) {\n return ourEventHandler?.(event);\n }\n };\n}\n\nexport function getOwnerWindow(element: Node | null | undefined) {\n if (!canUseDOM) {\n throw new Error('Cannot access window outside of the DOM');\n }\n // eslint-disable-next-line no-restricted-globals\n return element?.ownerDocument?.defaultView ?? window;\n}\n\nexport function getOwnerDocument(element: Node | null | undefined) {\n if (!canUseDOM) {\n throw new Error('Cannot access document outside of the DOM');\n }\n // eslint-disable-next-line no-restricted-globals\n return element?.ownerDocument ?? document;\n}\n\n/**\n * Lifted from https://github.com/ariakit/ariakit/blob/main/packages/ariakit-core/src/utils/dom.ts#L37\n * MIT License, Copyright (c) AriaKit.\n */\nexport function getActiveElement(\n node: Node | null | undefined,\n activeDescendant = false\n): HTMLElement | null {\n const { activeElement } = getOwnerDocument(node);\n if (!activeElement?.nodeName) {\n // `activeElement` might be an empty object if we're interacting with elements\n // inside of an iframe.\n return null;\n }\n\n if (isFrame(activeElement) && activeElement.contentDocument) {\n return getActiveElement(activeElement.contentDocument.body, activeDescendant);\n }\n\n if (activeDescendant) {\n const id = activeElement.getAttribute('aria-activedescendant');\n if (id) {\n const element = getOwnerDocument(activeElement).getElementById(id);\n if (element) {\n return element;\n }\n }\n }\n\n return activeElement as HTMLElement | null;\n}\n\nexport function isFrame(element: Element): element is HTMLIFrameElement {\n return element.tagName === 'IFRAME';\n}\n", "import * as React from 'react';\n\nfunction createContext(\n rootComponentName: string,\n defaultContext?: ContextValueType\n) {\n const Context = React.createContext(defaultContext);\n\n const Provider: React.FC = (props) => {\n const { children, ...context } = props;\n // Only re-memoize when prop values change\n // eslint-disable-next-line react-hooks/exhaustive-deps\n const value = React.useMemo(() => context, Object.values(context)) as ContextValueType;\n return {children};\n };\n\n Provider.displayName = rootComponentName + 'Provider';\n\n function useContext(consumerName: string) {\n const context = React.useContext(Context);\n if (context) return context;\n if (defaultContext !== undefined) return defaultContext;\n // if a defaultContext wasn't specified, it's a required context.\n throw new Error(`\\`${consumerName}\\` must be used within \\`${rootComponentName}\\``);\n }\n\n return [Provider, useContext] as const;\n}\n\n/* -------------------------------------------------------------------------------------------------\n * createContextScope\n * -----------------------------------------------------------------------------------------------*/\n\ntype Scope = { [scopeName: string]: React.Context[] } | undefined;\ntype ScopeHook = (scope: Scope) => { [__scopeProp: string]: Scope };\ninterface CreateScope {\n scopeName: string;\n (): ScopeHook;\n}\n\nfunction createContextScope(scopeName: string, createContextScopeDeps: CreateScope[] = []) {\n let defaultContexts: any[] = [];\n\n /* -----------------------------------------------------------------------------------------------\n * createContext\n * ---------------------------------------------------------------------------------------------*/\n\n function createContext(\n rootComponentName: string,\n defaultContext?: ContextValueType\n ) {\n const BaseContext = React.createContext(defaultContext);\n const index = defaultContexts.length;\n defaultContexts = [...defaultContexts, defaultContext];\n\n const Provider: React.FC<\n ContextValueType & { scope: Scope; children: React.ReactNode }\n > = (props) => {\n const { scope, children, ...context } = props;\n const Context = scope?.[scopeName]?.[index] || BaseContext;\n // Only re-memoize when prop values change\n // eslint-disable-next-line react-hooks/exhaustive-deps\n const value = React.useMemo(() => context, Object.values(context)) as ContextValueType;\n return {children};\n };\n\n Provider.displayName = rootComponentName + 'Provider';\n\n function useContext(consumerName: string, scope: Scope) {\n const Context = scope?.[scopeName]?.[index] || BaseContext;\n const context = React.useContext(Context);\n if (context) return context;\n if (defaultContext !== undefined) return defaultContext;\n // if a defaultContext wasn't specified, it's a required context.\n throw new Error(`\\`${consumerName}\\` must be used within \\`${rootComponentName}\\``);\n }\n\n return [Provider, useContext] as const;\n }\n\n /* -----------------------------------------------------------------------------------------------\n * createScope\n * ---------------------------------------------------------------------------------------------*/\n\n const createScope: CreateScope = () => {\n const scopeContexts = defaultContexts.map((defaultContext) => {\n return React.createContext(defaultContext);\n });\n return function useScope(scope: Scope) {\n const contexts = scope?.[scopeName] || scopeContexts;\n return React.useMemo(\n () => ({ [`__scope${scopeName}`]: { ...scope, [scopeName]: contexts } }),\n [scope, contexts]\n );\n };\n };\n\n createScope.scopeName = scopeName;\n return [createContext, composeContextScopes(createScope, ...createContextScopeDeps)] as const;\n}\n\n/* -------------------------------------------------------------------------------------------------\n * composeContextScopes\n * -----------------------------------------------------------------------------------------------*/\n\nfunction composeContextScopes(...scopes: CreateScope[]) {\n const baseScope = scopes[0];\n if (scopes.length === 1) return baseScope;\n\n const createScope: CreateScope = () => {\n const scopeHooks = scopes.map((createScope) => ({\n useScope: createScope(),\n scopeName: createScope.scopeName,\n }));\n\n return function useComposedScopes(overrideScopes) {\n const nextScopes = scopeHooks.reduce((nextScopes, { useScope, scopeName }) => {\n // We are calling a hook inside a callback which React warns against to avoid inconsistent\n // renders, however, scoping doesn't have render side effects so we ignore the rule.\n // eslint-disable-next-line react-hooks/rules-of-hooks\n const scopeProps = useScope(overrideScopes);\n const currentScope = scopeProps[`__scope${scopeName}`];\n return { ...nextScopes, ...currentScope };\n }, {});\n\n return React.useMemo(() => ({ [`__scope${baseScope.scopeName}`]: nextScopes }), [nextScopes]);\n };\n };\n\n createScope.scopeName = baseScope.scopeName;\n return createScope;\n}\n\n/* -----------------------------------------------------------------------------------------------*/\n\nexport { createContext, createContextScope };\nexport type { CreateScope, Scope };\n", "import * as React from 'react';\nimport { useLayoutEffect } from '@radix-ui/react-use-layout-effect';\n\n// Prevent bundlers from trying to optimize the import\nconst useInsertionEffect: typeof useLayoutEffect =\n (React as any)[' useInsertionEffect '.trim().toString()] || useLayoutEffect;\n\ntype ChangeHandler = (state: T) => void;\ntype SetStateFn = React.Dispatch>;\n\ninterface UseControllableStateParams {\n prop?: T | undefined;\n defaultProp: T;\n onChange?: ChangeHandler;\n caller?: string;\n}\n\nexport function useControllableState({\n prop,\n defaultProp,\n onChange = () => {},\n caller,\n}: UseControllableStateParams): [T, SetStateFn] {\n const [uncontrolledProp, setUncontrolledProp, onChangeRef] = useUncontrolledState({\n defaultProp,\n onChange,\n });\n const isControlled = prop !== undefined;\n const value = isControlled ? prop : uncontrolledProp;\n\n // OK to disable conditionally calling hooks here because they will always run\n // consistently in the same environment. Bundlers should be able to remove the\n // code block entirely in production.\n /* eslint-disable react-hooks/rules-of-hooks */\n if (process.env.NODE_ENV !== 'production') {\n const isControlledRef = React.useRef(prop !== undefined);\n React.useEffect(() => {\n const wasControlled = isControlledRef.current;\n if (wasControlled !== isControlled) {\n const from = wasControlled ? 'controlled' : 'uncontrolled';\n const to = isControlled ? 'controlled' : 'uncontrolled';\n console.warn(\n `${caller} is changing from ${from} to ${to}. Components should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled value for the lifetime of the component.`\n );\n }\n isControlledRef.current = isControlled;\n }, [isControlled, caller]);\n }\n /* eslint-enable react-hooks/rules-of-hooks */\n\n const setValue = React.useCallback>(\n (nextValue) => {\n if (isControlled) {\n const value = isFunction(nextValue) ? nextValue(prop) : nextValue;\n if (value !== prop) {\n onChangeRef.current?.(value);\n }\n } else {\n setUncontrolledProp(nextValue);\n }\n },\n [isControlled, prop, setUncontrolledProp, onChangeRef]\n );\n\n return [value, setValue];\n}\n\nfunction useUncontrolledState({\n defaultProp,\n onChange,\n}: Omit, 'prop'>): [\n Value: T,\n setValue: React.Dispatch>,\n OnChangeRef: React.RefObject | undefined>,\n] {\n const [value, setValue] = React.useState(defaultProp);\n const prevValueRef = React.useRef(value);\n\n const onChangeRef = React.useRef(onChange);\n useInsertionEffect(() => {\n onChangeRef.current = onChange;\n }, [onChange]);\n\n React.useEffect(() => {\n if (prevValueRef.current !== value) {\n onChangeRef.current?.(value);\n prevValueRef.current = value;\n }\n }, [value, prevValueRef]);\n\n return [value, setValue, onChangeRef];\n}\n\nfunction isFunction(value: unknown): value is (...args: any[]) => any {\n return typeof value === 'function';\n}\n", "import * as React from 'react';\nimport { useEffectEvent } from '@radix-ui/react-use-effect-event';\n\ntype ChangeHandler = (state: T) => void;\n\ninterface UseControllableStateParams {\n prop: T | undefined;\n defaultProp: T;\n onChange: ChangeHandler | undefined;\n caller: string;\n}\n\ninterface AnyAction {\n type: string;\n}\n\nconst SYNC_STATE = Symbol('RADIX:SYNC_STATE');\n\ninterface SyncStateAction {\n type: typeof SYNC_STATE;\n state: T;\n}\n\nexport function useControllableStateReducer(\n reducer: (prevState: S & { state: T }, action: A) => S & { state: T },\n userArgs: UseControllableStateParams,\n initialState: S\n): [S & { state: T }, React.Dispatch];\n\nexport function useControllableStateReducer(\n reducer: (prevState: S & { state: T }, action: A) => S & { state: T },\n userArgs: UseControllableStateParams,\n initialArg: I,\n init: (i: I & { state: T }) => S\n): [S & { state: T }, React.Dispatch];\n\nexport function useControllableStateReducer(\n reducer: (prevState: S & { state: T }, action: A) => S & { state: T },\n userArgs: UseControllableStateParams,\n initialArg: any,\n init?: (i: any) => Omit\n): [S & { state: T }, React.Dispatch] {\n const { prop: controlledState, defaultProp, onChange: onChangeProp, caller } = userArgs;\n const isControlled = controlledState !== undefined;\n\n const onChange = useEffectEvent(onChangeProp);\n\n // OK to disable conditionally calling hooks here because they will always run\n // consistently in the same environment. Bundlers should be able to remove the\n // code block entirely in production.\n /* eslint-disable react-hooks/rules-of-hooks */\n if (process.env.NODE_ENV !== 'production') {\n const isControlledRef = React.useRef(controlledState !== undefined);\n React.useEffect(() => {\n const wasControlled = isControlledRef.current;\n if (wasControlled !== isControlled) {\n const from = wasControlled ? 'controlled' : 'uncontrolled';\n const to = isControlled ? 'controlled' : 'uncontrolled';\n console.warn(\n `${caller} is changing from ${from} to ${to}. Components should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled value for the lifetime of the component.`\n );\n }\n isControlledRef.current = isControlled;\n }, [isControlled, caller]);\n }\n /* eslint-enable react-hooks/rules-of-hooks */\n\n type InternalState = S & { state: T };\n const args: [InternalState] = [{ ...initialArg, state: defaultProp }];\n if (init) {\n // @ts-expect-error\n args.push(init);\n }\n\n const [internalState, dispatch] = React.useReducer(\n (state: InternalState, action: A | SyncStateAction): InternalState => {\n if (action.type === SYNC_STATE) {\n return { ...state, state: action.state };\n }\n\n const next = reducer(state, action);\n if (isControlled && !Object.is(next.state, state.state)) {\n onChange(next.state);\n }\n return next;\n },\n ...args\n );\n\n const uncontrolledState = internalState.state;\n const prevValueRef = React.useRef(uncontrolledState);\n React.useEffect(() => {\n if (prevValueRef.current !== uncontrolledState) {\n prevValueRef.current = uncontrolledState;\n if (!isControlled) {\n onChange(uncontrolledState);\n }\n }\n }, [onChange, uncontrolledState, prevValueRef, isControlled]);\n\n const state = React.useMemo(() => {\n const isControlled = controlledState !== undefined;\n if (isControlled) {\n return { ...internalState, state: controlledState };\n }\n\n return internalState;\n }, [internalState, controlledState]);\n\n React.useEffect(() => {\n // Sync internal state for controlled components so that reducer is called\n // with the correct state values\n if (isControlled && !Object.is(controlledState, internalState.state)) {\n dispatch({ type: SYNC_STATE, state: controlledState });\n }\n }, [controlledState, internalState.state, isControlled]);\n\n return [state, dispatch as React.Dispatch];\n}\n", "/* eslint-disable react-hooks/rules-of-hooks */\nimport { useLayoutEffect } from '@radix-ui/react-use-layout-effect';\nimport * as React from 'react';\n\ntype AnyFunction = (...args: any[]) => any;\n\n// See https://github.com/webpack/webpack/issues/14814\nconst useReactEffectEvent = (React as any)[' useEffectEvent '.trim().toString()];\nconst useReactInsertionEffect = (React as any)[' useInsertionEffect '.trim().toString()];\n\n/**\n * Designed to approximate the behavior on `experimental_useEffectEvent` as best\n * as possible until its stable release, and back-fill it as a shim as needed.\n */\nexport function useEffectEvent(callback?: T): T {\n if (typeof useReactEffectEvent === 'function') {\n return useReactEffectEvent(callback);\n }\n\n const ref = React.useRef(() => {\n throw new Error('Cannot call an event handler while rendering.');\n });\n // See https://github.com/webpack/webpack/issues/14814\n if (typeof useReactInsertionEffect === 'function') {\n useReactInsertionEffect(() => {\n ref.current = callback;\n });\n } else {\n useLayoutEffect(() => {\n ref.current = callback;\n });\n }\n\n // https://github.com/facebook/react/issues/19240\n return React.useMemo(() => ((...args) => ref.current?.(...args)) as T, []);\n}\n", "import * as React from 'react';\nimport * as ReactDOM from 'react-dom';\nimport { createSlot } from '@radix-ui/react-slot';\n\nconst NODES = [\n 'a',\n 'button',\n 'div',\n 'form',\n 'h2',\n 'h3',\n 'img',\n 'input',\n 'label',\n 'li',\n 'nav',\n 'ol',\n 'p',\n 'select',\n 'span',\n 'svg',\n 'ul',\n] as const;\n\ntype Primitives = { [E in (typeof NODES)[number]]: PrimitiveForwardRefComponent };\ntype PrimitivePropsWithRef = React.ComponentPropsWithRef & {\n asChild?: boolean;\n};\n\ninterface PrimitiveForwardRefComponent\n extends React.ForwardRefExoticComponent> {}\n\n/* -------------------------------------------------------------------------------------------------\n * Primitive\n * -----------------------------------------------------------------------------------------------*/\n\nconst Primitive = NODES.reduce((primitive, node) => {\n const Slot = createSlot(`Primitive.${node}`);\n const Node = React.forwardRef((props: PrimitivePropsWithRef, forwardedRef: any) => {\n const { asChild, ...primitiveProps } = props;\n const Comp: any = asChild ? Slot : node;\n\n if (typeof window !== 'undefined') {\n (window as any)[Symbol.for('radix-ui')] = true;\n }\n\n return ;\n });\n\n Node.displayName = `Primitive.${node}`;\n\n return { ...primitive, [node]: Node };\n}, {} as Primitives);\n\n/* -------------------------------------------------------------------------------------------------\n * Utils\n * -----------------------------------------------------------------------------------------------*/\n\n/**\n * Flush custom event dispatch\n * https://github.com/radix-ui/primitives/pull/1378\n *\n * React batches *all* event handlers since version 18, this introduces certain considerations when using custom event types.\n *\n * Internally, React prioritises events in the following order:\n * - discrete\n * - continuous\n * - default\n *\n * https://github.com/facebook/react/blob/a8a4742f1c54493df00da648a3f9d26e3db9c8b5/packages/react-dom/src/events/ReactDOMEventListener.js#L294-L350\n *\n * `discrete` is an important distinction as updates within these events are applied immediately.\n * React however, is not able to infer the priority of custom event types due to how they are detected internally.\n * Because of this, it's possible for updates from custom events to be unexpectedly batched when\n * dispatched by another `discrete` event.\n *\n * In order to ensure that updates from custom events are applied predictably, we need to manually flush the batch.\n * This utility should be used when dispatching a custom event from within another `discrete` event, this utility\n * is not necessary when dispatching known event types, or if dispatching a custom type inside a non-discrete event.\n * For example:\n *\n * dispatching a known click \uD83D\uDC4E\n * target.dispatchEvent(new Event(\u2018click\u2019))\n *\n * dispatching a custom type within a non-discrete event \uD83D\uDC4E\n * onScroll={(event) => event.target.dispatchEvent(new CustomEvent(\u2018customType\u2019))}\n *\n * dispatching a custom type within a `discrete` event \uD83D\uDC4D\n * onPointerDown={(event) => dispatchDiscreteCustomEvent(event.target, new CustomEvent(\u2018customType\u2019))}\n *\n * Note: though React classifies `focus`, `focusin` and `focusout` events as `discrete`, it's not recommended to use\n * this utility with them. This is because it's possible for those handlers to be called implicitly during render\n * e.g. when focus is within a component as it is unmounted, or when managing focus on mount.\n */\n\nfunction dispatchDiscreteCustomEvent(target: E['target'], event: E) {\n if (target) ReactDOM.flushSync(() => target.dispatchEvent(event));\n}\n\n/* -----------------------------------------------------------------------------------------------*/\n\nconst Root = Primitive;\n\nexport {\n Primitive,\n //\n Root,\n //\n dispatchDiscreteCustomEvent,\n};\nexport type { PrimitivePropsWithRef };\n", "import * as React from 'react';\nimport { composeRefs } from '@radix-ui/react-compose-refs';\n\n/* -------------------------------------------------------------------------------------------------\n * Slot\n * -----------------------------------------------------------------------------------------------*/\n\ninterface SlotProps extends React.HTMLAttributes {\n children?: React.ReactNode;\n}\n\n/* @__NO_SIDE_EFFECTS__ */ export function createSlot(ownerName: string) {\n const SlotClone = createSlotClone(ownerName);\n const Slot = React.forwardRef((props, forwardedRef) => {\n const { children, ...slotProps } = props;\n const childrenArray = React.Children.toArray(children);\n const slottable = childrenArray.find(isSlottable);\n\n if (slottable) {\n // the new element to render is the one passed as a child of `Slottable`\n const newElement = slottable.props.children;\n\n const newChildren = childrenArray.map((child) => {\n if (child === slottable) {\n // because the new element will be the one rendered, we are only interested\n // in grabbing its children (`newElement.props.children`)\n if (React.Children.count(newElement) > 1) return React.Children.only(null);\n return React.isValidElement(newElement)\n ? (newElement.props as { children: React.ReactNode }).children\n : null;\n } else {\n return child;\n }\n });\n\n return (\n \n {React.isValidElement(newElement)\n ? React.cloneElement(newElement, undefined, newChildren)\n : null}\n \n );\n }\n\n return (\n \n {children}\n \n );\n });\n\n Slot.displayName = `${ownerName}.Slot`;\n return Slot;\n}\n\nconst Slot = createSlot('Slot');\n\n/* -------------------------------------------------------------------------------------------------\n * SlotClone\n * -----------------------------------------------------------------------------------------------*/\n\ninterface SlotCloneProps {\n children: React.ReactNode;\n}\n\n/* @__NO_SIDE_EFFECTS__ */ function createSlotClone(ownerName: string) {\n const SlotClone = React.forwardRef((props, forwardedRef) => {\n const { children, ...slotProps } = props;\n\n if (React.isValidElement(children)) {\n const childrenRef = getElementRef(children);\n const props = mergeProps(slotProps, children.props as AnyProps);\n // do not pass ref to React.Fragment for React 19 compatibility\n if (children.type !== React.Fragment) {\n props.ref = forwardedRef ? composeRefs(forwardedRef, childrenRef) : childrenRef;\n }\n return React.cloneElement(children, props);\n }\n\n return React.Children.count(children) > 1 ? React.Children.only(null) : null;\n });\n\n SlotClone.displayName = `${ownerName}.SlotClone`;\n return SlotClone;\n}\n\n/* -------------------------------------------------------------------------------------------------\n * Slottable\n * -----------------------------------------------------------------------------------------------*/\n\nconst SLOTTABLE_IDENTIFIER = Symbol('radix.slottable');\n\ninterface SlottableProps {\n children: React.ReactNode;\n}\n\ninterface SlottableComponent extends React.FC {\n __radixId: symbol;\n}\n\n/* @__NO_SIDE_EFFECTS__ */ export function createSlottable(ownerName: string) {\n const Slottable: SlottableComponent = ({ children }) => {\n return <>{children};\n };\n Slottable.displayName = `${ownerName}.Slottable`;\n Slottable.__radixId = SLOTTABLE_IDENTIFIER;\n return Slottable;\n}\n\nconst Slottable = createSlottable('Slottable');\n\n/* ---------------------------------------------------------------------------------------------- */\n\ntype AnyProps = Record;\n\nfunction isSlottable(\n child: React.ReactNode\n): child is React.ReactElement {\n return (\n React.isValidElement(child) &&\n typeof child.type === 'function' &&\n '__radixId' in child.type &&\n child.type.__radixId === SLOTTABLE_IDENTIFIER\n );\n}\n\nfunction mergeProps(slotProps: AnyProps, childProps: AnyProps) {\n // all child props should override\n const overrideProps = { ...childProps };\n\n for (const propName in childProps) {\n const slotPropValue = slotProps[propName];\n const childPropValue = childProps[propName];\n\n const isHandler = /^on[A-Z]/.test(propName);\n if (isHandler) {\n // if the handler exists on both, we compose them\n if (slotPropValue && childPropValue) {\n overrideProps[propName] = (...args: unknown[]) => {\n const result = childPropValue(...args);\n slotPropValue(...args);\n return result;\n };\n }\n // but if it exists only on the slot, we use only this one\n else if (slotPropValue) {\n overrideProps[propName] = slotPropValue;\n }\n }\n // if it's `style`, we merge them\n else if (propName === 'style') {\n overrideProps[propName] = { ...slotPropValue, ...childPropValue };\n } else if (propName === 'className') {\n overrideProps[propName] = [slotPropValue, childPropValue].filter(Boolean).join(' ');\n }\n }\n\n return { ...slotProps, ...overrideProps };\n}\n\n// Before React 19 accessing `element.props.ref` will throw a warning and suggest using `element.ref`\n// After React 19 accessing `element.ref` does the opposite.\n// https://github.com/facebook/react/pull/28348\n//\n// Access the ref using the method that doesn't yield a warning.\nfunction getElementRef(element: React.ReactElement) {\n // React <=18 in DEV\n let getter = Object.getOwnPropertyDescriptor(element.props, 'ref')?.get;\n let mayWarn = getter && 'isReactWarning' in getter && getter.isReactWarning;\n if (mayWarn) {\n return (element as any).ref;\n }\n\n // React 19 in DEV\n getter = Object.getOwnPropertyDescriptor(element, 'ref')?.get;\n mayWarn = getter && 'isReactWarning' in getter && getter.isReactWarning;\n if (mayWarn) {\n return (element.props as { ref?: React.Ref }).ref;\n }\n\n // Not DEV\n return (element.props as { ref?: React.Ref }).ref || (element as any).ref;\n}\n\nexport {\n Slot,\n Slottable,\n //\n Slot as Root,\n};\nexport type { SlotProps };\n", "import * as React from 'react';\nimport { composeEventHandlers } from '@radix-ui/primitive';\nimport { createCollection } from '@radix-ui/react-collection';\nimport { useComposedRefs, composeRefs } from '@radix-ui/react-compose-refs';\nimport { createContextScope } from '@radix-ui/react-context';\nimport { useDirection } from '@radix-ui/react-direction';\nimport { DismissableLayer } from '@radix-ui/react-dismissable-layer';\nimport { useFocusGuards } from '@radix-ui/react-focus-guards';\nimport { FocusScope } from '@radix-ui/react-focus-scope';\nimport { useId } from '@radix-ui/react-id';\nimport * as PopperPrimitive from '@radix-ui/react-popper';\nimport { createPopperScope } from '@radix-ui/react-popper';\nimport { Portal as PortalPrimitive } from '@radix-ui/react-portal';\nimport { Presence } from '@radix-ui/react-presence';\nimport { Primitive, dispatchDiscreteCustomEvent } from '@radix-ui/react-primitive';\nimport * as RovingFocusGroup from '@radix-ui/react-roving-focus';\nimport { createRovingFocusGroupScope } from '@radix-ui/react-roving-focus';\nimport { createSlot } from '@radix-ui/react-slot';\nimport { useCallbackRef } from '@radix-ui/react-use-callback-ref';\nimport { hideOthers } from 'aria-hidden';\nimport { RemoveScroll } from 'react-remove-scroll';\n\nimport type { Scope } from '@radix-ui/react-context';\n\ntype Direction = 'ltr' | 'rtl';\n\nconst SELECTION_KEYS = ['Enter', ' '];\nconst FIRST_KEYS = ['ArrowDown', 'PageUp', 'Home'];\nconst LAST_KEYS = ['ArrowUp', 'PageDown', 'End'];\nconst FIRST_LAST_KEYS = [...FIRST_KEYS, ...LAST_KEYS];\nconst SUB_OPEN_KEYS: Record = {\n ltr: [...SELECTION_KEYS, 'ArrowRight'],\n rtl: [...SELECTION_KEYS, 'ArrowLeft'],\n};\nconst SUB_CLOSE_KEYS: Record = {\n ltr: ['ArrowLeft'],\n rtl: ['ArrowRight'],\n};\n\n/* -------------------------------------------------------------------------------------------------\n * Menu\n * -----------------------------------------------------------------------------------------------*/\n\nconst MENU_NAME = 'Menu';\n\ntype ItemData = { disabled: boolean; textValue: string };\nconst [Collection, useCollection, createCollectionScope] = createCollection<\n MenuItemElement,\n ItemData\n>(MENU_NAME);\n\ntype ScopedProps

= P & { __scopeMenu?: Scope };\nconst [createMenuContext, createMenuScope] = createContextScope(MENU_NAME, [\n createCollectionScope,\n createPopperScope,\n createRovingFocusGroupScope,\n]);\nconst usePopperScope = createPopperScope();\nconst useRovingFocusGroupScope = createRovingFocusGroupScope();\n\ntype MenuContextValue = {\n open: boolean;\n onOpenChange(open: boolean): void;\n content: MenuContentElement | null;\n onContentChange(content: MenuContentElement | null): void;\n};\n\nconst [MenuProvider, useMenuContext] = createMenuContext(MENU_NAME);\n\ntype MenuRootContextValue = {\n onClose(): void;\n isUsingKeyboardRef: React.RefObject;\n dir: Direction;\n modal: boolean;\n};\n\nconst [MenuRootProvider, useMenuRootContext] = createMenuContext(MENU_NAME);\n\ninterface MenuProps {\n children?: React.ReactNode;\n open?: boolean;\n onOpenChange?(open: boolean): void;\n dir?: Direction;\n modal?: boolean;\n}\n\nconst Menu: React.FC = (props: ScopedProps) => {\n const { __scopeMenu, open = false, children, dir, onOpenChange, modal = true } = props;\n const popperScope = usePopperScope(__scopeMenu);\n const [content, setContent] = React.useState(null);\n const isUsingKeyboardRef = React.useRef(false);\n const handleOpenChange = useCallbackRef(onOpenChange);\n const direction = useDirection(dir);\n\n React.useEffect(() => {\n // Capture phase ensures we set the boolean before any side effects execute\n // in response to the key or pointer event as they might depend on this value.\n const handleKeyDown = () => {\n isUsingKeyboardRef.current = true;\n document.addEventListener('pointerdown', handlePointer, { capture: true, once: true });\n document.addEventListener('pointermove', handlePointer, { capture: true, once: true });\n };\n const handlePointer = () => (isUsingKeyboardRef.current = false);\n document.addEventListener('keydown', handleKeyDown, { capture: true });\n return () => {\n document.removeEventListener('keydown', handleKeyDown, { capture: true });\n document.removeEventListener('pointerdown', handlePointer, { capture: true });\n document.removeEventListener('pointermove', handlePointer, { capture: true });\n };\n }, []);\n\n return (\n \n \n handleOpenChange(false), [handleOpenChange])}\n isUsingKeyboardRef={isUsingKeyboardRef}\n dir={direction}\n modal={modal}\n >\n {children}\n \n \n \n );\n};\n\nMenu.displayName = MENU_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * MenuAnchor\n * -----------------------------------------------------------------------------------------------*/\n\nconst ANCHOR_NAME = 'MenuAnchor';\n\ntype MenuAnchorElement = React.ComponentRef;\ntype PopperAnchorProps = React.ComponentPropsWithoutRef;\ninterface MenuAnchorProps extends PopperAnchorProps {}\n\nconst MenuAnchor = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const { __scopeMenu, ...anchorProps } = props;\n const popperScope = usePopperScope(__scopeMenu);\n return ;\n }\n);\n\nMenuAnchor.displayName = ANCHOR_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * MenuPortal\n * -----------------------------------------------------------------------------------------------*/\n\nconst PORTAL_NAME = 'MenuPortal';\n\ntype PortalContextValue = { forceMount?: true };\nconst [PortalProvider, usePortalContext] = createMenuContext(PORTAL_NAME, {\n forceMount: undefined,\n});\n\ntype PortalProps = React.ComponentPropsWithoutRef;\ninterface MenuPortalProps {\n children?: React.ReactNode;\n /**\n * Specify a container element to portal the content into.\n */\n container?: PortalProps['container'];\n /**\n * Used to force mounting when more control is needed. Useful when\n * controlling animation with React animation libraries.\n */\n forceMount?: true;\n}\n\nconst MenuPortal: React.FC = (props: ScopedProps) => {\n const { __scopeMenu, forceMount, children, container } = props;\n const context = useMenuContext(PORTAL_NAME, __scopeMenu);\n return (\n \n \n \n {children}\n \n \n \n );\n};\n\nMenuPortal.displayName = PORTAL_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * MenuContent\n * -----------------------------------------------------------------------------------------------*/\n\nconst CONTENT_NAME = 'MenuContent';\n\ntype MenuContentContextValue = {\n onItemEnter(event: React.PointerEvent): void;\n onItemLeave(event: React.PointerEvent): void;\n onTriggerLeave(event: React.PointerEvent): void;\n searchRef: React.RefObject;\n pointerGraceTimerRef: React.MutableRefObject;\n onPointerGraceIntentChange(intent: GraceIntent | null): void;\n};\nconst [MenuContentProvider, useMenuContentContext] =\n createMenuContext(CONTENT_NAME);\n\ntype MenuContentElement = MenuRootContentTypeElement;\n/**\n * We purposefully don't union MenuRootContent and MenuSubContent props here because\n * they have conflicting prop types. We agreed that we would allow MenuSubContent to\n * accept props that it would just ignore.\n */\ninterface MenuContentProps extends MenuRootContentTypeProps {\n /**\n * Used to force mounting when more control is needed. Useful when\n * controlling animation with React animation libraries.\n */\n forceMount?: true;\n}\n\nconst MenuContent = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const portalContext = usePortalContext(CONTENT_NAME, props.__scopeMenu);\n const { forceMount = portalContext.forceMount, ...contentProps } = props;\n const context = useMenuContext(CONTENT_NAME, props.__scopeMenu);\n const rootContext = useMenuRootContext(CONTENT_NAME, props.__scopeMenu);\n\n return (\n \n \n \n {rootContext.modal ? (\n \n ) : (\n \n )}\n \n \n \n );\n }\n);\n\n/* ---------------------------------------------------------------------------------------------- */\n\ntype MenuRootContentTypeElement = MenuContentImplElement;\ninterface MenuRootContentTypeProps\n extends Omit {}\n\nconst MenuRootContentModal = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const context = useMenuContext(CONTENT_NAME, props.__scopeMenu);\n const ref = React.useRef(null);\n const composedRefs = useComposedRefs(forwardedRef, ref);\n\n // Hide everything from ARIA except the `MenuContent`\n React.useEffect(() => {\n const content = ref.current;\n if (content) return hideOthers(content);\n }, []);\n\n return (\n event.preventDefault(),\n { checkForDefaultPrevented: false }\n )}\n onDismiss={() => context.onOpenChange(false)}\n />\n );\n }\n);\n\nconst MenuRootContentNonModal = React.forwardRef<\n MenuRootContentTypeElement,\n MenuRootContentTypeProps\n>((props: ScopedProps, forwardedRef) => {\n const context = useMenuContext(CONTENT_NAME, props.__scopeMenu);\n return (\n context.onOpenChange(false)}\n />\n );\n});\n\n/* ---------------------------------------------------------------------------------------------- */\n\ntype MenuContentImplElement = React.ComponentRef;\ntype FocusScopeProps = React.ComponentPropsWithoutRef;\ntype DismissableLayerProps = React.ComponentPropsWithoutRef;\ntype RovingFocusGroupProps = React.ComponentPropsWithoutRef;\ntype PopperContentProps = React.ComponentPropsWithoutRef;\ntype MenuContentImplPrivateProps = {\n onOpenAutoFocus?: FocusScopeProps['onMountAutoFocus'];\n onDismiss?: DismissableLayerProps['onDismiss'];\n disableOutsidePointerEvents?: DismissableLayerProps['disableOutsidePointerEvents'];\n\n /**\n * Whether scrolling outside the `MenuContent` should be prevented\n * (default: `false`)\n */\n disableOutsideScroll?: boolean;\n\n /**\n * Whether focus should be trapped within the `MenuContent`\n * (default: false)\n */\n trapFocus?: FocusScopeProps['trapped'];\n};\ninterface MenuContentImplProps\n extends MenuContentImplPrivateProps,\n Omit {\n /**\n * Event handler called when auto-focusing on close.\n * Can be prevented.\n */\n onCloseAutoFocus?: FocusScopeProps['onUnmountAutoFocus'];\n\n /**\n * Whether keyboard navigation should loop around\n * @defaultValue false\n */\n loop?: RovingFocusGroupProps['loop'];\n\n onEntryFocus?: RovingFocusGroupProps['onEntryFocus'];\n onEscapeKeyDown?: DismissableLayerProps['onEscapeKeyDown'];\n onPointerDownOutside?: DismissableLayerProps['onPointerDownOutside'];\n onFocusOutside?: DismissableLayerProps['onFocusOutside'];\n onInteractOutside?: DismissableLayerProps['onInteractOutside'];\n}\n\nconst Slot = createSlot('MenuContent.ScrollLock');\n\nconst MenuContentImpl = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const {\n __scopeMenu,\n loop = false,\n trapFocus,\n onOpenAutoFocus,\n onCloseAutoFocus,\n disableOutsidePointerEvents,\n onEntryFocus,\n onEscapeKeyDown,\n onPointerDownOutside,\n onFocusOutside,\n onInteractOutside,\n onDismiss,\n disableOutsideScroll,\n ...contentProps\n } = props;\n const context = useMenuContext(CONTENT_NAME, __scopeMenu);\n const rootContext = useMenuRootContext(CONTENT_NAME, __scopeMenu);\n const popperScope = usePopperScope(__scopeMenu);\n const rovingFocusGroupScope = useRovingFocusGroupScope(__scopeMenu);\n const getItems = useCollection(__scopeMenu);\n const [currentItemId, setCurrentItemId] = React.useState(null);\n const contentRef = React.useRef(null);\n const composedRefs = useComposedRefs(forwardedRef, contentRef, context.onContentChange);\n const timerRef = React.useRef(0);\n const searchRef = React.useRef('');\n const pointerGraceTimerRef = React.useRef(0);\n const pointerGraceIntentRef = React.useRef(null);\n const pointerDirRef = React.useRef('right');\n const lastPointerXRef = React.useRef(0);\n\n const ScrollLockWrapper = disableOutsideScroll ? RemoveScroll : React.Fragment;\n const scrollLockWrapperProps = disableOutsideScroll\n ? { as: Slot, allowPinchZoom: true }\n : undefined;\n\n const handleTypeaheadSearch = (key: string) => {\n const search = searchRef.current + key;\n const items = getItems().filter((item) => !item.disabled);\n const currentItem = document.activeElement;\n const currentMatch = items.find((item) => item.ref.current === currentItem)?.textValue;\n const values = items.map((item) => item.textValue);\n const nextMatch = getNextMatch(values, search, currentMatch);\n const newItem = items.find((item) => item.textValue === nextMatch)?.ref.current;\n\n // Reset `searchRef` 1 second after it was last updated\n (function updateSearch(value: string) {\n searchRef.current = value;\n window.clearTimeout(timerRef.current);\n if (value !== '') timerRef.current = window.setTimeout(() => updateSearch(''), 1000);\n })(search);\n\n if (newItem) {\n /**\n * Imperative focus during keydown is risky so we prevent React's batching updates\n * to avoid potential bugs. See: https://github.com/facebook/react/issues/20332\n */\n setTimeout(() => (newItem as HTMLElement).focus());\n }\n };\n\n React.useEffect(() => {\n return () => window.clearTimeout(timerRef.current);\n }, []);\n\n // Make sure the whole tree has focus guards as our `MenuContent` may be\n // the last element in the DOM (because of the `Portal`)\n useFocusGuards();\n\n const isPointerMovingToSubmenu = React.useCallback((event: React.PointerEvent) => {\n const isMovingTowards = pointerDirRef.current === pointerGraceIntentRef.current?.side;\n return isMovingTowards && isPointerInGraceArea(event, pointerGraceIntentRef.current?.area);\n }, []);\n\n return (\n {\n if (isPointerMovingToSubmenu(event)) event.preventDefault();\n },\n [isPointerMovingToSubmenu]\n )}\n onItemLeave={React.useCallback(\n (event) => {\n if (isPointerMovingToSubmenu(event)) return;\n contentRef.current?.focus();\n setCurrentItemId(null);\n },\n [isPointerMovingToSubmenu]\n )}\n onTriggerLeave={React.useCallback(\n (event) => {\n if (isPointerMovingToSubmenu(event)) event.preventDefault();\n },\n [isPointerMovingToSubmenu]\n )}\n pointerGraceTimerRef={pointerGraceTimerRef}\n onPointerGraceIntentChange={React.useCallback((intent) => {\n pointerGraceIntentRef.current = intent;\n }, [])}\n >\n \n {\n // when opening, explicitly focus the content area only and leave\n // `onEntryFocus` in control of focusing first item\n event.preventDefault();\n contentRef.current?.focus({ preventScroll: true });\n })}\n onUnmountAutoFocus={onCloseAutoFocus}\n >\n \n {\n // only focus first item when using keyboard\n if (!rootContext.isUsingKeyboardRef.current) event.preventDefault();\n })}\n preventScrollOnEntryFocus\n >\n {\n // submenu key events bubble through portals. We only care about keys in this menu.\n const target = event.target as HTMLElement;\n const isKeyDownInside =\n target.closest('[data-radix-menu-content]') === event.currentTarget;\n const isModifierKey = event.ctrlKey || event.altKey || event.metaKey;\n const isCharacterKey = event.key.length === 1;\n if (isKeyDownInside) {\n // menus should not be navigated using tab key so we prevent it\n if (event.key === 'Tab') event.preventDefault();\n if (!isModifierKey && isCharacterKey) handleTypeaheadSearch(event.key);\n }\n // focus first/last item based on key pressed\n const content = contentRef.current;\n if (event.target !== content) return;\n if (!FIRST_LAST_KEYS.includes(event.key)) return;\n event.preventDefault();\n const items = getItems().filter((item) => !item.disabled);\n const candidateNodes = items.map((item) => item.ref.current!);\n if (LAST_KEYS.includes(event.key)) candidateNodes.reverse();\n focusFirst(candidateNodes);\n })}\n onBlur={composeEventHandlers(props.onBlur, (event) => {\n // clear search buffer when leaving the menu\n if (!event.currentTarget.contains(event.target)) {\n window.clearTimeout(timerRef.current);\n searchRef.current = '';\n }\n })}\n onPointerMove={composeEventHandlers(\n props.onPointerMove,\n whenMouse((event) => {\n const target = event.target as HTMLElement;\n const pointerXHasChanged = lastPointerXRef.current !== event.clientX;\n\n // We don't use `event.movementX` for this check because Safari will\n // always return `0` on a pointer event.\n if (event.currentTarget.contains(target) && pointerXHasChanged) {\n const newDir = event.clientX > lastPointerXRef.current ? 'right' : 'left';\n pointerDirRef.current = newDir;\n lastPointerXRef.current = event.clientX;\n }\n })\n )}\n />\n \n \n \n \n \n );\n }\n);\n\nMenuContent.displayName = CONTENT_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * MenuGroup\n * -----------------------------------------------------------------------------------------------*/\n\nconst GROUP_NAME = 'MenuGroup';\n\ntype MenuGroupElement = React.ComponentRef;\ntype PrimitiveDivProps = React.ComponentPropsWithoutRef;\ninterface MenuGroupProps extends PrimitiveDivProps {}\n\nconst MenuGroup = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const { __scopeMenu, ...groupProps } = props;\n return ;\n }\n);\n\nMenuGroup.displayName = GROUP_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * MenuLabel\n * -----------------------------------------------------------------------------------------------*/\n\nconst LABEL_NAME = 'MenuLabel';\n\ntype MenuLabelElement = React.ComponentRef;\ninterface MenuLabelProps extends PrimitiveDivProps {}\n\nconst MenuLabel = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const { __scopeMenu, ...labelProps } = props;\n return ;\n }\n);\n\nMenuLabel.displayName = LABEL_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * MenuItem\n * -----------------------------------------------------------------------------------------------*/\n\nconst ITEM_NAME = 'MenuItem';\nconst ITEM_SELECT = 'menu.itemSelect';\n\ntype MenuItemElement = MenuItemImplElement;\ninterface MenuItemProps extends Omit {\n onSelect?: (event: Event) => void;\n}\n\nconst MenuItem = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const { disabled = false, onSelect, ...itemProps } = props;\n const ref = React.useRef(null);\n const rootContext = useMenuRootContext(ITEM_NAME, props.__scopeMenu);\n const contentContext = useMenuContentContext(ITEM_NAME, props.__scopeMenu);\n const composedRefs = useComposedRefs(forwardedRef, ref);\n const isPointerDownRef = React.useRef(false);\n\n const handleSelect = () => {\n const menuItem = ref.current;\n if (!disabled && menuItem) {\n const itemSelectEvent = new CustomEvent(ITEM_SELECT, { bubbles: true, cancelable: true });\n menuItem.addEventListener(ITEM_SELECT, (event) => onSelect?.(event), { once: true });\n dispatchDiscreteCustomEvent(menuItem, itemSelectEvent);\n if (itemSelectEvent.defaultPrevented) {\n isPointerDownRef.current = false;\n } else {\n rootContext.onClose();\n }\n }\n };\n\n return (\n {\n props.onPointerDown?.(event);\n isPointerDownRef.current = true;\n }}\n onPointerUp={composeEventHandlers(props.onPointerUp, (event) => {\n // Pointer down can move to a different menu item which should activate it on pointer up.\n // We dispatch a click for selection to allow composition with click based triggers and to\n // prevent Firefox from getting stuck in text selection mode when the menu closes.\n if (!isPointerDownRef.current) event.currentTarget?.click();\n })}\n onKeyDown={composeEventHandlers(props.onKeyDown, (event) => {\n const isTypingAhead = contentContext.searchRef.current !== '';\n if (disabled || (isTypingAhead && event.key === ' ')) return;\n if (SELECTION_KEYS.includes(event.key)) {\n event.currentTarget.click();\n /**\n * We prevent default browser behaviour for selection keys as they should trigger\n * a selection only:\n * - prevents space from scrolling the page.\n * - if keydown causes focus to move, prevents keydown from firing on the new target.\n */\n event.preventDefault();\n }\n })}\n />\n );\n }\n);\n\nMenuItem.displayName = ITEM_NAME;\n\n/* ---------------------------------------------------------------------------------------------- */\n\ntype MenuItemImplElement = React.ComponentRef;\ninterface MenuItemImplProps extends PrimitiveDivProps {\n disabled?: boolean;\n textValue?: string;\n}\n\nconst MenuItemImpl = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const { __scopeMenu, disabled = false, textValue, ...itemProps } = props;\n const contentContext = useMenuContentContext(ITEM_NAME, __scopeMenu);\n const rovingFocusGroupScope = useRovingFocusGroupScope(__scopeMenu);\n const ref = React.useRef(null);\n const composedRefs = useComposedRefs(forwardedRef, ref);\n const [isFocused, setIsFocused] = React.useState(false);\n\n // get the item's `.textContent` as default strategy for typeahead `textValue`\n const [textContent, setTextContent] = React.useState('');\n React.useEffect(() => {\n const menuItem = ref.current;\n if (menuItem) {\n setTextContent((menuItem.textContent ?? '').trim());\n }\n }, [itemProps.children]);\n\n return (\n \n \n {\n if (disabled) {\n contentContext.onItemLeave(event);\n } else {\n contentContext.onItemEnter(event);\n if (!event.defaultPrevented) {\n const item = event.currentTarget;\n item.focus({ preventScroll: true });\n }\n }\n })\n )}\n onPointerLeave={composeEventHandlers(\n props.onPointerLeave,\n whenMouse((event) => contentContext.onItemLeave(event))\n )}\n onFocus={composeEventHandlers(props.onFocus, () => setIsFocused(true))}\n onBlur={composeEventHandlers(props.onBlur, () => setIsFocused(false))}\n />\n \n \n );\n }\n);\n\n/* -------------------------------------------------------------------------------------------------\n * MenuCheckboxItem\n * -----------------------------------------------------------------------------------------------*/\n\nconst CHECKBOX_ITEM_NAME = 'MenuCheckboxItem';\n\ntype MenuCheckboxItemElement = MenuItemElement;\n\ntype CheckedState = boolean | 'indeterminate';\n\ninterface MenuCheckboxItemProps extends MenuItemProps {\n checked?: CheckedState;\n // `onCheckedChange` can never be called with `\"indeterminate\"` from the inside\n onCheckedChange?: (checked: boolean) => void;\n}\n\nconst MenuCheckboxItem = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const { checked = false, onCheckedChange, ...checkboxItemProps } = props;\n return (\n \n onCheckedChange?.(isIndeterminate(checked) ? true : !checked),\n { checkForDefaultPrevented: false }\n )}\n />\n \n );\n }\n);\n\nMenuCheckboxItem.displayName = CHECKBOX_ITEM_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * MenuRadioGroup\n * -----------------------------------------------------------------------------------------------*/\n\nconst RADIO_GROUP_NAME = 'MenuRadioGroup';\n\nconst [RadioGroupProvider, useRadioGroupContext] = createMenuContext(\n RADIO_GROUP_NAME,\n { value: undefined, onValueChange: () => {} }\n);\n\ntype MenuRadioGroupElement = React.ComponentRef;\ninterface MenuRadioGroupProps extends MenuGroupProps {\n value?: string;\n onValueChange?: (value: string) => void;\n}\n\nconst MenuRadioGroup = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const { value, onValueChange, ...groupProps } = props;\n const handleValueChange = useCallbackRef(onValueChange);\n return (\n \n \n \n );\n }\n);\n\nMenuRadioGroup.displayName = RADIO_GROUP_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * MenuRadioItem\n * -----------------------------------------------------------------------------------------------*/\n\nconst RADIO_ITEM_NAME = 'MenuRadioItem';\n\ntype MenuRadioItemElement = React.ComponentRef;\ninterface MenuRadioItemProps extends MenuItemProps {\n value: string;\n}\n\nconst MenuRadioItem = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const { value, ...radioItemProps } = props;\n const context = useRadioGroupContext(RADIO_ITEM_NAME, props.__scopeMenu);\n const checked = value === context.value;\n return (\n \n context.onValueChange?.(value),\n { checkForDefaultPrevented: false }\n )}\n />\n \n );\n }\n);\n\nMenuRadioItem.displayName = RADIO_ITEM_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * MenuItemIndicator\n * -----------------------------------------------------------------------------------------------*/\n\nconst ITEM_INDICATOR_NAME = 'MenuItemIndicator';\n\ntype CheckboxContextValue = { checked: CheckedState };\n\nconst [ItemIndicatorProvider, useItemIndicatorContext] = createMenuContext(\n ITEM_INDICATOR_NAME,\n { checked: false }\n);\n\ntype MenuItemIndicatorElement = React.ComponentRef;\ntype PrimitiveSpanProps = React.ComponentPropsWithoutRef;\ninterface MenuItemIndicatorProps extends PrimitiveSpanProps {\n /**\n * Used to force mounting when more control is needed. Useful when\n * controlling animation with React animation libraries.\n */\n forceMount?: true;\n}\n\nconst MenuItemIndicator = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const { __scopeMenu, forceMount, ...itemIndicatorProps } = props;\n const indicatorContext = useItemIndicatorContext(ITEM_INDICATOR_NAME, __scopeMenu);\n return (\n \n \n \n );\n }\n);\n\nMenuItemIndicator.displayName = ITEM_INDICATOR_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * MenuSeparator\n * -----------------------------------------------------------------------------------------------*/\n\nconst SEPARATOR_NAME = 'MenuSeparator';\n\ntype MenuSeparatorElement = React.ComponentRef;\ninterface MenuSeparatorProps extends PrimitiveDivProps {}\n\nconst MenuSeparator = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const { __scopeMenu, ...separatorProps } = props;\n return (\n \n );\n }\n);\n\nMenuSeparator.displayName = SEPARATOR_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * MenuArrow\n * -----------------------------------------------------------------------------------------------*/\n\nconst ARROW_NAME = 'MenuArrow';\n\ntype MenuArrowElement = React.ComponentRef;\ntype PopperArrowProps = React.ComponentPropsWithoutRef;\ninterface MenuArrowProps extends PopperArrowProps {}\n\nconst MenuArrow = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const { __scopeMenu, ...arrowProps } = props;\n const popperScope = usePopperScope(__scopeMenu);\n return ;\n }\n);\n\nMenuArrow.displayName = ARROW_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * MenuSub\n * -----------------------------------------------------------------------------------------------*/\n\nconst SUB_NAME = 'MenuSub';\n\ntype MenuSubContextValue = {\n contentId: string;\n triggerId: string;\n trigger: MenuSubTriggerElement | null;\n onTriggerChange(trigger: MenuSubTriggerElement | null): void;\n};\n\nconst [MenuSubProvider, useMenuSubContext] = createMenuContext(SUB_NAME);\n\ninterface MenuSubProps {\n children?: React.ReactNode;\n open?: boolean;\n onOpenChange?(open: boolean): void;\n}\n\nconst MenuSub: React.FC = (props: ScopedProps) => {\n const { __scopeMenu, children, open = false, onOpenChange } = props;\n const parentMenuContext = useMenuContext(SUB_NAME, __scopeMenu);\n const popperScope = usePopperScope(__scopeMenu);\n const [trigger, setTrigger] = React.useState(null);\n const [content, setContent] = React.useState(null);\n const handleOpenChange = useCallbackRef(onOpenChange);\n\n // Prevent the parent menu from reopening with open submenus.\n React.useEffect(() => {\n if (parentMenuContext.open === false) handleOpenChange(false);\n return () => handleOpenChange(false);\n }, [parentMenuContext.open, handleOpenChange]);\n\n return (\n \n \n \n {children}\n \n \n \n );\n};\n\nMenuSub.displayName = SUB_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * MenuSubTrigger\n * -----------------------------------------------------------------------------------------------*/\n\nconst SUB_TRIGGER_NAME = 'MenuSubTrigger';\n\ntype MenuSubTriggerElement = MenuItemImplElement;\ninterface MenuSubTriggerProps extends MenuItemImplProps {}\n\nconst MenuSubTrigger = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const context = useMenuContext(SUB_TRIGGER_NAME, props.__scopeMenu);\n const rootContext = useMenuRootContext(SUB_TRIGGER_NAME, props.__scopeMenu);\n const subContext = useMenuSubContext(SUB_TRIGGER_NAME, props.__scopeMenu);\n const contentContext = useMenuContentContext(SUB_TRIGGER_NAME, props.__scopeMenu);\n const openTimerRef = React.useRef(null);\n const { pointerGraceTimerRef, onPointerGraceIntentChange } = contentContext;\n const scope = { __scopeMenu: props.__scopeMenu };\n\n const clearOpenTimer = React.useCallback(() => {\n if (openTimerRef.current) window.clearTimeout(openTimerRef.current);\n openTimerRef.current = null;\n }, []);\n\n React.useEffect(() => clearOpenTimer, [clearOpenTimer]);\n\n React.useEffect(() => {\n const pointerGraceTimer = pointerGraceTimerRef.current;\n return () => {\n window.clearTimeout(pointerGraceTimer);\n onPointerGraceIntentChange(null);\n };\n }, [pointerGraceTimerRef, onPointerGraceIntentChange]);\n\n return (\n \n {\n props.onClick?.(event);\n if (props.disabled || event.defaultPrevented) return;\n /**\n * We manually focus because iOS Safari doesn't always focus on click (e.g. buttons)\n * and we rely heavily on `onFocusOutside` for submenus to close when switching\n * between separate submenus.\n */\n event.currentTarget.focus();\n if (!context.open) context.onOpenChange(true);\n }}\n onPointerMove={composeEventHandlers(\n props.onPointerMove,\n whenMouse((event) => {\n contentContext.onItemEnter(event);\n if (event.defaultPrevented) return;\n if (!props.disabled && !context.open && !openTimerRef.current) {\n contentContext.onPointerGraceIntentChange(null);\n openTimerRef.current = window.setTimeout(() => {\n context.onOpenChange(true);\n clearOpenTimer();\n }, 100);\n }\n })\n )}\n onPointerLeave={composeEventHandlers(\n props.onPointerLeave,\n whenMouse((event) => {\n clearOpenTimer();\n\n const contentRect = context.content?.getBoundingClientRect();\n if (contentRect) {\n // TODO: make sure to update this when we change positioning logic\n const side = context.content?.dataset.side as Side;\n const rightSide = side === 'right';\n const bleed = rightSide ? -5 : +5;\n const contentNearEdge = contentRect[rightSide ? 'left' : 'right'];\n const contentFarEdge = contentRect[rightSide ? 'right' : 'left'];\n\n contentContext.onPointerGraceIntentChange({\n area: [\n // Apply a bleed on clientX to ensure that our exit point is\n // consistently within polygon bounds\n { x: event.clientX + bleed, y: event.clientY },\n { x: contentNearEdge, y: contentRect.top },\n { x: contentFarEdge, y: contentRect.top },\n { x: contentFarEdge, y: contentRect.bottom },\n { x: contentNearEdge, y: contentRect.bottom },\n ],\n side,\n });\n\n window.clearTimeout(pointerGraceTimerRef.current);\n pointerGraceTimerRef.current = window.setTimeout(\n () => contentContext.onPointerGraceIntentChange(null),\n 300\n );\n } else {\n contentContext.onTriggerLeave(event);\n if (event.defaultPrevented) return;\n\n // There's 100ms where the user may leave an item before the submenu was opened.\n contentContext.onPointerGraceIntentChange(null);\n }\n })\n )}\n onKeyDown={composeEventHandlers(props.onKeyDown, (event) => {\n const isTypingAhead = contentContext.searchRef.current !== '';\n if (props.disabled || (isTypingAhead && event.key === ' ')) return;\n if (SUB_OPEN_KEYS[rootContext.dir].includes(event.key)) {\n context.onOpenChange(true);\n // The trigger may hold focus if opened via pointer interaction\n // so we ensure content is given focus again when switching to keyboard.\n context.content?.focus();\n // prevent window from scrolling\n event.preventDefault();\n }\n })}\n />\n \n );\n }\n);\n\nMenuSubTrigger.displayName = SUB_TRIGGER_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * MenuSubContent\n * -----------------------------------------------------------------------------------------------*/\n\nconst SUB_CONTENT_NAME = 'MenuSubContent';\n\ntype MenuSubContentElement = MenuContentImplElement;\ninterface MenuSubContentProps\n extends Omit<\n MenuContentImplProps,\n keyof MenuContentImplPrivateProps | 'onCloseAutoFocus' | 'onEntryFocus' | 'side' | 'align'\n > {\n /**\n * Used to force mounting when more control is needed. Useful when\n * controlling animation with React animation libraries.\n */\n forceMount?: true;\n}\n\nconst MenuSubContent = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const portalContext = usePortalContext(CONTENT_NAME, props.__scopeMenu);\n const { forceMount = portalContext.forceMount, ...subContentProps } = props;\n const context = useMenuContext(CONTENT_NAME, props.__scopeMenu);\n const rootContext = useMenuRootContext(CONTENT_NAME, props.__scopeMenu);\n const subContext = useMenuSubContext(SUB_CONTENT_NAME, props.__scopeMenu);\n const ref = React.useRef(null);\n const composedRefs = useComposedRefs(forwardedRef, ref);\n return (\n \n \n \n {\n // when opening a submenu, focus content for keyboard users only\n if (rootContext.isUsingKeyboardRef.current) ref.current?.focus();\n event.preventDefault();\n }}\n // The menu might close because of focusing another menu item in the parent menu. We\n // don't want it to refocus the trigger in that case so we handle trigger focus ourselves.\n onCloseAutoFocus={(event) => event.preventDefault()}\n onFocusOutside={composeEventHandlers(props.onFocusOutside, (event) => {\n // We prevent closing when the trigger is focused to avoid triggering a re-open animation\n // on pointer interaction.\n if (event.target !== subContext.trigger) context.onOpenChange(false);\n })}\n onEscapeKeyDown={composeEventHandlers(props.onEscapeKeyDown, (event) => {\n rootContext.onClose();\n // ensure pressing escape in submenu doesn't escape full screen mode\n event.preventDefault();\n })}\n onKeyDown={composeEventHandlers(props.onKeyDown, (event) => {\n // Submenu key events bubble through portals. We only care about keys in this menu.\n const isKeyDownInside = event.currentTarget.contains(event.target as HTMLElement);\n const isCloseKey = SUB_CLOSE_KEYS[rootContext.dir].includes(event.key);\n if (isKeyDownInside && isCloseKey) {\n context.onOpenChange(false);\n // We focus manually because we prevented it in `onCloseAutoFocus`\n subContext.trigger?.focus();\n // prevent window from scrolling\n event.preventDefault();\n }\n })}\n />\n \n \n \n );\n }\n);\n\nMenuSubContent.displayName = SUB_CONTENT_NAME;\n\n/* -----------------------------------------------------------------------------------------------*/\n\nfunction getOpenState(open: boolean) {\n return open ? 'open' : 'closed';\n}\n\nfunction isIndeterminate(checked?: CheckedState): checked is 'indeterminate' {\n return checked === 'indeterminate';\n}\n\nfunction getCheckedState(checked: CheckedState) {\n return isIndeterminate(checked) ? 'indeterminate' : checked ? 'checked' : 'unchecked';\n}\n\nfunction focusFirst(candidates: HTMLElement[]) {\n const PREVIOUSLY_FOCUSED_ELEMENT = document.activeElement;\n for (const candidate of candidates) {\n // if focus is already where we want to go, we don't want to keep going through the candidates\n if (candidate === PREVIOUSLY_FOCUSED_ELEMENT) return;\n candidate.focus();\n if (document.activeElement !== PREVIOUSLY_FOCUSED_ELEMENT) return;\n }\n}\n\n/**\n * Wraps an array around itself at a given start index\n * Example: `wrapArray(['a', 'b', 'c', 'd'], 2) === ['c', 'd', 'a', 'b']`\n */\nfunction wrapArray(array: T[], startIndex: number) {\n return array.map((_, index) => array[(startIndex + index) % array.length]!);\n}\n\n/**\n * This is the \"meat\" of the typeahead matching logic. It takes in all the values,\n * the search and the current match, and returns the next match (or `undefined`).\n *\n * We normalize the search because if a user has repeatedly pressed a character,\n * we want the exact same behavior as if we only had that one character\n * (ie. cycle through options starting with that character)\n *\n * We also reorder the values by wrapping the array around the current match.\n * This is so we always look forward from the current match, and picking the first\n * match will always be the correct one.\n *\n * Finally, if the normalized search is exactly one character, we exclude the\n * current match from the values because otherwise it would be the first to match always\n * and focus would never move. This is as opposed to the regular case, where we\n * don't want focus to move if the current match still matches.\n */\nfunction getNextMatch(values: string[], search: string, currentMatch?: string) {\n const isRepeated = search.length > 1 && Array.from(search).every((char) => char === search[0]);\n const normalizedSearch = isRepeated ? search[0]! : search;\n const currentMatchIndex = currentMatch ? values.indexOf(currentMatch) : -1;\n let wrappedValues = wrapArray(values, Math.max(currentMatchIndex, 0));\n const excludeCurrentMatch = normalizedSearch.length === 1;\n if (excludeCurrentMatch) wrappedValues = wrappedValues.filter((v) => v !== currentMatch);\n const nextMatch = wrappedValues.find((value) =>\n value.toLowerCase().startsWith(normalizedSearch.toLowerCase())\n );\n return nextMatch !== currentMatch ? nextMatch : undefined;\n}\n\ntype Point = { x: number; y: number };\ntype Polygon = Point[];\ntype Side = 'left' | 'right';\ntype GraceIntent = { area: Polygon; side: Side };\n\n// Determine if a point is inside of a polygon.\n// Based on https://github.com/substack/point-in-polygon\nfunction isPointInPolygon(point: Point, polygon: Polygon) {\n const { x, y } = point;\n let inside = false;\n for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {\n const ii = polygon[i]!;\n const jj = polygon[j]!;\n const xi = ii.x;\n const yi = ii.y;\n const xj = jj.x;\n const yj = jj.y;\n\n // prettier-ignore\n const intersect = ((yi > y) !== (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi);\n if (intersect) inside = !inside;\n }\n\n return inside;\n}\n\nfunction isPointerInGraceArea(event: React.PointerEvent, area?: Polygon) {\n if (!area) return false;\n const cursorPos = { x: event.clientX, y: event.clientY };\n return isPointInPolygon(cursorPos, area);\n}\n\nfunction whenMouse(handler: React.PointerEventHandler): React.PointerEventHandler {\n return (event) => (event.pointerType === 'mouse' ? handler(event) : undefined);\n}\n\nconst Root = Menu;\nconst Anchor = MenuAnchor;\nconst Portal = MenuPortal;\nconst Content = MenuContent;\nconst Group = MenuGroup;\nconst Label = MenuLabel;\nconst Item = MenuItem;\nconst CheckboxItem = MenuCheckboxItem;\nconst RadioGroup = MenuRadioGroup;\nconst RadioItem = MenuRadioItem;\nconst ItemIndicator = MenuItemIndicator;\nconst Separator = MenuSeparator;\nconst Arrow = MenuArrow;\nconst Sub = MenuSub;\nconst SubTrigger = MenuSubTrigger;\nconst SubContent = MenuSubContent;\n\nexport {\n createMenuScope,\n //\n Menu,\n MenuAnchor,\n MenuPortal,\n MenuContent,\n MenuGroup,\n MenuLabel,\n MenuItem,\n MenuCheckboxItem,\n MenuRadioGroup,\n MenuRadioItem,\n MenuItemIndicator,\n MenuSeparator,\n MenuArrow,\n MenuSub,\n MenuSubTrigger,\n MenuSubContent,\n //\n Root,\n Anchor,\n Portal,\n Content,\n Group,\n Label,\n Item,\n CheckboxItem,\n RadioGroup,\n RadioItem,\n ItemIndicator,\n Separator,\n Arrow,\n Sub,\n SubTrigger,\n SubContent,\n};\nexport type {\n MenuProps,\n MenuAnchorProps,\n MenuPortalProps,\n MenuContentProps,\n MenuGroupProps,\n MenuLabelProps,\n MenuItemProps,\n MenuCheckboxItemProps,\n MenuRadioGroupProps,\n MenuRadioItemProps,\n MenuItemIndicatorProps,\n MenuSeparatorProps,\n MenuArrowProps,\n MenuSubProps,\n MenuSubTriggerProps,\n MenuSubContentProps,\n};\n", "import React from 'react';\nimport { createContextScope } from '@radix-ui/react-context';\nimport { useComposedRefs } from '@radix-ui/react-compose-refs';\nimport { createSlot, type Slot } from '@radix-ui/react-slot';\n\ntype SlotProps = React.ComponentPropsWithoutRef;\ntype CollectionElement = HTMLElement;\ninterface CollectionProps extends SlotProps {\n scope: any;\n}\n\n// We have resorted to returning slots directly rather than exposing primitives that can then\n// be slotted like `\u2026`.\n// This is because we encountered issues with generic types that cannot be statically analysed\n// due to creating them dynamically via createCollection.\n\nfunction createCollection(name: string) {\n /* -----------------------------------------------------------------------------------------------\n * CollectionProvider\n * ---------------------------------------------------------------------------------------------*/\n\n const PROVIDER_NAME = name + 'CollectionProvider';\n const [createCollectionContext, createCollectionScope] = createContextScope(PROVIDER_NAME);\n\n type ContextValue = {\n collectionRef: React.RefObject;\n itemMap: Map<\n React.RefObject,\n { ref: React.RefObject } & ItemData\n >;\n };\n\n const [CollectionProviderImpl, useCollectionContext] = createCollectionContext(\n PROVIDER_NAME,\n { collectionRef: { current: null }, itemMap: new Map() }\n );\n\n const CollectionProvider: React.FC<{ children?: React.ReactNode; scope: any }> = (props) => {\n const { scope, children } = props;\n const ref = React.useRef(null);\n const itemMap = React.useRef(new Map()).current;\n return (\n \n {children}\n \n );\n };\n\n CollectionProvider.displayName = PROVIDER_NAME;\n\n /* -----------------------------------------------------------------------------------------------\n * CollectionSlot\n * ---------------------------------------------------------------------------------------------*/\n\n const COLLECTION_SLOT_NAME = name + 'CollectionSlot';\n\n const CollectionSlotImpl = createSlot(COLLECTION_SLOT_NAME);\n const CollectionSlot = React.forwardRef(\n (props, forwardedRef) => {\n const { scope, children } = props;\n const context = useCollectionContext(COLLECTION_SLOT_NAME, scope);\n const composedRefs = useComposedRefs(forwardedRef, context.collectionRef);\n return {children};\n }\n );\n\n CollectionSlot.displayName = COLLECTION_SLOT_NAME;\n\n /* -----------------------------------------------------------------------------------------------\n * CollectionItem\n * ---------------------------------------------------------------------------------------------*/\n\n const ITEM_SLOT_NAME = name + 'CollectionItemSlot';\n const ITEM_DATA_ATTR = 'data-radix-collection-item';\n\n type CollectionItemSlotProps = ItemData & {\n children: React.ReactNode;\n scope: any;\n };\n\n const CollectionItemSlotImpl = createSlot(ITEM_SLOT_NAME);\n const CollectionItemSlot = React.forwardRef(\n (props, forwardedRef) => {\n const { scope, children, ...itemData } = props;\n const ref = React.useRef(null);\n const composedRefs = useComposedRefs(forwardedRef, ref);\n const context = useCollectionContext(ITEM_SLOT_NAME, scope);\n\n React.useEffect(() => {\n context.itemMap.set(ref, { ref, ...(itemData as unknown as ItemData) });\n return () => void context.itemMap.delete(ref);\n });\n\n return (\n \n {children}\n \n );\n }\n );\n\n CollectionItemSlot.displayName = ITEM_SLOT_NAME;\n\n /* -----------------------------------------------------------------------------------------------\n * useCollection\n * ---------------------------------------------------------------------------------------------*/\n\n function useCollection(scope: any) {\n const context = useCollectionContext(name + 'CollectionConsumer', scope);\n\n const getItems = React.useCallback(() => {\n const collectionNode = context.collectionRef.current;\n if (!collectionNode) return [];\n const orderedNodes = Array.from(collectionNode.querySelectorAll(`[${ITEM_DATA_ATTR}]`));\n const items = Array.from(context.itemMap.values());\n const orderedItems = items.sort(\n (a, b) => orderedNodes.indexOf(a.ref.current!) - orderedNodes.indexOf(b.ref.current!)\n );\n return orderedItems;\n }, [context.collectionRef, context.itemMap]);\n\n return getItems;\n }\n\n return [\n { Provider: CollectionProvider, Slot: CollectionSlot, ItemSlot: CollectionItemSlot },\n useCollection,\n createCollectionScope,\n ] as const;\n}\n\nexport { createCollection };\nexport type { CollectionProps };\n", "import React from 'react';\nimport { createContextScope } from '@radix-ui/react-context';\nimport { useComposedRefs } from '@radix-ui/react-compose-refs';\nimport { createSlot, type Slot } from '@radix-ui/react-slot';\nimport type { EntryOf } from './ordered-dictionary';\nimport { OrderedDict } from './ordered-dictionary';\n\ntype SlotProps = React.ComponentPropsWithoutRef;\ntype CollectionElement = HTMLElement;\ninterface CollectionProps extends SlotProps {\n scope: any;\n}\n\ninterface BaseItemData {\n id?: string;\n}\n\ntype ItemDataWithElement<\n ItemData extends BaseItemData,\n ItemElement extends HTMLElement,\n> = ItemData & {\n element: ItemElement;\n};\n\ntype ItemMap = OrderedDict<\n ItemElement,\n ItemDataWithElement\n>;\n\nfunction createCollection<\n ItemElement extends HTMLElement,\n ItemData extends BaseItemData = BaseItemData,\n>(name: string) {\n /* -----------------------------------------------------------------------------------------------\n * CollectionProvider\n * ---------------------------------------------------------------------------------------------*/\n\n const PROVIDER_NAME = name + 'CollectionProvider';\n const [createCollectionContext, createCollectionScope] = createContextScope(PROVIDER_NAME);\n\n type ContextValue = {\n collectionElement: CollectionElement | null;\n collectionRef: React.Ref;\n collectionRefObject: React.RefObject;\n itemMap: ItemMap;\n setItemMap: React.Dispatch>>;\n };\n\n const [CollectionContextProvider, useCollectionContext] = createCollectionContext(\n PROVIDER_NAME,\n {\n collectionElement: null,\n collectionRef: { current: null },\n collectionRefObject: { current: null },\n itemMap: new OrderedDict(),\n setItemMap: () => void 0,\n }\n );\n\n type CollectionState = [\n ItemMap: ItemMap,\n SetItemMap: React.Dispatch>>,\n ];\n\n const CollectionProvider: React.FC<{\n children?: React.ReactNode;\n scope: any;\n state?: CollectionState;\n }> = ({ state, ...props }) => {\n return state ? (\n \n ) : (\n \n );\n };\n CollectionProvider.displayName = PROVIDER_NAME;\n\n const CollectionInit: React.FC<{\n children?: React.ReactNode;\n scope: any;\n }> = (props) => {\n const state = useInitCollection();\n return ;\n };\n CollectionInit.displayName = PROVIDER_NAME + 'Init';\n\n const CollectionProviderImpl: React.FC<{\n children?: React.ReactNode;\n scope: any;\n state: CollectionState;\n }> = (props) => {\n const { scope, children, state } = props;\n const ref = React.useRef(null);\n const [collectionElement, setCollectionElement] = React.useState(\n null\n );\n const composeRefs = useComposedRefs(ref, setCollectionElement);\n const [itemMap, setItemMap] = state;\n\n React.useEffect(() => {\n if (!collectionElement) return;\n\n const observer = getChildListObserver(() => {\n // setItemMap((map) => {\n // const copy = new OrderedDict(map).toSorted(([, a], [, b]) =>\n // !a.element || !b.element ? 0 : isElementPreceding(a.element, b.element) ? -1 : 1\n // );\n // // check if the order has changed\n // let index = -1;\n // for (const entry of copy) {\n // index++;\n // const key = map.keyAt(index)!;\n // const [copyKey] = entry;\n // if (key !== copyKey) {\n // // order has changed!\n // return copy;\n // }\n // }\n // return map;\n // });\n });\n observer.observe(collectionElement, {\n childList: true,\n subtree: true,\n });\n return () => {\n observer.disconnect();\n };\n }, [collectionElement]);\n\n return (\n \n {children}\n \n );\n };\n\n CollectionProviderImpl.displayName = PROVIDER_NAME + 'Impl';\n\n /* -----------------------------------------------------------------------------------------------\n * CollectionSlot\n * ---------------------------------------------------------------------------------------------*/\n\n const COLLECTION_SLOT_NAME = name + 'CollectionSlot';\n\n const CollectionSlotImpl = createSlot(COLLECTION_SLOT_NAME);\n const CollectionSlot = React.forwardRef(\n (props, forwardedRef) => {\n const { scope, children } = props;\n const context = useCollectionContext(COLLECTION_SLOT_NAME, scope);\n const composedRefs = useComposedRefs(forwardedRef, context.collectionRef);\n return {children};\n }\n );\n\n CollectionSlot.displayName = COLLECTION_SLOT_NAME;\n\n /* -----------------------------------------------------------------------------------------------\n * CollectionItem\n * ---------------------------------------------------------------------------------------------*/\n\n const ITEM_SLOT_NAME = name + 'CollectionItemSlot';\n const ITEM_DATA_ATTR = 'data-radix-collection-item';\n\n type CollectionItemSlotProps = ItemData & {\n children: React.ReactNode;\n scope: any;\n };\n\n const CollectionItemSlotImpl = createSlot(ITEM_SLOT_NAME);\n const CollectionItemSlot = React.forwardRef(\n (props, forwardedRef) => {\n const { scope, children, ...itemData } = props;\n const ref = React.useRef(null);\n const [element, setElement] = React.useState(null);\n const composedRefs = useComposedRefs(forwardedRef, ref, setElement);\n const context = useCollectionContext(ITEM_SLOT_NAME, scope);\n\n const { setItemMap } = context;\n\n const itemDataRef = React.useRef(itemData);\n if (!shallowEqual(itemDataRef.current, itemData)) {\n itemDataRef.current = itemData;\n }\n const memoizedItemData = itemDataRef.current;\n\n React.useEffect(() => {\n const itemData = memoizedItemData;\n setItemMap((map) => {\n if (!element) {\n return map;\n }\n\n if (!map.has(element)) {\n map.set(element, { ...(itemData as unknown as ItemData), element });\n return map.toSorted(sortByDocumentPosition);\n }\n\n return map\n .set(element, { ...(itemData as unknown as ItemData), element })\n .toSorted(sortByDocumentPosition);\n });\n\n return () => {\n setItemMap((map) => {\n if (!element || !map.has(element)) {\n return map;\n }\n map.delete(element);\n return new OrderedDict(map);\n });\n };\n }, [element, memoizedItemData, setItemMap]);\n\n return (\n \n {children}\n \n );\n }\n );\n\n CollectionItemSlot.displayName = ITEM_SLOT_NAME;\n\n /* -----------------------------------------------------------------------------------------------\n * useInitCollection\n * ---------------------------------------------------------------------------------------------*/\n\n function useInitCollection() {\n return React.useState>(new OrderedDict());\n }\n\n /* -----------------------------------------------------------------------------------------------\n * useCollection\n * ---------------------------------------------------------------------------------------------*/\n\n function useCollection(scope: any) {\n const { itemMap } = useCollectionContext(name + 'CollectionConsumer', scope);\n\n return itemMap;\n }\n\n const functions = {\n createCollectionScope,\n useCollection,\n useInitCollection,\n };\n\n return [\n { Provider: CollectionProvider, Slot: CollectionSlot, ItemSlot: CollectionItemSlot },\n functions,\n ] as const;\n}\n\nexport { createCollection };\nexport type { CollectionProps };\n\nfunction shallowEqual(a: any, b: any) {\n if (a === b) return true;\n if (typeof a !== 'object' || typeof b !== 'object') return false;\n if (a == null || b == null) return false;\n const keysA = Object.keys(a);\n const keysB = Object.keys(b);\n if (keysA.length !== keysB.length) return false;\n for (const key of keysA) {\n if (!Object.prototype.hasOwnProperty.call(b, key)) return false;\n if (a[key] !== b[key]) return false;\n }\n return true;\n}\n\nfunction isElementPreceding(a: Element, b: Element) {\n return !!(b.compareDocumentPosition(a) & Node.DOCUMENT_POSITION_PRECEDING);\n}\n\nfunction sortByDocumentPosition(\n a: EntryOf>,\n b: EntryOf>\n) {\n return !a[1].element || !b[1].element\n ? 0\n : isElementPreceding(a[1].element, b[1].element)\n ? -1\n : 1;\n}\n\nfunction getChildListObserver(callback: () => void) {\n const observer = new MutationObserver((mutationsList) => {\n for (const mutation of mutationsList) {\n if (mutation.type === 'childList') {\n callback();\n return;\n }\n }\n });\n\n return observer;\n}\n", "// Not a real member because it shouldn't be accessible, but the super class\n// calls `set` which needs to read the instanciation state, so it can't be a\n// private member.\nconst __instanciated = new WeakMap, boolean>();\nexport class OrderedDict extends Map {\n #keys: K[];\n\n constructor(iterable?: Iterable | null | undefined);\n constructor(entries?: readonly (readonly [K, V])[] | null) {\n super(entries);\n this.#keys = [...super.keys()];\n __instanciated.set(this, true);\n }\n\n set(key: K, value: V) {\n if (__instanciated.get(this)) {\n if (this.has(key)) {\n this.#keys[this.#keys.indexOf(key)] = key;\n } else {\n this.#keys.push(key);\n }\n }\n super.set(key, value);\n return this;\n }\n\n insert(index: number, key: K, value: V) {\n const has = this.has(key);\n const length = this.#keys.length;\n const relativeIndex = toSafeInteger(index);\n let actualIndex = relativeIndex >= 0 ? relativeIndex : length + relativeIndex;\n const safeIndex = actualIndex < 0 || actualIndex >= length ? -1 : actualIndex;\n\n if (safeIndex === this.size || (has && safeIndex === this.size - 1) || safeIndex === -1) {\n this.set(key, value);\n return this;\n }\n\n const size = this.size + (has ? 0 : 1);\n\n // If you insert at, say, -2, without this bit you'd replace the\n // second-to-last item and push the rest up one, which means the new item is\n // 3rd to last. This isn't very intuitive; inserting at -2 is more like\n // saying \"make this item the second to last\".\n if (relativeIndex < 0) {\n actualIndex++;\n }\n\n const keys = [...this.#keys];\n let nextValue: V | undefined;\n let shouldSkip = false;\n for (let i = actualIndex; i < size; i++) {\n if (actualIndex === i) {\n let nextKey = keys[i]!;\n if (keys[i] === key) {\n nextKey = keys[i + 1]!;\n }\n if (has) {\n // delete first to ensure that the item is moved to the end\n this.delete(key);\n }\n nextValue = this.get(nextKey);\n this.set(key, value);\n } else {\n if (!shouldSkip && keys[i - 1] === key) {\n shouldSkip = true;\n }\n const currentKey = keys[shouldSkip ? i : i - 1]!;\n const currentValue = nextValue!;\n nextValue = this.get(currentKey);\n this.delete(currentKey);\n this.set(currentKey, currentValue);\n }\n }\n return this;\n }\n\n with(index: number, key: K, value: V) {\n const copy = new OrderedDict(this);\n copy.insert(index, key, value);\n return copy;\n }\n\n before(key: K) {\n const index = this.#keys.indexOf(key) - 1;\n if (index < 0) {\n return undefined;\n }\n return this.entryAt(index);\n }\n\n /**\n * Sets a new key-value pair at the position before the given key.\n */\n setBefore(key: K, newKey: K, value: V) {\n const index = this.#keys.indexOf(key);\n if (index === -1) {\n return this;\n }\n return this.insert(index, newKey, value);\n }\n\n after(key: K) {\n let index = this.#keys.indexOf(key);\n index = index === -1 || index === this.size - 1 ? -1 : index + 1;\n if (index === -1) {\n return undefined;\n }\n return this.entryAt(index);\n }\n\n /**\n * Sets a new key-value pair at the position after the given key.\n */\n setAfter(key: K, newKey: K, value: V) {\n const index = this.#keys.indexOf(key);\n if (index === -1) {\n return this;\n }\n return this.insert(index + 1, newKey, value);\n }\n\n first() {\n return this.entryAt(0);\n }\n\n last() {\n return this.entryAt(-1);\n }\n\n clear() {\n this.#keys = [];\n return super.clear();\n }\n\n delete(key: K) {\n const deleted = super.delete(key);\n if (deleted) {\n this.#keys.splice(this.#keys.indexOf(key), 1);\n }\n return deleted;\n }\n\n deleteAt(index: number) {\n const key = this.keyAt(index);\n if (key !== undefined) {\n return this.delete(key);\n }\n return false;\n }\n\n at(index: number) {\n const key = at(this.#keys, index);\n if (key !== undefined) {\n return this.get(key);\n }\n }\n\n entryAt(index: number): [K, V] | undefined {\n const key = at(this.#keys, index);\n if (key !== undefined) {\n return [key, this.get(key)!];\n }\n }\n\n indexOf(key: K) {\n return this.#keys.indexOf(key);\n }\n\n keyAt(index: number) {\n return at(this.#keys, index);\n }\n\n from(key: K, offset: number) {\n const index = this.indexOf(key);\n if (index === -1) {\n return undefined;\n }\n let dest = index + offset;\n if (dest < 0) dest = 0;\n if (dest >= this.size) dest = this.size - 1;\n return this.at(dest);\n }\n\n keyFrom(key: K, offset: number) {\n const index = this.indexOf(key);\n if (index === -1) {\n return undefined;\n }\n let dest = index + offset;\n if (dest < 0) dest = 0;\n if (dest >= this.size) dest = this.size - 1;\n return this.keyAt(dest);\n }\n\n find(\n predicate: (entry: [K, V], index: number, dictionary: OrderedDict) => boolean,\n thisArg?: any\n ) {\n let index = 0;\n for (const entry of this) {\n if (Reflect.apply(predicate, thisArg, [entry, index, this])) {\n return entry;\n }\n index++;\n }\n return undefined;\n }\n\n findIndex(\n predicate: (entry: [K, V], index: number, dictionary: OrderedDict) => boolean,\n thisArg?: any\n ) {\n let index = 0;\n for (const entry of this) {\n if (Reflect.apply(predicate, thisArg, [entry, index, this])) {\n return index;\n }\n index++;\n }\n return -1;\n }\n\n filter(\n predicate: (entry: [K, V], index: number, dict: OrderedDict) => entry is [KK, VV],\n thisArg?: any\n ): OrderedDict;\n\n filter(\n predicate: (entry: [K, V], index: number, dictionary: OrderedDict) => unknown,\n thisArg?: any\n ): OrderedDict;\n\n filter(\n predicate: (entry: [K, V], index: number, dictionary: OrderedDict) => unknown,\n thisArg?: any\n ) {\n const entries: Array<[K, V]> = [];\n let index = 0;\n for (const entry of this) {\n if (Reflect.apply(predicate, thisArg, [entry, index, this])) {\n entries.push(entry);\n }\n index++;\n }\n return new OrderedDict(entries);\n }\n\n map(\n callbackfn: (entry: [K, V], index: number, dictionary: OrderedDict) => U,\n thisArg?: any\n ): OrderedDict {\n const entries: [K, U][] = [];\n let index = 0;\n for (const entry of this) {\n entries.push([entry[0], Reflect.apply(callbackfn, thisArg, [entry, index, this])]);\n index++;\n }\n return new OrderedDict(entries);\n }\n\n reduce(\n callbackfn: (\n previousValue: [K, V],\n currentEntry: [K, V],\n currentIndex: number,\n dictionary: OrderedDict\n ) => [K, V]\n ): [K, V];\n reduce(\n callbackfn: (\n previousValue: [K, V],\n currentEntry: [K, V],\n currentIndex: number,\n dictionary: OrderedDict\n ) => [K, V],\n initialValue: [K, V]\n ): [K, V];\n reduce(\n callbackfn: (\n previousValue: U,\n currentEntry: [K, V],\n currentIndex: number,\n dictionary: OrderedDict\n ) => U,\n initialValue: U\n ): U;\n\n reduce(\n ...args: [\n (\n previousValue: U,\n currentEntry: [K, V],\n currentIndex: number,\n dictionary: OrderedDict\n ) => U,\n U?,\n ]\n ) {\n const [callbackfn, initialValue] = args;\n let index = 0;\n let accumulator = initialValue ?? this.at(0)!;\n for (const entry of this) {\n if (index === 0 && args.length === 1) {\n accumulator = entry as any;\n } else {\n accumulator = Reflect.apply(callbackfn, this, [accumulator, entry, index, this]);\n }\n index++;\n }\n return accumulator;\n }\n\n reduceRight(\n callbackfn: (\n previousValue: [K, V],\n currentEntry: [K, V],\n currentIndex: number,\n dictionary: OrderedDict\n ) => [K, V]\n ): [K, V];\n reduceRight(\n callbackfn: (\n previousValue: [K, V],\n currentEntry: [K, V],\n currentIndex: number,\n dictionary: OrderedDict\n ) => [K, V],\n initialValue: [K, V]\n ): [K, V];\n reduceRight(\n callbackfn: (\n previousValue: [K, V],\n currentValue: U,\n currentIndex: number,\n dictionary: OrderedDict\n ) => U,\n initialValue: U\n ): U;\n\n reduceRight(\n ...args: [\n (\n previousValue: U,\n currentEntry: [K, V],\n currentIndex: number,\n dictionary: OrderedDict\n ) => U,\n U?,\n ]\n ) {\n const [callbackfn, initialValue] = args;\n let accumulator = initialValue ?? this.at(-1)!;\n for (let index = this.size - 1; index >= 0; index--) {\n const entry = this.at(index)!;\n if (index === this.size - 1 && args.length === 1) {\n accumulator = entry as any;\n } else {\n accumulator = Reflect.apply(callbackfn, this, [accumulator, entry, index, this]);\n }\n }\n return accumulator;\n }\n\n toSorted(compareFn?: (a: [K, V], b: [K, V]) => number): OrderedDict {\n const entries = [...this.entries()].sort(compareFn);\n return new OrderedDict(entries);\n }\n\n toReversed(): OrderedDict {\n const reversed = new OrderedDict();\n for (let index = this.size - 1; index >= 0; index--) {\n const key = this.keyAt(index)!;\n const element = this.get(key)!;\n reversed.set(key, element);\n }\n return reversed;\n }\n\n toSpliced(start: number, deleteCount?: number): OrderedDict;\n toSpliced(start: number, deleteCount: number, ...items: [K, V][]): OrderedDict;\n\n toSpliced(...args: [start: number, deleteCount: number, ...items: [K, V][]]) {\n const entries = [...this.entries()];\n entries.splice(...args);\n return new OrderedDict(entries);\n }\n\n slice(start?: number, end?: number) {\n const result = new OrderedDict();\n let stop = this.size - 1;\n\n if (start === undefined) {\n return result;\n }\n\n if (start < 0) {\n start = start + this.size;\n }\n\n if (end !== undefined && end > 0) {\n stop = end - 1;\n }\n\n for (let index = start; index <= stop; index++) {\n const key = this.keyAt(index)!;\n const element = this.get(key)!;\n result.set(key, element);\n }\n return result;\n }\n\n every(\n predicate: (entry: [K, V], index: number, dictionary: OrderedDict) => unknown,\n thisArg?: any\n ) {\n let index = 0;\n for (const entry of this) {\n if (!Reflect.apply(predicate, thisArg, [entry, index, this])) {\n return false;\n }\n index++;\n }\n return true;\n }\n\n some(\n predicate: (entry: [K, V], index: number, dictionary: OrderedDict) => unknown,\n thisArg?: any\n ) {\n let index = 0;\n for (const entry of this) {\n if (Reflect.apply(predicate, thisArg, [entry, index, this])) {\n return true;\n }\n index++;\n }\n return false;\n }\n}\n\nexport type KeyOf> =\n D extends OrderedDict ? K : never;\nexport type ValueOf> =\n D extends OrderedDict ? V : never;\nexport type EntryOf> = [KeyOf, ValueOf];\nexport type KeyFrom> = E[0];\nexport type ValueFrom> = E[1];\n\nfunction at(array: ArrayLike, index: number): T | undefined {\n if ('at' in Array.prototype) {\n return Array.prototype.at.call(array, index);\n }\n const actualIndex = toSafeIndex(array, index);\n return actualIndex === -1 ? undefined : array[actualIndex];\n}\n\nfunction toSafeIndex(array: ArrayLike, index: number) {\n const length = array.length;\n const relativeIndex = toSafeInteger(index);\n const actualIndex = relativeIndex >= 0 ? relativeIndex : length + relativeIndex;\n return actualIndex < 0 || actualIndex >= length ? -1 : actualIndex;\n}\n\nfunction toSafeInteger(number: number) {\n // eslint-disable-next-line no-self-compare\n return number !== number || number === 0 ? 0 : Math.trunc(number);\n}\n", "import * as React from 'react';\nimport { composeRefs } from '@radix-ui/react-compose-refs';\n\n/* -------------------------------------------------------------------------------------------------\n * Slot\n * -----------------------------------------------------------------------------------------------*/\n\ninterface SlotProps extends React.HTMLAttributes {\n children?: React.ReactNode;\n}\n\n/* @__NO_SIDE_EFFECTS__ */ export function createSlot(ownerName: string) {\n const SlotClone = createSlotClone(ownerName);\n const Slot = React.forwardRef((props, forwardedRef) => {\n const { children, ...slotProps } = props;\n const childrenArray = React.Children.toArray(children);\n const slottable = childrenArray.find(isSlottable);\n\n if (slottable) {\n // the new element to render is the one passed as a child of `Slottable`\n const newElement = slottable.props.children;\n\n const newChildren = childrenArray.map((child) => {\n if (child === slottable) {\n // because the new element will be the one rendered, we are only interested\n // in grabbing its children (`newElement.props.children`)\n if (React.Children.count(newElement) > 1) return React.Children.only(null);\n return React.isValidElement(newElement)\n ? (newElement.props as { children: React.ReactNode }).children\n : null;\n } else {\n return child;\n }\n });\n\n return (\n \n {React.isValidElement(newElement)\n ? React.cloneElement(newElement, undefined, newChildren)\n : null}\n \n );\n }\n\n return (\n \n {children}\n \n );\n });\n\n Slot.displayName = `${ownerName}.Slot`;\n return Slot;\n}\n\nconst Slot = createSlot('Slot');\n\n/* -------------------------------------------------------------------------------------------------\n * SlotClone\n * -----------------------------------------------------------------------------------------------*/\n\ninterface SlotCloneProps {\n children: React.ReactNode;\n}\n\n/* @__NO_SIDE_EFFECTS__ */ function createSlotClone(ownerName: string) {\n const SlotClone = React.forwardRef((props, forwardedRef) => {\n const { children, ...slotProps } = props;\n\n if (React.isValidElement(children)) {\n const childrenRef = getElementRef(children);\n const props = mergeProps(slotProps, children.props as AnyProps);\n // do not pass ref to React.Fragment for React 19 compatibility\n if (children.type !== React.Fragment) {\n props.ref = forwardedRef ? composeRefs(forwardedRef, childrenRef) : childrenRef;\n }\n return React.cloneElement(children, props);\n }\n\n return React.Children.count(children) > 1 ? React.Children.only(null) : null;\n });\n\n SlotClone.displayName = `${ownerName}.SlotClone`;\n return SlotClone;\n}\n\n/* -------------------------------------------------------------------------------------------------\n * Slottable\n * -----------------------------------------------------------------------------------------------*/\n\nconst SLOTTABLE_IDENTIFIER = Symbol('radix.slottable');\n\ninterface SlottableProps {\n children: React.ReactNode;\n}\n\ninterface SlottableComponent extends React.FC {\n __radixId: symbol;\n}\n\n/* @__NO_SIDE_EFFECTS__ */ export function createSlottable(ownerName: string) {\n const Slottable: SlottableComponent = ({ children }) => {\n return <>{children};\n };\n Slottable.displayName = `${ownerName}.Slottable`;\n Slottable.__radixId = SLOTTABLE_IDENTIFIER;\n return Slottable;\n}\n\nconst Slottable = createSlottable('Slottable');\n\n/* ---------------------------------------------------------------------------------------------- */\n\ntype AnyProps = Record;\n\nfunction isSlottable(\n child: React.ReactNode\n): child is React.ReactElement {\n return (\n React.isValidElement(child) &&\n typeof child.type === 'function' &&\n '__radixId' in child.type &&\n child.type.__radixId === SLOTTABLE_IDENTIFIER\n );\n}\n\nfunction mergeProps(slotProps: AnyProps, childProps: AnyProps) {\n // all child props should override\n const overrideProps = { ...childProps };\n\n for (const propName in childProps) {\n const slotPropValue = slotProps[propName];\n const childPropValue = childProps[propName];\n\n const isHandler = /^on[A-Z]/.test(propName);\n if (isHandler) {\n // if the handler exists on both, we compose them\n if (slotPropValue && childPropValue) {\n overrideProps[propName] = (...args: unknown[]) => {\n const result = childPropValue(...args);\n slotPropValue(...args);\n return result;\n };\n }\n // but if it exists only on the slot, we use only this one\n else if (slotPropValue) {\n overrideProps[propName] = slotPropValue;\n }\n }\n // if it's `style`, we merge them\n else if (propName === 'style') {\n overrideProps[propName] = { ...slotPropValue, ...childPropValue };\n } else if (propName === 'className') {\n overrideProps[propName] = [slotPropValue, childPropValue].filter(Boolean).join(' ');\n }\n }\n\n return { ...slotProps, ...overrideProps };\n}\n\n// Before React 19 accessing `element.props.ref` will throw a warning and suggest using `element.ref`\n// After React 19 accessing `element.ref` does the opposite.\n// https://github.com/facebook/react/pull/28348\n//\n// Access the ref using the method that doesn't yield a warning.\nfunction getElementRef(element: React.ReactElement) {\n // React <=18 in DEV\n let getter = Object.getOwnPropertyDescriptor(element.props, 'ref')?.get;\n let mayWarn = getter && 'isReactWarning' in getter && getter.isReactWarning;\n if (mayWarn) {\n return (element as any).ref;\n }\n\n // React 19 in DEV\n getter = Object.getOwnPropertyDescriptor(element, 'ref')?.get;\n mayWarn = getter && 'isReactWarning' in getter && getter.isReactWarning;\n if (mayWarn) {\n return (element.props as { ref?: React.Ref }).ref;\n }\n\n // Not DEV\n return (element.props as { ref?: React.Ref }).ref || (element as any).ref;\n}\n\nexport {\n Slot,\n Slottable,\n //\n Slot as Root,\n};\nexport type { SlotProps };\n", "import * as React from 'react';\n\ntype Direction = 'ltr' | 'rtl';\nconst DirectionContext = React.createContext(undefined);\n\n/* -------------------------------------------------------------------------------------------------\n * Direction\n * -----------------------------------------------------------------------------------------------*/\n\ninterface DirectionProviderProps {\n children?: React.ReactNode;\n dir: Direction;\n}\nconst DirectionProvider: React.FC = (props) => {\n const { dir, children } = props;\n return {children};\n};\n\n/* -----------------------------------------------------------------------------------------------*/\n\nfunction useDirection(localDir?: Direction) {\n const globalDir = React.useContext(DirectionContext);\n return localDir || globalDir || 'ltr';\n}\n\nconst Provider = DirectionProvider;\n\nexport {\n useDirection,\n //\n Provider,\n //\n DirectionProvider,\n};\n", "import * as React from 'react';\nimport { composeEventHandlers } from '@radix-ui/primitive';\nimport { Primitive, dispatchDiscreteCustomEvent } from '@radix-ui/react-primitive';\nimport { useComposedRefs } from '@radix-ui/react-compose-refs';\nimport { useCallbackRef } from '@radix-ui/react-use-callback-ref';\nimport { useEscapeKeydown } from '@radix-ui/react-use-escape-keydown';\n\n/* -------------------------------------------------------------------------------------------------\n * DismissableLayer\n * -----------------------------------------------------------------------------------------------*/\n\nconst DISMISSABLE_LAYER_NAME = 'DismissableLayer';\nconst CONTEXT_UPDATE = 'dismissableLayer.update';\nconst POINTER_DOWN_OUTSIDE = 'dismissableLayer.pointerDownOutside';\nconst FOCUS_OUTSIDE = 'dismissableLayer.focusOutside';\n\nlet originalBodyPointerEvents: string;\n\nconst DismissableLayerContext = React.createContext({\n layers: new Set(),\n layersWithOutsidePointerEventsDisabled: new Set(),\n branches: new Set(),\n});\n\ntype DismissableLayerElement = React.ComponentRef;\ntype PrimitiveDivProps = React.ComponentPropsWithoutRef;\ninterface DismissableLayerProps extends PrimitiveDivProps {\n /**\n * When `true`, hover/focus/click interactions will be disabled on elements outside\n * the `DismissableLayer`. Users will need to click twice on outside elements to\n * interact with them: once to close the `DismissableLayer`, and again to trigger the element.\n */\n disableOutsidePointerEvents?: boolean;\n /**\n * Event handler called when the escape key is down.\n * Can be prevented.\n */\n onEscapeKeyDown?: (event: KeyboardEvent) => void;\n /**\n * Event handler called when the a `pointerdown` event happens outside of the `DismissableLayer`.\n * Can be prevented.\n */\n onPointerDownOutside?: (event: PointerDownOutsideEvent) => void;\n /**\n * Event handler called when the focus moves outside of the `DismissableLayer`.\n * Can be prevented.\n */\n onFocusOutside?: (event: FocusOutsideEvent) => void;\n /**\n * Event handler called when an interaction happens outside the `DismissableLayer`.\n * Specifically, when a `pointerdown` event happens outside or focus moves outside of it.\n * Can be prevented.\n */\n onInteractOutside?: (event: PointerDownOutsideEvent | FocusOutsideEvent) => void;\n /**\n * Handler called when the `DismissableLayer` should be dismissed\n */\n onDismiss?: () => void;\n}\n\nconst DismissableLayer = React.forwardRef(\n (props, forwardedRef) => {\n const {\n disableOutsidePointerEvents = false,\n onEscapeKeyDown,\n onPointerDownOutside,\n onFocusOutside,\n onInteractOutside,\n onDismiss,\n ...layerProps\n } = props;\n const context = React.useContext(DismissableLayerContext);\n const [node, setNode] = React.useState(null);\n const ownerDocument = node?.ownerDocument ?? globalThis?.document;\n const [, force] = React.useState({});\n const composedRefs = useComposedRefs(forwardedRef, (node) => setNode(node));\n const layers = Array.from(context.layers);\n const [highestLayerWithOutsidePointerEventsDisabled] = [...context.layersWithOutsidePointerEventsDisabled].slice(-1); // prettier-ignore\n const highestLayerWithOutsidePointerEventsDisabledIndex = layers.indexOf(highestLayerWithOutsidePointerEventsDisabled!); // prettier-ignore\n const index = node ? layers.indexOf(node) : -1;\n const isBodyPointerEventsDisabled = context.layersWithOutsidePointerEventsDisabled.size > 0;\n const isPointerEventsEnabled = index >= highestLayerWithOutsidePointerEventsDisabledIndex;\n\n const pointerDownOutside = usePointerDownOutside((event) => {\n const target = event.target as HTMLElement;\n const isPointerDownOnBranch = [...context.branches].some((branch) => branch.contains(target));\n if (!isPointerEventsEnabled || isPointerDownOnBranch) return;\n onPointerDownOutside?.(event);\n onInteractOutside?.(event);\n if (!event.defaultPrevented) onDismiss?.();\n }, ownerDocument);\n\n const focusOutside = useFocusOutside((event) => {\n const target = event.target as HTMLElement;\n const isFocusInBranch = [...context.branches].some((branch) => branch.contains(target));\n if (isFocusInBranch) return;\n onFocusOutside?.(event);\n onInteractOutside?.(event);\n if (!event.defaultPrevented) onDismiss?.();\n }, ownerDocument);\n\n useEscapeKeydown((event) => {\n const isHighestLayer = index === context.layers.size - 1;\n if (!isHighestLayer) return;\n onEscapeKeyDown?.(event);\n if (!event.defaultPrevented && onDismiss) {\n event.preventDefault();\n onDismiss();\n }\n }, ownerDocument);\n\n React.useEffect(() => {\n if (!node) return;\n if (disableOutsidePointerEvents) {\n if (context.layersWithOutsidePointerEventsDisabled.size === 0) {\n originalBodyPointerEvents = ownerDocument.body.style.pointerEvents;\n ownerDocument.body.style.pointerEvents = 'none';\n }\n context.layersWithOutsidePointerEventsDisabled.add(node);\n }\n context.layers.add(node);\n dispatchUpdate();\n return () => {\n if (\n disableOutsidePointerEvents &&\n context.layersWithOutsidePointerEventsDisabled.size === 1\n ) {\n ownerDocument.body.style.pointerEvents = originalBodyPointerEvents;\n }\n };\n }, [node, ownerDocument, disableOutsidePointerEvents, context]);\n\n /**\n * We purposefully prevent combining this effect with the `disableOutsidePointerEvents` effect\n * because a change to `disableOutsidePointerEvents` would remove this layer from the stack\n * and add it to the end again so the layering order wouldn't be _creation order_.\n * We only want them to be removed from context stacks when unmounted.\n */\n React.useEffect(() => {\n return () => {\n if (!node) return;\n context.layers.delete(node);\n context.layersWithOutsidePointerEventsDisabled.delete(node);\n dispatchUpdate();\n };\n }, [node, context]);\n\n React.useEffect(() => {\n const handleUpdate = () => force({});\n document.addEventListener(CONTEXT_UPDATE, handleUpdate);\n return () => document.removeEventListener(CONTEXT_UPDATE, handleUpdate);\n }, []);\n\n return (\n \n );\n }\n);\n\nDismissableLayer.displayName = DISMISSABLE_LAYER_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * DismissableLayerBranch\n * -----------------------------------------------------------------------------------------------*/\n\nconst BRANCH_NAME = 'DismissableLayerBranch';\n\ntype DismissableLayerBranchElement = React.ComponentRef;\ninterface DismissableLayerBranchProps extends PrimitiveDivProps {}\n\nconst DismissableLayerBranch = React.forwardRef<\n DismissableLayerBranchElement,\n DismissableLayerBranchProps\n>((props, forwardedRef) => {\n const context = React.useContext(DismissableLayerContext);\n const ref = React.useRef(null);\n const composedRefs = useComposedRefs(forwardedRef, ref);\n\n React.useEffect(() => {\n const node = ref.current;\n if (node) {\n context.branches.add(node);\n return () => {\n context.branches.delete(node);\n };\n }\n }, [context.branches]);\n\n return ;\n});\n\nDismissableLayerBranch.displayName = BRANCH_NAME;\n\n/* -----------------------------------------------------------------------------------------------*/\n\ntype PointerDownOutsideEvent = CustomEvent<{ originalEvent: PointerEvent }>;\ntype FocusOutsideEvent = CustomEvent<{ originalEvent: FocusEvent }>;\n\n/**\n * Listens for `pointerdown` outside a react subtree. We use `pointerdown` rather than `pointerup`\n * to mimic layer dismissing behaviour present in OS.\n * Returns props to pass to the node we want to check for outside events.\n */\nfunction usePointerDownOutside(\n onPointerDownOutside?: (event: PointerDownOutsideEvent) => void,\n ownerDocument: Document = globalThis?.document\n) {\n const handlePointerDownOutside = useCallbackRef(onPointerDownOutside) as EventListener;\n const isPointerInsideReactTreeRef = React.useRef(false);\n const handleClickRef = React.useRef(() => {});\n\n React.useEffect(() => {\n const handlePointerDown = (event: PointerEvent) => {\n if (event.target && !isPointerInsideReactTreeRef.current) {\n const eventDetail = { originalEvent: event };\n\n function handleAndDispatchPointerDownOutsideEvent() {\n handleAndDispatchCustomEvent(\n POINTER_DOWN_OUTSIDE,\n handlePointerDownOutside,\n eventDetail,\n { discrete: true }\n );\n }\n\n /**\n * On touch devices, we need to wait for a click event because browsers implement\n * a ~350ms delay between the time the user stops touching the display and when the\n * browser executres events. We need to ensure we don't reactivate pointer-events within\n * this timeframe otherwise the browser may execute events that should have been prevented.\n *\n * Additionally, this also lets us deal automatically with cancellations when a click event\n * isn't raised because the page was considered scrolled/drag-scrolled, long-pressed, etc.\n *\n * This is why we also continuously remove the previous listener, because we cannot be\n * certain that it was raised, and therefore cleaned-up.\n */\n if (event.pointerType === 'touch') {\n ownerDocument.removeEventListener('click', handleClickRef.current);\n handleClickRef.current = handleAndDispatchPointerDownOutsideEvent;\n ownerDocument.addEventListener('click', handleClickRef.current, { once: true });\n } else {\n handleAndDispatchPointerDownOutsideEvent();\n }\n } else {\n // We need to remove the event listener in case the outside click has been canceled.\n // See: https://github.com/radix-ui/primitives/issues/2171\n ownerDocument.removeEventListener('click', handleClickRef.current);\n }\n isPointerInsideReactTreeRef.current = false;\n };\n /**\n * if this hook executes in a component that mounts via a `pointerdown` event, the event\n * would bubble up to the document and trigger a `pointerDownOutside` event. We avoid\n * this by delaying the event listener registration on the document.\n * This is not React specific, but rather how the DOM works, ie:\n * ```\n * button.addEventListener('pointerdown', () => {\n * console.log('I will log');\n * document.addEventListener('pointerdown', () => {\n * console.log('I will also log');\n * })\n * });\n */\n const timerId = window.setTimeout(() => {\n ownerDocument.addEventListener('pointerdown', handlePointerDown);\n }, 0);\n return () => {\n window.clearTimeout(timerId);\n ownerDocument.removeEventListener('pointerdown', handlePointerDown);\n ownerDocument.removeEventListener('click', handleClickRef.current);\n };\n }, [ownerDocument, handlePointerDownOutside]);\n\n return {\n // ensures we check React component tree (not just DOM tree)\n onPointerDownCapture: () => (isPointerInsideReactTreeRef.current = true),\n };\n}\n\n/**\n * Listens for when focus happens outside a react subtree.\n * Returns props to pass to the root (node) of the subtree we want to check.\n */\nfunction useFocusOutside(\n onFocusOutside?: (event: FocusOutsideEvent) => void,\n ownerDocument: Document = globalThis?.document\n) {\n const handleFocusOutside = useCallbackRef(onFocusOutside) as EventListener;\n const isFocusInsideReactTreeRef = React.useRef(false);\n\n React.useEffect(() => {\n const handleFocus = (event: FocusEvent) => {\n if (event.target && !isFocusInsideReactTreeRef.current) {\n const eventDetail = { originalEvent: event };\n handleAndDispatchCustomEvent(FOCUS_OUTSIDE, handleFocusOutside, eventDetail, {\n discrete: false,\n });\n }\n };\n ownerDocument.addEventListener('focusin', handleFocus);\n return () => ownerDocument.removeEventListener('focusin', handleFocus);\n }, [ownerDocument, handleFocusOutside]);\n\n return {\n onFocusCapture: () => (isFocusInsideReactTreeRef.current = true),\n onBlurCapture: () => (isFocusInsideReactTreeRef.current = false),\n };\n}\n\nfunction dispatchUpdate() {\n const event = new CustomEvent(CONTEXT_UPDATE);\n document.dispatchEvent(event);\n}\n\nfunction handleAndDispatchCustomEvent(\n name: string,\n handler: ((event: E) => void) | undefined,\n detail: { originalEvent: OriginalEvent } & (E extends CustomEvent ? D : never),\n { discrete }: { discrete: boolean }\n) {\n const target = detail.originalEvent.target;\n const event = new CustomEvent(name, { bubbles: false, cancelable: true, detail });\n if (handler) target.addEventListener(name, handler as EventListener, { once: true });\n\n if (discrete) {\n dispatchDiscreteCustomEvent(target, event);\n } else {\n target.dispatchEvent(event);\n }\n}\n\nconst Root = DismissableLayer;\nconst Branch = DismissableLayerBranch;\n\nexport {\n DismissableLayer,\n DismissableLayerBranch,\n //\n Root,\n Branch,\n};\nexport type { DismissableLayerProps };\n", "import * as React from 'react';\nimport { useCallbackRef } from '@radix-ui/react-use-callback-ref';\n\n/**\n * Listens for when the escape key is down\n */\nfunction useEscapeKeydown(\n onEscapeKeyDownProp?: (event: KeyboardEvent) => void,\n ownerDocument: Document = globalThis?.document\n) {\n const onEscapeKeyDown = useCallbackRef(onEscapeKeyDownProp);\n\n React.useEffect(() => {\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Escape') {\n onEscapeKeyDown(event);\n }\n };\n ownerDocument.addEventListener('keydown', handleKeyDown, { capture: true });\n return () => ownerDocument.removeEventListener('keydown', handleKeyDown, { capture: true });\n }, [onEscapeKeyDown, ownerDocument]);\n}\n\nexport { useEscapeKeydown };\n", "import * as React from 'react';\n\n/** Number of components which have requested interest to have focus guards */\nlet count = 0;\n\ninterface FocusGuardsProps {\n children?: React.ReactNode;\n}\n\nfunction FocusGuards(props: FocusGuardsProps) {\n useFocusGuards();\n return props.children;\n}\n\n/**\n * Injects a pair of focus guards at the edges of the whole DOM tree\n * to ensure `focusin` & `focusout` events can be caught consistently.\n */\nfunction useFocusGuards() {\n /* eslint-disable no-restricted-globals */\n React.useEffect(() => {\n const edgeGuards = document.querySelectorAll('[data-radix-focus-guard]');\n document.body.insertAdjacentElement('afterbegin', edgeGuards[0] ?? createFocusGuard());\n document.body.insertAdjacentElement('beforeend', edgeGuards[1] ?? createFocusGuard());\n count++;\n\n return () => {\n if (count === 1) {\n document.querySelectorAll('[data-radix-focus-guard]').forEach((node) => node.remove());\n }\n count--;\n };\n }, []);\n /* eslint-enable no-restricted-globals */\n}\n\nfunction createFocusGuard() {\n // eslint-disable-next-line no-restricted-globals\n const element = document.createElement('span');\n element.setAttribute('data-radix-focus-guard', '');\n element.tabIndex = 0;\n element.style.outline = 'none';\n element.style.opacity = '0';\n element.style.position = 'fixed';\n element.style.pointerEvents = 'none';\n return element;\n}\n\nexport {\n FocusGuards,\n //\n FocusGuards as Root,\n //\n useFocusGuards,\n};\n", "import * as React from 'react';\nimport { useComposedRefs } from '@radix-ui/react-compose-refs';\nimport { Primitive } from '@radix-ui/react-primitive';\nimport { useCallbackRef } from '@radix-ui/react-use-callback-ref';\n\nconst AUTOFOCUS_ON_MOUNT = 'focusScope.autoFocusOnMount';\nconst AUTOFOCUS_ON_UNMOUNT = 'focusScope.autoFocusOnUnmount';\nconst EVENT_OPTIONS = { bubbles: false, cancelable: true };\n\ntype FocusableTarget = HTMLElement | { focus(): void };\n\n/* -------------------------------------------------------------------------------------------------\n * FocusScope\n * -----------------------------------------------------------------------------------------------*/\n\nconst FOCUS_SCOPE_NAME = 'FocusScope';\n\ntype FocusScopeElement = React.ComponentRef;\ntype PrimitiveDivProps = React.ComponentPropsWithoutRef;\ninterface FocusScopeProps extends PrimitiveDivProps {\n /**\n * When `true`, tabbing from last item will focus first tabbable\n * and shift+tab from first item will focus last tababble.\n * @defaultValue false\n */\n loop?: boolean;\n\n /**\n * When `true`, focus cannot escape the focus scope via keyboard,\n * pointer, or a programmatic focus.\n * @defaultValue false\n */\n trapped?: boolean;\n\n /**\n * Event handler called when auto-focusing on mount.\n * Can be prevented.\n */\n onMountAutoFocus?: (event: Event) => void;\n\n /**\n * Event handler called when auto-focusing on unmount.\n * Can be prevented.\n */\n onUnmountAutoFocus?: (event: Event) => void;\n}\n\nconst FocusScope = React.forwardRef((props, forwardedRef) => {\n const {\n loop = false,\n trapped = false,\n onMountAutoFocus: onMountAutoFocusProp,\n onUnmountAutoFocus: onUnmountAutoFocusProp,\n ...scopeProps\n } = props;\n const [container, setContainer] = React.useState(null);\n const onMountAutoFocus = useCallbackRef(onMountAutoFocusProp);\n const onUnmountAutoFocus = useCallbackRef(onUnmountAutoFocusProp);\n const lastFocusedElementRef = React.useRef(null);\n const composedRefs = useComposedRefs(forwardedRef, (node) => setContainer(node));\n\n const focusScope = React.useRef({\n paused: false,\n pause() {\n this.paused = true;\n },\n resume() {\n this.paused = false;\n },\n }).current;\n\n // Takes care of trapping focus if focus is moved outside programmatically for example\n React.useEffect(() => {\n if (trapped) {\n function handleFocusIn(event: FocusEvent) {\n if (focusScope.paused || !container) return;\n const target = event.target as HTMLElement | null;\n if (container.contains(target)) {\n lastFocusedElementRef.current = target;\n } else {\n focus(lastFocusedElementRef.current, { select: true });\n }\n }\n\n function handleFocusOut(event: FocusEvent) {\n if (focusScope.paused || !container) return;\n const relatedTarget = event.relatedTarget as HTMLElement | null;\n\n // A `focusout` event with a `null` `relatedTarget` will happen in at least two cases:\n //\n // 1. When the user switches app/tabs/windows/the browser itself loses focus.\n // 2. In Google Chrome, when the focused element is removed from the DOM.\n //\n // We let the browser do its thing here because:\n //\n // 1. The browser already keeps a memory of what's focused for when the page gets refocused.\n // 2. In Google Chrome, if we try to focus the deleted focused element (as per below), it\n // throws the CPU to 100%, so we avoid doing anything for this reason here too.\n if (relatedTarget === null) return;\n\n // If the focus has moved to an actual legitimate element (`relatedTarget !== null`)\n // that is outside the container, we move focus to the last valid focused element inside.\n if (!container.contains(relatedTarget)) {\n focus(lastFocusedElementRef.current, { select: true });\n }\n }\n\n // When the focused element gets removed from the DOM, browsers move focus\n // back to the document.body. In this case, we move focus to the container\n // to keep focus trapped correctly.\n function handleMutations(mutations: MutationRecord[]) {\n const focusedElement = document.activeElement as HTMLElement | null;\n if (focusedElement !== document.body) return;\n for (const mutation of mutations) {\n if (mutation.removedNodes.length > 0) focus(container);\n }\n }\n\n document.addEventListener('focusin', handleFocusIn);\n document.addEventListener('focusout', handleFocusOut);\n const mutationObserver = new MutationObserver(handleMutations);\n if (container) mutationObserver.observe(container, { childList: true, subtree: true });\n\n return () => {\n document.removeEventListener('focusin', handleFocusIn);\n document.removeEventListener('focusout', handleFocusOut);\n mutationObserver.disconnect();\n };\n }\n }, [trapped, container, focusScope.paused]);\n\n React.useEffect(() => {\n if (container) {\n focusScopesStack.add(focusScope);\n const previouslyFocusedElement = document.activeElement as HTMLElement | null;\n const hasFocusedCandidate = container.contains(previouslyFocusedElement);\n\n if (!hasFocusedCandidate) {\n const mountEvent = new CustomEvent(AUTOFOCUS_ON_MOUNT, EVENT_OPTIONS);\n container.addEventListener(AUTOFOCUS_ON_MOUNT, onMountAutoFocus);\n container.dispatchEvent(mountEvent);\n if (!mountEvent.defaultPrevented) {\n focusFirst(removeLinks(getTabbableCandidates(container)), { select: true });\n if (document.activeElement === previouslyFocusedElement) {\n focus(container);\n }\n }\n }\n\n return () => {\n container.removeEventListener(AUTOFOCUS_ON_MOUNT, onMountAutoFocus);\n\n // We hit a react bug (fixed in v17) with focusing in unmount.\n // We need to delay the focus a little to get around it for now.\n // See: https://github.com/facebook/react/issues/17894\n setTimeout(() => {\n const unmountEvent = new CustomEvent(AUTOFOCUS_ON_UNMOUNT, EVENT_OPTIONS);\n container.addEventListener(AUTOFOCUS_ON_UNMOUNT, onUnmountAutoFocus);\n container.dispatchEvent(unmountEvent);\n if (!unmountEvent.defaultPrevented) {\n focus(previouslyFocusedElement ?? document.body, { select: true });\n }\n // we need to remove the listener after we `dispatchEvent`\n container.removeEventListener(AUTOFOCUS_ON_UNMOUNT, onUnmountAutoFocus);\n\n focusScopesStack.remove(focusScope);\n }, 0);\n };\n }\n }, [container, onMountAutoFocus, onUnmountAutoFocus, focusScope]);\n\n // Takes care of looping focus (when tabbing whilst at the edges)\n const handleKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (!loop && !trapped) return;\n if (focusScope.paused) return;\n\n const isTabKey = event.key === 'Tab' && !event.altKey && !event.ctrlKey && !event.metaKey;\n const focusedElement = document.activeElement as HTMLElement | null;\n\n if (isTabKey && focusedElement) {\n const container = event.currentTarget as HTMLElement;\n const [first, last] = getTabbableEdges(container);\n const hasTabbableElementsInside = first && last;\n\n // we can only wrap focus if we have tabbable edges\n if (!hasTabbableElementsInside) {\n if (focusedElement === container) event.preventDefault();\n } else {\n if (!event.shiftKey && focusedElement === last) {\n event.preventDefault();\n if (loop) focus(first, { select: true });\n } else if (event.shiftKey && focusedElement === first) {\n event.preventDefault();\n if (loop) focus(last, { select: true });\n }\n }\n }\n },\n [loop, trapped, focusScope.paused]\n );\n\n return (\n \n );\n});\n\nFocusScope.displayName = FOCUS_SCOPE_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * Utils\n * -----------------------------------------------------------------------------------------------*/\n\n/**\n * Attempts focusing the first element in a list of candidates.\n * Stops when focus has actually moved.\n */\nfunction focusFirst(candidates: HTMLElement[], { select = false } = {}) {\n const previouslyFocusedElement = document.activeElement;\n for (const candidate of candidates) {\n focus(candidate, { select });\n if (document.activeElement !== previouslyFocusedElement) return;\n }\n}\n\n/**\n * Returns the first and last tabbable elements inside a container.\n */\nfunction getTabbableEdges(container: HTMLElement) {\n const candidates = getTabbableCandidates(container);\n const first = findVisible(candidates, container);\n const last = findVisible(candidates.reverse(), container);\n return [first, last] as const;\n}\n\n/**\n * Returns a list of potential tabbable candidates.\n *\n * NOTE: This is only a close approximation. For example it doesn't take into account cases like when\n * elements are not visible. This cannot be worked out easily by just reading a property, but rather\n * necessitate runtime knowledge (computed styles, etc). We deal with these cases separately.\n *\n * See: https://developer.mozilla.org/en-US/docs/Web/API/TreeWalker\n * Credit: https://github.com/discord/focus-layers/blob/master/src/util/wrapFocus.tsx#L1\n */\nfunction getTabbableCandidates(container: HTMLElement) {\n const nodes: HTMLElement[] = [];\n const walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT, {\n acceptNode: (node: any) => {\n const isHiddenInput = node.tagName === 'INPUT' && node.type === 'hidden';\n if (node.disabled || node.hidden || isHiddenInput) return NodeFilter.FILTER_SKIP;\n // `.tabIndex` is not the same as the `tabindex` attribute. It works on the\n // runtime's understanding of tabbability, so this automatically accounts\n // for any kind of element that could be tabbed to.\n return node.tabIndex >= 0 ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;\n },\n });\n while (walker.nextNode()) nodes.push(walker.currentNode as HTMLElement);\n // we do not take into account the order of nodes with positive `tabIndex` as it\n // hinders accessibility to have tab order different from visual order.\n return nodes;\n}\n\n/**\n * Returns the first visible element in a list.\n * NOTE: Only checks visibility up to the `container`.\n */\nfunction findVisible(elements: HTMLElement[], container: HTMLElement) {\n for (const element of elements) {\n // we stop checking if it's hidden at the `container` level (excluding)\n if (!isHidden(element, { upTo: container })) return element;\n }\n}\n\nfunction isHidden(node: HTMLElement, { upTo }: { upTo?: HTMLElement }) {\n if (getComputedStyle(node).visibility === 'hidden') return true;\n while (node) {\n // we stop at `upTo` (excluding it)\n if (upTo !== undefined && node === upTo) return false;\n if (getComputedStyle(node).display === 'none') return true;\n node = node.parentElement as HTMLElement;\n }\n return false;\n}\n\nfunction isSelectableInput(element: any): element is FocusableTarget & { select: () => void } {\n return element instanceof HTMLInputElement && 'select' in element;\n}\n\nfunction focus(element?: FocusableTarget | null, { select = false } = {}) {\n // only focus if that element is focusable\n if (element && element.focus) {\n const previouslyFocusedElement = document.activeElement;\n // NOTE: we prevent scrolling on focus, to minimize jarring transitions for users\n element.focus({ preventScroll: true });\n // only select if its not the same element, it supports selection and we need to select\n if (element !== previouslyFocusedElement && isSelectableInput(element) && select)\n element.select();\n }\n}\n\n/* -------------------------------------------------------------------------------------------------\n * FocusScope stack\n * -----------------------------------------------------------------------------------------------*/\n\ntype FocusScopeAPI = { paused: boolean; pause(): void; resume(): void };\nconst focusScopesStack = createFocusScopesStack();\n\nfunction createFocusScopesStack() {\n /** A stack of focus scopes, with the active one at the top */\n let stack: FocusScopeAPI[] = [];\n\n return {\n add(focusScope: FocusScopeAPI) {\n // pause the currently active focus scope (at the top of the stack)\n const activeFocusScope = stack[0];\n if (focusScope !== activeFocusScope) {\n activeFocusScope?.pause();\n }\n // remove in case it already exists (because we'll re-add it at the top of the stack)\n stack = arrayRemove(stack, focusScope);\n stack.unshift(focusScope);\n },\n\n remove(focusScope: FocusScopeAPI) {\n stack = arrayRemove(stack, focusScope);\n stack[0]?.resume();\n },\n };\n}\n\nfunction arrayRemove(array: T[], item: T) {\n const updatedArray = [...array];\n const index = updatedArray.indexOf(item);\n if (index !== -1) {\n updatedArray.splice(index, 1);\n }\n return updatedArray;\n}\n\nfunction removeLinks(items: HTMLElement[]) {\n return items.filter((item) => item.tagName !== 'A');\n}\n\nconst Root = FocusScope;\n\nexport {\n FocusScope,\n //\n Root,\n};\nexport type { FocusScopeProps };\n", "import * as React from 'react';\nimport { useLayoutEffect } from '@radix-ui/react-use-layout-effect';\n\n// We spaces with `.trim().toString()` to prevent bundlers from trying to `import { useId } from 'react';`\nconst useReactId = (React as any)[' useId '.trim().toString()] || (() => undefined);\nlet count = 0;\n\nfunction useId(deterministicId?: string): string {\n const [id, setId] = React.useState(useReactId());\n // React versions older than 18 will have client-side ids only.\n useLayoutEffect(() => {\n if (!deterministicId) setId((reactId) => reactId ?? String(count++));\n }, [deterministicId]);\n return deterministicId || (id ? `radix-${id}` : '');\n}\n\nexport { useId };\n", "import * as React from 'react';\nimport {\n useFloating,\n autoUpdate,\n offset,\n shift,\n limitShift,\n hide,\n arrow as floatingUIarrow,\n flip,\n size,\n} from '@floating-ui/react-dom';\nimport * as ArrowPrimitive from '@radix-ui/react-arrow';\nimport { useComposedRefs } from '@radix-ui/react-compose-refs';\nimport { createContextScope } from '@radix-ui/react-context';\nimport { Primitive } from '@radix-ui/react-primitive';\nimport { useCallbackRef } from '@radix-ui/react-use-callback-ref';\nimport { useLayoutEffect } from '@radix-ui/react-use-layout-effect';\nimport { useSize } from '@radix-ui/react-use-size';\n\nimport type { Placement, Middleware } from '@floating-ui/react-dom';\nimport type { Scope } from '@radix-ui/react-context';\nimport type { Measurable } from '@radix-ui/rect';\n\nconst SIDE_OPTIONS = ['top', 'right', 'bottom', 'left'] as const;\nconst ALIGN_OPTIONS = ['start', 'center', 'end'] as const;\n\ntype Side = (typeof SIDE_OPTIONS)[number];\ntype Align = (typeof ALIGN_OPTIONS)[number];\n\n/* -------------------------------------------------------------------------------------------------\n * Popper\n * -----------------------------------------------------------------------------------------------*/\n\nconst POPPER_NAME = 'Popper';\n\ntype ScopedProps

= P & { __scopePopper?: Scope };\nconst [createPopperContext, createPopperScope] = createContextScope(POPPER_NAME);\n\ntype PopperContextValue = {\n anchor: Measurable | null;\n onAnchorChange(anchor: Measurable | null): void;\n};\nconst [PopperProvider, usePopperContext] = createPopperContext(POPPER_NAME);\n\ninterface PopperProps {\n children?: React.ReactNode;\n}\nconst Popper: React.FC = (props: ScopedProps) => {\n const { __scopePopper, children } = props;\n const [anchor, setAnchor] = React.useState(null);\n return (\n \n {children}\n \n );\n};\n\nPopper.displayName = POPPER_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * PopperAnchor\n * -----------------------------------------------------------------------------------------------*/\n\nconst ANCHOR_NAME = 'PopperAnchor';\n\ntype PopperAnchorElement = React.ComponentRef;\ntype PrimitiveDivProps = React.ComponentPropsWithoutRef;\ninterface PopperAnchorProps extends PrimitiveDivProps {\n virtualRef?: React.RefObject;\n}\n\nconst PopperAnchor = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const { __scopePopper, virtualRef, ...anchorProps } = props;\n const context = usePopperContext(ANCHOR_NAME, __scopePopper);\n const ref = React.useRef(null);\n const composedRefs = useComposedRefs(forwardedRef, ref);\n\n const anchorRef = React.useRef(null);\n React.useEffect(() => {\n const previousAnchor = anchorRef.current;\n anchorRef.current = virtualRef?.current || ref.current;\n if (previousAnchor !== anchorRef.current) {\n // Consumer can anchor the popper to something that isn't\n // a DOM node e.g. pointer position, so we override the\n // `anchorRef` with their virtual ref in this case.\n context.onAnchorChange(anchorRef.current);\n }\n });\n\n return virtualRef ? null : ;\n }\n);\n\nPopperAnchor.displayName = ANCHOR_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * PopperContent\n * -----------------------------------------------------------------------------------------------*/\n\nconst CONTENT_NAME = 'PopperContent';\n\ntype PopperContentContextValue = {\n placedSide: Side;\n onArrowChange(arrow: HTMLSpanElement | null): void;\n arrowX?: number;\n arrowY?: number;\n shouldHideArrow: boolean;\n};\n\nconst [PopperContentProvider, useContentContext] =\n createPopperContext(CONTENT_NAME);\n\ntype Boundary = Element | null;\n\ntype PopperContentElement = React.ComponentRef;\ninterface PopperContentProps extends PrimitiveDivProps {\n side?: Side;\n sideOffset?: number;\n align?: Align;\n alignOffset?: number;\n arrowPadding?: number;\n avoidCollisions?: boolean;\n collisionBoundary?: Boundary | Boundary[];\n collisionPadding?: number | Partial>;\n sticky?: 'partial' | 'always';\n hideWhenDetached?: boolean;\n updatePositionStrategy?: 'optimized' | 'always';\n onPlaced?: () => void;\n}\n\nconst PopperContent = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const {\n __scopePopper,\n side = 'bottom',\n sideOffset = 0,\n align = 'center',\n alignOffset = 0,\n arrowPadding = 0,\n avoidCollisions = true,\n collisionBoundary = [],\n collisionPadding: collisionPaddingProp = 0,\n sticky = 'partial',\n hideWhenDetached = false,\n updatePositionStrategy = 'optimized',\n onPlaced,\n ...contentProps\n } = props;\n\n const context = usePopperContext(CONTENT_NAME, __scopePopper);\n\n const [content, setContent] = React.useState(null);\n const composedRefs = useComposedRefs(forwardedRef, (node) => setContent(node));\n\n const [arrow, setArrow] = React.useState(null);\n const arrowSize = useSize(arrow);\n const arrowWidth = arrowSize?.width ?? 0;\n const arrowHeight = arrowSize?.height ?? 0;\n\n const desiredPlacement = (side + (align !== 'center' ? '-' + align : '')) as Placement;\n\n const collisionPadding =\n typeof collisionPaddingProp === 'number'\n ? collisionPaddingProp\n : { top: 0, right: 0, bottom: 0, left: 0, ...collisionPaddingProp };\n\n const boundary = Array.isArray(collisionBoundary) ? collisionBoundary : [collisionBoundary];\n const hasExplicitBoundaries = boundary.length > 0;\n\n const detectOverflowOptions = {\n padding: collisionPadding,\n boundary: boundary.filter(isNotNull),\n // with `strategy: 'fixed'`, this is the only way to get it to respect boundaries\n altBoundary: hasExplicitBoundaries,\n };\n\n const { refs, floatingStyles, placement, isPositioned, middlewareData } = useFloating({\n // default to `fixed` strategy so users don't have to pick and we also avoid focus scroll issues\n strategy: 'fixed',\n placement: desiredPlacement,\n whileElementsMounted: (...args) => {\n const cleanup = autoUpdate(...args, {\n animationFrame: updatePositionStrategy === 'always',\n });\n return cleanup;\n },\n elements: {\n reference: context.anchor,\n },\n middleware: [\n offset({ mainAxis: sideOffset + arrowHeight, alignmentAxis: alignOffset }),\n avoidCollisions &&\n shift({\n mainAxis: true,\n crossAxis: false,\n limiter: sticky === 'partial' ? limitShift() : undefined,\n ...detectOverflowOptions,\n }),\n avoidCollisions && flip({ ...detectOverflowOptions }),\n size({\n ...detectOverflowOptions,\n apply: ({ elements, rects, availableWidth, availableHeight }) => {\n const { width: anchorWidth, height: anchorHeight } = rects.reference;\n const contentStyle = elements.floating.style;\n contentStyle.setProperty('--radix-popper-available-width', `${availableWidth}px`);\n contentStyle.setProperty('--radix-popper-available-height', `${availableHeight}px`);\n contentStyle.setProperty('--radix-popper-anchor-width', `${anchorWidth}px`);\n contentStyle.setProperty('--radix-popper-anchor-height', `${anchorHeight}px`);\n },\n }),\n arrow && floatingUIarrow({ element: arrow, padding: arrowPadding }),\n transformOrigin({ arrowWidth, arrowHeight }),\n hideWhenDetached && hide({ strategy: 'referenceHidden', ...detectOverflowOptions }),\n ],\n });\n\n const [placedSide, placedAlign] = getSideAndAlignFromPlacement(placement);\n\n const handlePlaced = useCallbackRef(onPlaced);\n useLayoutEffect(() => {\n if (isPositioned) {\n handlePlaced?.();\n }\n }, [isPositioned, handlePlaced]);\n\n const arrowX = middlewareData.arrow?.x;\n const arrowY = middlewareData.arrow?.y;\n const cannotCenterArrow = middlewareData.arrow?.centerOffset !== 0;\n\n const [contentZIndex, setContentZIndex] = React.useState();\n useLayoutEffect(() => {\n if (content) setContentZIndex(window.getComputedStyle(content).zIndex);\n }, [content]);\n\n return (\n