diff --git a/.Agent Context/Bibels/Desineuron Stable Ingress Handoff.md b/.Agent Context/Bibels/Desineuron Stable Ingress Handoff.md new file mode 100644 index 00000000..9b9a8ed1 --- /dev/null +++ b/.Agent Context/Bibels/Desineuron Stable Ingress Handoff.md @@ -0,0 +1,591 @@ +## Desineuron Stable Ingress Handoff + +Date: 2026-04-08 + +### Chapters + +1. Outcome +2. Final Architecture +3. AWS Resources +4. Linux Origin State +5. Migration Changes Applied +6. Validation Results +7. ComfyUI Recovery and GPU Route +8. Files and Config Artifacts +9. Dynamic Home IP Sync +10. Operational Commands +11. Future Service Mapping Runbook +12. Security Notes +13. Remaining Improvement Ideas +14. Rollback +15. Team Summary +16. Current Status Snapshot - 2026-04-12 +17. Linux Ops Control Plane + +### Outcome + +The Cloudflare Tunnel dependency for the six public `desineuron.in` services has been replaced with a self-hosted AWS ingress layer: + +- Public edge: AWS EC2 `t4g.micro` +- Stable public IP: `98.87.120.120` +- TLS termination: `Caddy` on the ingress node +- Private backend relay: `rathole` +- Origin: Linux box at `192.168.1.4` +- DNS: Cloudflare, `DNS only` + +Public hostnames now route through AWS instead of Cloudflare Tunnel: + +- `office.desineuron.in` +- `git.desineuron.in` +- `cloud.desineuron.in` +- `projects.desineuron.in` +- `talk.desineuron.in` +- `vpn.desineuron.in` +- `comfy.desineuron.in` (ingress route created for AWS GPU ComfyUI) +- `ops.desineuron.in` (private operator control surface on the Linux box) + +### Final Architecture + +```text +Internet + -> Cloudflare DNS + -> 98.87.120.120 + -> EC2 ingress: desineuron-ingress-01 + -> Caddy :443 + -> rathole server (control on 2333, local relay on 127.0.0.1:8443) + -> Linux origin tunnel client + -> Linux nginx :443 + -> per-host upstream routing + -> Gitea + -> Nextcloud + -> Taiga + -> OnlyOffice + -> NetBird + -> comfy.desineuron.in + -> EC2 ingress Caddy + -> private proxy to AWS GPU box `172.31.46.190:8188` + -> ComfyUI endpoints on systemd-managed GPU service +``` + +### AWS Resources + +- Instance name: `desineuron-ingress-01` +- Instance ID: `i-094df09acafb72494` +- Type: `t4g.micro` +- Region: `us-east-1` +- Subnet: `subnet-03d684ed15f327151` +- VPC: `vpc-081d2397920aad268` +- Root disk: `20 GB gp3` +- Elastic IP: `98.87.120.120` +- IAM role: `desineuron-ingress-role` +- Instance profile: `desineuron-ingress-profile` +- Security group: `sg-0721b8b48e12c531d` + +Current GPU worker: + +- Instance ID: `i-0e4eab5fe67cf9abe` +- Type: `g6.12xlarge` +- Region: `us-east-1` +- Private IP: `172.31.46.190` +- Current public IP: `100.31.64.121` +- Launch time: `2026-04-11T06:14:04Z` + +Open ingress ports: + +- `80/tcp` from internet +- `443/tcp` from internet +- `22/tcp` restricted to the current home public IP and auto-synced from the Linux origin +- `2333/tcp` from internet for `rathole` control and data relay + +GPU node security posture for ComfyUI: + +- public `8118/tcp` removed +- public `8188/tcp` removed +- `8188/tcp` now allowed only from ingress security group `sg-0721b8b48e12c531d` + +### Linux Origin State + +Services exposed to local nginx: + +- `git.desineuron.in` -> `127.0.0.1:3000` (`gitea`) +- `cloud.desineuron.in` -> `127.0.0.1:11000` (`nextcloud_app`) +- `talk.desineuron.in` -> `127.0.0.1:11000` (`nextcloud_app`, Talk-focused hostname) +- `projects.desineuron.in` -> `127.0.0.1:9100` (`taiga-gateway`) +- `office.desineuron.in` -> `127.0.0.1:9980` (`nextcloud_onlyoffice`) +- `vpn.desineuron.in` -> `127.0.0.1:8080` / `127.0.0.1:8081` (`netbird`) + +Tunnel state: + +- `rathole-client.service` active on Linux +- `rathole-server.service` active on AWS +- `cloudflared` inactive on Linux + +### Migration Changes Applied + +#### Cloudflare + +Old CNAME tunnel records were removed for the six public hostnames. + +New records were created: + +- Type: `A` +- Value: `98.87.120.120` +- Proxy status: `DNS only` +- TTL: `300` + +#### AWS Ingress + +Installed and configured: + +- `Caddy` +- `rathole` +- `amazon-ssm-agent` +- Linux-driven SSH allowlist sync for the ingress node + +TLS: + +- Existing valid certificate/key pair from the Linux origin was copied to the ingress node. +- Caddy now terminates HTTPS at the edge. + +#### Linux Origin + +nginx was already routing by hostname and remains the origin router. + +Nextcloud was adjusted so `talk.desineuron.in` no longer canonicalizes to `cloud.desineuron.in`: + +- removed `overwritehost` pin +- added `talk.desineuron.in` to trusted domains +- restarted `nextcloud_app` + +### Validation Results + +Public hostname checks through the new ingress: + +- `office.desineuron.in` -> `200 /welcome/` +- `git.desineuron.in` -> `200` +- `cloud.desineuron.in` -> `200 /login` +- `projects.desineuron.in` -> `200` +- `talk.desineuron.in` -> `200 /login` on `talk.desineuron.in` +- `vpn.desineuron.in` -> `200` +- `ops.desineuron.in/login` -> `200` +- `comfy.desineuron.in` -> `200` + +Important note: + +- `talk.desineuron.in` now stays on the `talk` hostname. +- It is still backed by the same Nextcloud origin and presents the Nextcloud login flow, which is expected given the current Linux-side app layout. + +### ComfyUI Recovery and GPU Route + +Root cause of the earlier `502`: + +- ingress route and TLS were correct +- the GPU spot node had lost the actual `/opt/dlami/nvme/ComfyUI` app tree +- nothing was listening on `172.31.46.190:8188` + +Permanent fix applied: + +- restored `/opt/dlami/nvme/ComfyUI` from upstream source control +- installed ComfyUI Python requirements on the GPU node +- created `systemd` unit `comfyui.service` +- enabled `comfyui.service` at boot with automatic restart +- kept `comfy.desineuron.in` mapped through ingress Caddy +- removed direct public access to `8118` and `8188` +- allowed `8188` only from ingress security group + +Current live path: + +- `https://comfy.desineuron.in` + -> ingress `98.87.120.120` + -> Caddy reverse proxy + -> GPU private IP `172.31.46.190:8188` + -> `comfyui.service` + +Current public result: + +- `comfy.desineuron.in` currently returns `200 OK` +- ingress route is now managed dynamically instead of hardcoded to one GPU private IP + +Current GPU service: + +- `comfyui.service` +- app path: `/opt/dlami/nvme/ComfyUI` +- log path: `/var/log/comfyui/service.log` +- port: `8188/tcp` + +Current backend state on `2026-04-12`: + +- `comfyui.service` is `active` +- `main.py` is present under `/opt/dlami/nvme/ComfyUI` +- the process is listening on `0.0.0.0:8188` +- the public ingress path is healthy again + +Auto-healing fix applied: + +- ComfyUI `systemd` service now runs an `ExecStartPre` recovery script at `/usr/local/bin/desineuron-ensure-comfyui.sh` +- that script reclones/repairs `/opt/dlami/nvme/ComfyUI` if the tree is missing or damaged +- Linux now runs `desineuron-comfy-route-sync.timer` +- the timer updates the managed Caddy route for `comfy.desineuron.in` to the current private IP of the AWS instance tagged `DesineuronRole=comfyui` +- this protects the public route from GPU instance IP drift without manual Caddy edits + +Expected endpoints: + +- `https://comfy.desineuron.in/` +- `https://comfy.desineuron.in/prompt` +- `https://comfy.desineuron.in/history/{prompt_id}` +- `https://comfy.desineuron.in/queue` +- `https://comfy.desineuron.in/upload/image` + +### Files and Config Artifacts + +Infrastructure artifacts in repo: + +- [README.md](/F:/Workin%20In%20Progress/DESINEURON/GITLAB/Project_Velocity/infrastructure/desineuron_ingress/README.md) +- [Caddyfile](/F:/Workin%20In%20Progress/DESINEURON/GITLAB/Project_Velocity/infrastructure/desineuron_ingress/Caddyfile) +- [rathole-server.toml](/F:/Workin%20In%20Progress/DESINEURON/GITLAB/Project_Velocity/infrastructure/desineuron_ingress/rathole-server.toml) +- [rathole-client.toml](/F:/Workin%20In%20Progress/DESINEURON/GITLAB/Project_Velocity/infrastructure/desineuron_ingress/rathole-client.toml) +- [install_linux_rathole_client.sh](/F:/Workin%20In%20Progress/DESINEURON/GITLAB/Project_Velocity/infrastructure/desineuron_ingress/install_linux_rathole_client.sh) +- [user_data.sh](/F:/Workin%20In%20Progress/DESINEURON/GITLAB/Project_Velocity/infrastructure/desineuron_ingress/user_data.sh) +- [install_gpu_comfyui_service.sh](/F:/Workin%20In%20Progress/DESINEURON/GITLAB/Project_Velocity/infrastructure/desineuron_ingress/install_gpu_comfyui_service.sh) +- [map_gpu_comfy_security.ps1](/F:/Workin%20In%20Progress/DESINEURON/GITLAB/Project_Velocity/infrastructure/desineuron_ingress/map_gpu_comfy_security.ps1) +- [sync_ingress_home_ip.py](/F:/Workin%20In%20Progress/DESINEURON/GITLAB/Project_Velocity/infrastructure/desineuron_ingress/sync_ingress_home_ip.py) +- [desineuron-ingress-home-ip-sync.service](/F:/Workin%20In%20Progress/DESINEURON/GITLAB/Project_Velocity/infrastructure/desineuron_ingress/desineuron-ingress-home-ip-sync.service) +- [desineuron-ingress-home-ip-sync.timer](/F:/Workin%20In%20Progress/DESINEURON/GITLAB/Project_Velocity/infrastructure/desineuron_ingress/desineuron-ingress-home-ip-sync.timer) +- [install_linux_ingress_ip_sync.sh](/F:/Workin%20In%20Progress/DESINEURON/GITLAB/Project_Velocity/infrastructure/desineuron_ingress/install_linux_ingress_ip_sync.sh) +- [sync_comfy_route.py](/F:/Workin%20In%20Progress/DESINEURON/GITLAB/Project_Velocity/infrastructure/desineuron_ingress/sync_comfy_route.py) +- [desineuron-comfy-route-sync.service](/F:/Workin%20In%20Progress/DESINEURON/GITLAB/Project_Velocity/infrastructure/desineuron_ingress/desineuron-comfy-route-sync.service) +- [desineuron-comfy-route-sync.timer](/F:/Workin%20In%20Progress/DESINEURON/GITLAB/Project_Velocity/infrastructure/desineuron_ingress/desineuron-comfy-route-sync.timer) +- [install_linux_comfy_route_sync.sh](/F:/Workin%20In%20Progress/DESINEURON/GITLAB/Project_Velocity/infrastructure/desineuron_ingress/install_linux_comfy_route_sync.sh) +- [README.md](/F:/Workin%20In%20Progress/DESINEURON/GITLAB/Project_Velocity/infrastructure/ops_control_plane/README.md) +- [Desineuron Ops Control Plane Bibel.md](/F:/Workin%20In%20Progress/DESINEURON/GITLAB/Project_Velocity/.Agent%20Context/Bibels/Desineuron%20Ops%20Control%20Plane%20Bibel.md) + +Linux origin files touched: + +- `/etc/nginx/sites-enabled/desineuron.conf` +- `/mnt/ServerStorage/docker_apps/nextcloud/.env` +- `/mnt/ServerStorage/docker_apps/nextcloud/data/config/config.php` +- `/mnt/ServerStorage/docker_apps/nextcloud/data/config/reverse-proxy.config.php` + +Backups created on Linux: + +- `/mnt/ServerStorage/docker_apps/nextcloud/.env.pre_ingress_backup_2026-04-08` +- `/mnt/ServerStorage/docker_apps/nextcloud/data/config/reverse-proxy.config.php.pre_ingress_backup_2026-04-08` + +### Dynamic Home IP Sync + +Purpose: + +- Keep ingress `22/tcp` restricted to the current Airtel public IP even when the ISP changes it +- Prevent future manual outages for SSH fallback caused by stale home-IP security-group rules + +Design: + +- Linux origin runs `desineuron-ingress-home-ip-sync.timer` +- Timer fires on boot and every 5 minutes +- Service resolves the current home public IP via `https://api.ipify.org` +- Service updates only the ingress security group `sg-0721b8b48e12c531d` +- Only the SSH fallback rule is mutated +- `rathole` is no longer dependent on the Airtel IP because `2333/tcp` remains open on the ingress + +Installed Linux paths: + +- `/usr/local/bin/sync_ingress_home_ip.py` +- `/etc/systemd/system/desineuron-ingress-home-ip-sync.service` +- `/etc/systemd/system/desineuron-ingress-home-ip-sync.timer` +- `/etc/desineuron-ingress-home-ip-sync.env` +- `/opt/desineuron-ingress-ip-sync/.venv` +- `/var/lib/desineuron-ingress-ip-sync/current_ip.txt` + +Current state: + +- Timer: enabled and active +- Last recorded home public IP: `223.185.28.89` +- Ingress SSH rule CIDR: `223.185.28.89/32` + +### Dynamic Comfy Route Sync + +Purpose: + +- keep `comfy.desineuron.in` mapped to the correct AWS GPU private IP even if the GPU instance public/private IP changes +- remove the need to hand-edit `/etc/caddy/Caddyfile` for ComfyUI moves + +Design: + +- Linux runs `desineuron-comfy-route-sync.timer` +- timer fires on boot and every 2 minutes +- service looks for the newest running EC2 instance tagged `DesineuronRole=comfyui` +- service reads its current private IP +- service connects to the ingress node and updates the managed Caddy route with `/usr/local/bin/manage_desineuron_routes.py` +- Caddy is validated and reloaded only after a successful route update + +Installed Linux paths: + +- `/usr/local/bin/sync_comfy_route.py` +- `/etc/systemd/system/desineuron-comfy-route-sync.service` +- `/etc/systemd/system/desineuron-comfy-route-sync.timer` +- `/etc/desineuron-comfy-route-sync.env` +- `/opt/desineuron-comfy-route-sync/.venv` +- `/var/lib/desineuron-comfy-route-sync/current_target.txt` + +Current state: + +- Timer: enabled and active +- Current synced target: `172.31.46.190` +- Current target instance tag: `DesineuronRole=comfyui` + +### Operational Commands + +Check AWS ingress status: + +```powershell +aws ec2 describe-instances --instance-ids i-094df09acafb72494 --region us-east-1 +aws ec2 describe-addresses --allocation-ids eipalloc-0d54fc0f827450e7b --region us-east-1 +``` + +Check ingress services: + +```powershell +aws ssm send-command --region us-east-1 --instance-ids i-094df09acafb72494 --document-name AWS-RunShellScript --parameters commands="sudo systemctl status caddy rathole-server --no-pager" +``` + +Check GPU ComfyUI service: + +```powershell +aws ssm send-command --region us-east-1 --instance-ids i-0e4eab5fe67cf9abe --document-name AWS-RunShellScript --parameters commands="sudo systemctl status comfyui --no-pager","ss -ltnp | grep 8188 || true","tail -n 40 /var/log/comfyui/service.log || true" +``` + +Check Linux origin services: + +```powershell +ssh -i "$env:USERPROFILE\.ssh\id_ed25519_desineuron_lan" desineuron-node-01@192.168.1.4 "echo '***' | sudo -S systemctl status rathole-client nginx" +ssh -i "$env:USERPROFILE\.ssh\id_ed25519_desineuron_lan" desineuron-node-01@192.168.1.4 "echo '***' | sudo -S systemctl status desineuron-ingress-home-ip-sync.service desineuron-ingress-home-ip-sync.timer" +ssh -i "$env:USERPROFILE\.ssh\id_ed25519_desineuron_lan" desineuron-node-01@192.168.1.4 "echo '***' | sudo -S journalctl -u desineuron-ingress-home-ip-sync -n 50 --no-pager" +ssh -i "$env:USERPROFILE\.ssh\id_ed25519_desineuron_lan" desineuron-node-01@192.168.1.4 "echo '***' | sudo -S systemctl status desineuron-ops-control-plane.service --no-pager" +ssh -i "$env:USERPROFILE\.ssh\id_ed25519_desineuron_lan" desineuron-node-01@192.168.1.4 "echo '***' | sudo -S docker compose -f /opt/desineuron-ops-control-plane/docker-compose.yml ps" +ssh -i "$env:USERPROFILE\.ssh\id_ed25519_desineuron_lan" desineuron-node-01@192.168.1.4 "echo '***' | sudo -S systemctl status desineuron-comfy-route-sync.service desineuron-comfy-route-sync.timer --no-pager" +``` + +Public endpoint validation: + +```powershell +curl.exe -I https://office.desineuron.in +curl.exe -I https://git.desineuron.in +curl.exe -I https://cloud.desineuron.in +curl.exe -I https://projects.desineuron.in +curl.exe -I https://talk.desineuron.in +curl.exe -I https://vpn.desineuron.in +curl.exe -I https://comfy.desineuron.in +curl.exe -I https://ops.desineuron.in/login +``` + +### Future Service Mapping Runbook + +Use this pattern for any future public service behind the stable ingress layer. + +1. Decide the backend location. + +- Linux origin behind `rathole` +- AWS GPU/private EC2 node +- another private backend later + +2. Decide whether the service should terminate TLS at ingress. + +- default: yes +- Caddy on ingress should own the public hostname and certificate + +3. Create the DNS record in Cloudflare. + +- type: `A` +- value: `98.87.120.120` +- proxy mode: `DNS only` +- low TTL during rollout + +4. Add the ingress route in [`Caddyfile`](/F:/Workin%20In%20Progress/DESINEURON/GITLAB/Project_Velocity/infrastructure/desineuron_ingress/Caddyfile). + +Patterns: + +- Linux-origin service: + - proxy to `https://127.0.0.1:8443` + - preserve `Host` +- private AWS backend service: + - proxy to `http://:` or `https://:` + +5. Restrict backend network access. + +- never leave backend app ports open to `0.0.0.0/0` unless absolutely necessary +- prefer security-group rule allowing traffic only from ingress security group +- for home-origin services, keep them private behind `rathole` + +6. Reload ingress. + +```powershell +ssh -i "F:\Workin In Progress\DESINEURON\GITLAB\Project_Velocity\desineuron-l4-node.pem" ec2-user@98.87.120.120 "sudo caddy validate --config /etc/caddy/Caddyfile && sudo systemctl reload caddy" +``` + +7. Validate TLS and app response. + +- check certificate subject matches hostname +- check `curl -I https://` +- check login page or health endpoint +- check browser behavior + +8. If the backend is stateful, create a persistent service. + +- prefer `systemd` +- enable restart on failure +- log to a stable path +- record service name, working directory, ports, and restart policy in this handoff doc + +9. Update team docs immediately. + +- hostname +- DNS record type +- ingress route target +- backend service owner +- service name +- health check command +- rollback step + +### Security Notes + +- Public traffic terminates only at the AWS edge. +- The Linux box no longer needs Cloudflare Tunnel for these six routes. +- The Linux origin is reached through an outbound tunnel, not by directly exposing the home machine to the public for app traffic. +- SSH on the Linux box remains key-only. +- The AWS ingress IAM role is limited to SSM core. +- ComfyUI is no longer directly exposed on the GPU public IP; only the ingress layer can reach `8188`. +- Ingress `22/tcp` stays restricted and is now auto-synced from the Linux origin. +- Ingress `2333/tcp` is intentionally open so `rathole` survives Airtel IP changes without operator action. + +### Remaining Improvement Ideas + +- Move the Linux nginx certificate issuance/renewal model to the AWS edge permanently instead of copying an existing certificate. +- Clean up nginx warnings about duplicated protocol options. +- Separate `talk.desineuron.in` more fully from general Nextcloud if a distinct Talk-only UX is desired. +- Add authentication in front of `comfy.desineuron.in`; internet scanners started hitting the route immediately after it went live. +- Consider putting Basic Auth or an allowlist in front of `comfy.desineuron.in` before broader team rollout. +- Add monitoring and alerting on: + - `caddy` + - `rathole-server` + - `rathole-client` + - public HTTPS checks +- Add infrastructure-as-code for the EC2 ingress node if this should be reproducible by the team without manual AWS CLI steps. + +### Rollback + +If rollback is needed: + +1. Recreate Cloudflare CNAME/tunnel routes or repoint the DNS records away from `98.87.120.120`. +2. Stop `caddy` and `rathole-server` on AWS. +3. Stop `rathole-client` on Linux. +4. Restore Nextcloud files from: + - `.env.pre_ingress_backup_2026-04-08` + - `reverse-proxy.config.php.pre_ingress_backup_2026-04-08` +5. Restart `nextcloud_app` and nginx. + +### Team Summary + +This migration is complete. + +Cloudflare Tunnel is no longer the production path for the six public service hostnames. The stable production ingress is now the AWS `t4g.micro` node with Elastic IP `98.87.120.120`, and the Linux machine remains the private origin behind `rathole`. + +Additional mapped route: + +- `comfy.desineuron.in` now terminates on the same stable ingress and forwards to the GPU node's private address `172.31.46.190:8188`. +- No further DNS change is needed for ComfyUI. +- The backend is supervised by `systemd` and currently healthy. +- The route is now auto-synced from Linux based on the tagged AWS ComfyUI worker, so future IP changes do not require manual ingress edits. +- The team can use: + - `https://comfy.desineuron.in/prompt` + - `https://comfy.desineuron.in/history/{prompt_id}` + - `https://comfy.desineuron.in/queue` + - `https://comfy.desineuron.in/upload/image` + +### Current Status Snapshot - 2026-04-12 + +Live public service state: + +- `office.desineuron.in` -> `200` +- `git.desineuron.in` -> `200` +- `cloud.desineuron.in` -> `200` +- `projects.desineuron.in` -> `200` +- `talk.desineuron.in` -> `200` +- `vpn.desineuron.in` -> `200` +- `ops.desineuron.in/login` -> `200` +- `comfy.desineuron.in` -> `200` + +Linux-origin health: + +- `nginx.service` -> `active` +- `rathole-client.service` -> `active` +- `desineuron-ingress-home-ip-sync.timer` -> `active` +- `desineuron-ops-control-plane.service` -> `active` + +Linux ops stack containers: + +- `desineuron-ops-api` -> `Up` +- `desineuron-ops-db` -> `Up (healthy)` +- `desineuron-ops-worker` -> `Up` + +Ingress health: + +- `caddy` -> `active` +- `rathole-server` -> `active` +- `comfy.desineuron.in` Caddy route is present in `/etc/caddy/Caddyfile` + +GPU ComfyUI state: + +- `comfyui.service` -> `active` +- `main.py` present under `/opt/dlami/nvme/ComfyUI` +- listener present on `0.0.0.0:8188` +- public ingress path is healthy + +Comfy auto-heal state: + +- `desineuron-comfy-route-sync.timer` -> `active` +- synced target file -> `/var/lib/desineuron-comfy-route-sync/current_target.txt` +- current synced target -> `172.31.46.190` + +### Linux Ops Control Plane + +The Linux box now also hosts the private AWS control surface for the team. + +Public operator URL: + +- `https://ops.desineuron.in/login` + +Purpose: + +- launch/stop/terminate AWS machines +- view spot/on-demand market data +- track runtime and estimated cost +- ingest model directories from the Linux box into S3 +- hydrate models from S3 to AWS GPU nodes +- manage ingress routes through the `t4g.micro` +- export session/cost CSVs + +Linux runtime paths: + +- stack root: `/opt/desineuron-ops-control-plane` +- env file: `/opt/desineuron-ops-control-plane/.env` +- exports: `/opt/desineuron-ops-control-plane/exports` +- state: `/opt/desineuron-ops-control-plane/state` + +Canonical S3 bucket: + +- `desineuron-ops-control-plane-819079556187-us-east-1` + +Model library source on Linux: + +- `/mnt/ServerStorage/ai-models/models` + +Current operator accounts: + +- `sagnik@desineuron.in` +- `sayan@desineuron.in` +- `sourik@desineuron.in` + +Reference docs: + +- [README.md](/F:/Workin%20In%20Progress/DESINEURON/GITLAB/Project_Velocity/infrastructure/ops_control_plane/README.md) +- [Desineuron Ops Control Plane Bibel.md](/F:/Workin%20In%20Progress/DESINEURON/GITLAB/Project_Velocity/.Agent%20Context/Bibels/Desineuron%20Ops%20Control%20Plane%20Bibel.md) diff --git a/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/00 - Sayan Pack Guide.md b/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/00 - Sayan Pack Guide.md new file mode 100644 index 00000000..161ee2da --- /dev/null +++ b/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/00 - Sayan Pack Guide.md @@ -0,0 +1,87 @@ +# Sayan Multi-Surface and Oracle Delivery Pack Guide + +**Date:** 2026-04-16 +**Status:** Active work packet +**Owner:** Sagnik +**Primary Assignee:** Sayan +**Reviewers:** Sagnik, integration owners for WebOS, iOS, backend, and schema +**Scope:** Multi-surface product expansion across iPad, iPhone Edge, Android Tab, Android Phone Edge, Oracle schema expansion, template database, inventory ingestion, and admin control plane +**Purpose:** Provide a complete, implementation-grade handoff set for Sayan so he can execute a bounded but broad workstream without reconstructing architecture from scattered files. + +## 1. Why This Pack Exists + +Sayan is not being assigned a single screen or a single file. He is being assigned a coordinated expansion of Project Velocity across: + +- existing iPad app completion +- new iPhone edge app +- new Android tablet app +- new Android phone edge app +- Oracle schema expansion +- Oracle template book and JSON template database +- Kimi Synthetic Data follow-on path +- inventory loading pipeline +- admin control plane + +Without a disciplined packet, this will fragment into duplicated UI, duplicated schemas, unsupported mobile assumptions, and merge conflicts. + +## 2. Current Repository Truth + +Existing repository centers already matter: + +- WebOS frontend: `Project_Velocity/app/` +- FastAPI backend: `Project_Velocity/backend/` +- native iPad foundation: `Project_Velocity/iOS/` +- Oracle schema and routes: `Project_Velocity/backend/oracle/` +- inventory asset corpus: `Project_Velocity/db assets/Inventory/` +- operational ingress and deployment truth: `Project_Velocity/infrastructure/` + +This packet extends those surfaces. It does not replace them. + +## 3. Reading Order + +Read these in order: + +1. [Introduction for Sayan.md](./Introduction%20for%20Sayan.md) +2. [Sayan Work Assignment_ Multi-Surface Platform and Oracle Expansion.md](./Sayan%20Work%20Assignment_%20Multi-Surface%20Platform%20and%20Oracle%20Expansion.md) +3. [Sayan Work Assignment_ Sprint 1 Execution Slice.md](./Sayan%20Work%20Assignment_%20Sprint%201%20Execution%20Slice.md) +4. [14 - Platform Reality and Communications Capture Strategy.md](./14%20-%20Platform%20Reality%20and%20Communications%20Capture%20Strategy.md) +5. [01 - First Principles_ Multi-Surface Platform and Oracle Expansion.md](./01%20-%20First%20Principles_%20Multi-Surface%20Platform%20and%20Oracle%20Expansion.md) +6. [02 - PRD_ Multi-Surface Platform and Oracle Expansion.md](./02%20-%20PRD_%20Multi-Surface%20Platform%20and%20Oracle%20Expansion.md) +7. [03 - SRS_ Multi-Surface Platform and Oracle Expansion.md](./03%20-%20SRS_%20Multi-Surface%20Platform%20and%20Oracle%20Expansion.md) +8. [04 - Sprint Plan_ Multi-Surface Platform and Oracle Expansion.md](./04%20-%20Sprint%20Plan_%20Multi-Surface%20Platform%20and%20Oracle%20Expansion.md) +9. [05 - Implementation Blueprint_ Multi-Surface Apps WebOS Edge and Oracle Delivery.md](./05%20-%20Implementation%20Blueprint_%20Multi-Surface%20Apps%20WebOS%20Edge%20and%20Oracle%20Delivery.md) +10. [06 - Execution Backlog_ Multi-Surface Apps Oracle Templates Inventory Admin.md](./06%20-%20Execution%20Backlog_%20Multi-Surface%20Apps%20Oracle%20Templates%20Inventory%20Admin.md) +11. [07 - Contracts and JSON Schemas_ Templates Inventory Edge Capture.md](./07%20-%20Contracts%20and%20JSON%20Schemas_%20Templates%20Inventory%20Edge%20Capture.md) +12. [08 - Adapter Detailed Implementation Spec_ Mobile Edge Inventory Admin.md](./08%20-%20Adapter%20Detailed%20Implementation%20Spec_%20Mobile%20Edge%20Inventory%20Admin.md) +13. [09 - Oracle Schema and Root API Spec_ Multi-Surface Platform.md](./09%20-%20Oracle%20Schema%20and%20Root%20API%20Spec_%20Multi-Surface%20Platform.md) +14. [10 - Shared Surface Module Spec_ WebOS iPad Android Edge.md](./10%20-%20Shared%20Surface%20Module%20Spec_%20WebOS%20iPad%20Android%20Edge.md) +15. [11 - Delivery Roles and Ownership Spec_ Mobile Oracle Admin.md](./11%20-%20Delivery%20Roles%20and%20Ownership%20Spec_%20Mobile%20Oracle%20Admin.md) +16. [12 - Deployment Operations and Release Readiness_ Multi-Surface Platform.md](./12%20-%20Deployment%20Operations%20and%20Release%20Readiness_%20Multi-Surface%20Platform.md) +17. [13 - Implementation Ticket Breakdown and Dependency Matrix_ Multi-Surface Platform.md](./13%20-%20Implementation%20Ticket%20Breakdown%20and%20Dependency%20Matrix_%20Multi-Surface%20Platform.md) + +## 4. Main Design Rule + +One workstream, one packet, one truth. + +The packet assumes: + +- WebOS remains the canonical broad operator surface +- the current iPad app remains the native pattern reference for tablet field work +- phone edge apps are narrow operator control and capture surfaces, not full WebOS clones +- Oracle remains the analytical composition center +- backend schema authority remains in the current FastAPI root + +## 5. Hard Constraints + +- do not create parallel backend authority +- do not fork schema truth outside the current backend +- do not assume unrestricted call recording or message interception on iPhone or Android +- do not assume WhatsApp API alone can solve every voice and video ingestion need +- do not downgrade the business requirement for communication memory just because some capture paths are blocked +- solve the requirement through supported telephony, supported business messaging APIs, explicit imports, and operator-assist flows +- do not break existing iPad or WebOS code while extending it +- do not build merge-hostile ownership overlaps + +## 6. Bottom Line + +This is not a brainstorming folder. It is the implementation and handoff packet for a major product expansion that must respect the current Velocity root while opening the next execution lane for Sayan. diff --git a/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/01 - First Principles_ Multi-Surface Platform and Oracle Expansion.md b/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/01 - First Principles_ Multi-Surface Platform and Oracle Expansion.md new file mode 100644 index 00000000..29265f9e --- /dev/null +++ b/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/01 - First Principles_ Multi-Surface Platform and Oracle Expansion.md @@ -0,0 +1,125 @@ +# Project Velocity - First Principles_ Multi-Surface Platform and Oracle Expansion + +**Date:** 2026-04-16 +**Status:** Active handoff baseline +**Owner:** Sayan +**Reviewers:** Sagnik, WebOS owner, backend owner +**Scope:** iPad residual completion, phone edge apps, Android tablet parity, Oracle schema and template DB expansion, inventory loading pipeline, admin control plane +**Purpose:** Define the real problem, intended behavior, and architectural logic before implementation extends across multiple product surfaces. + +## Executive Summary + +Project Velocity currently has a meaningful but uneven product surface: + +- WebOS is broad +- iPad has a real native start +- Oracle exists and already has route and schema direction +- inventory assets exist +- backend authority already exists + +What is missing is surface completion and normalization. + +The next expansion is not "make more screens." The real objective is: + +- finish the tablet field product properly +- create narrow edge apps for phone operators +- create durable communication memory so the system can remind operators about prior calls, messages, promises, and follow-up windows +- align all surfaces to one backend and one schema truth +- turn Oracle templates into a governed data product + +## Assumptions and Constraints + +### Assumptions + +- WebOS remains the broad operator shell +- the current iPad Swift app is the native parity reference for tablet work +- Android tablet should mirror the iPad information architecture, not reinvent it +- phone edge apps are lower-density, control-oriented, and communication-aware +- Oracle template generation will be backed by JSON templates and database persistence +- the business requirement includes remembering communication history and follow-up commitments across supported channels + +### Constraints + +- current repo ownership must be respected +- inventory source files are only partially present and more client data is still pending +- phone call capture and transcription must remain within OS, API, and consent constraints +- WhatsApp, email, and message ingestion must be built around supported business surfaces +- a managed or dedicated work phone does not remove iOS, Android, carrier, or provider restrictions +- backend authority remains in `Project_Velocity/backend/` + +## Reference Sources and Rationale + +### Local Sources + +- `Project_Velocity/README.md` +- `Project_Velocity/app/` +- `Project_Velocity/iOS/velocity/velocity/` +- `Project_Velocity/backend/oracle/router_v1.py` +- `Project_Velocity/backend/oracle/schema_oracle.sql` +- `Project_Velocity/backend/api/routes_oracle.py` +- `Project_Velocity/db assets/Inventory/` + +### Upstream or External Sources + +- Apple and Android platform rules matter for capture and telephony constraints +- WhatsApp Business Platform capability limits must be respected +- no external framework should override the current Velocity architecture + +## Problem Statement + +Velocity currently risks product asymmetry: + +- WebOS can do more than native surfaces +- iPad exists but is not yet the finished field product +- there is no dedicated phone edge experience for brand reps or realtors +- Oracle template strategy is present but not yet converted into a full template book and DB program +- inventory ingestion is still operator-heavy +- there is no dedicated admin control surface to operate the moving parts coherently + +## System Vision + +The intended end state is: + +- iPad and Android tablet act as fully capable field tablets +- iPhone and Android phone edge apps act as mobile control and communication surfaces +- Oracle becomes the composition and analytical runtime across all surfaces +- templates become governed JSON assets with synthetic expansion capability +- inventory loading becomes an explicit pipeline, not ad hoc manual work +- admin gets a controlled operational surface + +## First-Principles Architecture + +### Principle 1: Surface specialization is mandatory + +Each device class should do the work it is physically good at. + +### Principle 2: Backend truth stays centralized + +The current FastAPI backend already owns auth, Oracle, CRM direction, and route authority. + +### Principle 3: Oracle templates are product assets + +Templates must become categorized, versioned, queryable, and persistable. + +### Principle 4: Inventory data must become pipelineable + +Manual import as the permanent model is not acceptable. + +### Principle 5: Mobile capture must be consent-first and platform-realistic + +Do not promise unsupported device capture behavior. + +### Principle 6: Communication memory is a core product feature + +The real requirement is not raw recording for its own sake. The real requirement is durable operator memory: + +- what happened with this lead +- what was promised +- when the client said they would return, call back, or decide +- what reminder should fire later + +That requirement stays in scope even when some direct OS-level capture paths do not. + +## Bottom Line + +This work is a multi-surface normalization program around the current Velocity root. The correct implementation path is shared contracts, centralized backend truth, specialized device roles, and a real Oracle template data model. diff --git a/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/02 - PRD_ Multi-Surface Platform and Oracle Expansion.md b/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/02 - PRD_ Multi-Surface Platform and Oracle Expansion.md new file mode 100644 index 00000000..cddc51a8 --- /dev/null +++ b/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/02 - PRD_ Multi-Surface Platform and Oracle Expansion.md @@ -0,0 +1,116 @@ +# Product Requirements Document (PRD)_ Multi-Surface Platform and Oracle Expansion + +**Date:** 2026-04-16 +**Status:** Active planning artifact +**Owner:** Sayan +**Reviewers:** Sagnik, product and integration reviewers +**Scope:** User-facing and operator-facing outcomes for tablets, phone edge apps, Oracle template system, inventory loading, and admin control plane +**Purpose:** Define what product value this work must deliver. + +## 1. Product Goal + +Expand Project Velocity from a partially asymmetric surface set into a coherent suite where: + +- tablets can run real field workflows +- phones can monitor and control the edge of the system +- Oracle can serve structured templates across all surfaces +- inventory can be loaded and maintained systematically +- admins can operate the whole suite safely + +## 2. Target Users + +- realtor and brand representatives +- internal operator teams +- property sales directors +- platform admins +- content and data operators + +## 3. Core Product Outcomes + +### 3.1 iPad App + +- finish the residual pages +- update components to match current product direction + +### 3.2 iPhone Edge App + +- narrow mobile control surface +- lead and communication awareness +- alerts, status, notes, and fast actions +- communication-memory reminders and follow-up surfaces +- dedicated business communications surface for WhatsApp messaging and calling where supported +- cellular-call context and recording-import flow +- consent-based recording import and transcription workflows where supported +- provider-routed business communication ingestion where direct device capture is unavailable +- personal calendar surface for the assigned user + +### 3.3 Android Tablet App + +- same structural product as iPad +- Android-native presentation and layout discipline + +### 3.4 Android Phone Edge App + +- Android counterpart of iPhone edge app +- same product scope, platform-specific implementation +- optional deeper capture hooks only where default-handler, enterprise, or approved provider conditions make them defensible + +### 3.5 Oracle Template Product + +- chaptered template book +- subchapter categorization +- seed JSON template database +- later synthetic expansion through Kimi + +### 3.6 Inventory Pipeline + +- operator-friendly add/edit flow +- backend ingestion path +- future client self-service readiness + +### 3.7 Admin Control Plane + +- login +- status visibility +- installation and debugging surface +- system operation tooling + +### 3.8 Calendar and Insight Loop + +- exclusive user calendar +- reminders derived from conversations +- transcript insight extraction +- CRM and QD-score update recommendations + +## 4. Product Boundaries + +In scope: + +- native and near-native product surfaces +- backend-facing contracts +- schema and template expansion +- admin operations surface +- communication memory built from supported capture, supported provider APIs, and operator-assisted imports +- user calendar derived from communication outcomes +- transcript and speaker-segregation pipeline + +Out of scope for this run: + +- unsupported device-level interception features +- uncontrolled consumer messaging hacks +- replacing the current backend architecture + +## 5. Success Criteria + +- iPad residual work is clearly closed or ticketed with acceptance +- Android tablet plan maps directly to iPad structure +- phone edge apps have bounded, platform-realistic MVP scope +- communication-memory strategy is explicit per channel, not hand-waved +- user calendar and insight loop are explicitly modeled +- Oracle template book and DB model are explicit +- inventory ingestion path is defined and buildable +- admin control plane scope is concrete + +## 6. Bottom Line + +The value of this work is not more screens. The value is a coherent, device-aware, Oracle-backed operating suite that can be extended without multiplying technical debt. diff --git a/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/03 - SRS_ Multi-Surface Platform and Oracle Expansion.md b/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/03 - SRS_ Multi-Surface Platform and Oracle Expansion.md new file mode 100644 index 00000000..b607b3b2 --- /dev/null +++ b/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/03 - SRS_ Multi-Surface Platform and Oracle Expansion.md @@ -0,0 +1,84 @@ +# Software Requirements Specification (SRS)_ Multi-Surface Platform and Oracle Expansion + +**Date:** 2026-04-16 +**Status:** Active technical baseline +**Owner:** Sayan +**Reviewers:** Sagnik, frontend lead, backend lead +**Scope:** Technical requirements for tablet apps, phone edge apps, Oracle template DB, inventory ingestion, and admin control plane +**Purpose:** Define the system requirements, interfaces, constraints, and acceptance boundaries for implementation. + +## 1. Purpose + +Specify the technical system needed to extend Velocity across tablets, phone edge apps, Oracle template infrastructure, inventory ingestion, and admin operations without breaking the current root architecture. + +## 2. System Context + +The system lives inside current Project Velocity: + +- `Project_Velocity/app/` for WebOS +- `Project_Velocity/backend/` for backend authority +- `Project_Velocity/iOS/` for current native tablet reference +- `Project_Velocity/db assets/Inventory/` for current seed data + +## 3. Assumptions and Constraints + +- backend remains canonical +- Oracle route and schema direction already exists +- current iPad app is the tablet reference implementation +- Android and phone apps must not create a second backend center +- mobile capture must follow supported platform and consent paths + +## 4. Functional Requirements + +- preserve a single backend authority in `Project_Velocity/backend/` +- support completion of the existing iPad app residual pages and component updates +- support an Android tablet product with the same feature architecture as the iPad app +- support phone edge apps as narrow operator surfaces +- support durable communication-memory capture, import, retrieval, and reminder generation +- support per-user calendar entities and calendar action flows +- support transcription with speaker segregation for supported recordings +- support downstream insight extraction for CRM and QD-score recommendations +- support Oracle template catalog persistence +- support Kimi Synthetic Data as a follow-on program using the seed template DB +- support inventory property ingest and edit workflows +- support an admin control plane + +## 5. Interface Requirements + +- existing backend auth APIs +- Oracle route surface in `routes_oracle.py` and `oracle/router_v1.py` +- inventory APIs to be added through current backend +- admin APIs to be added through current backend +- business telephony and messaging provider adapters where channel support exists +- calendar APIs through the current backend +- transcript processing interfaces for diarized transcript outputs + +## 6. Data Model + +Core entities: + +- `surface_sessions` +- `edge_events` +- `communication_memory_facts` +- `calendar_events` +- `transcription_jobs` +- `transcript_segments` +- `insight_recommendations` +- `inventory_properties` +- `inventory_media_assets` +- `oracle_template_chapters` +- `oracle_template_subchapters` +- `oracle_component_templates` +- `oracle_template_examples` +- `admin_action_events` + +## 7. Acceptance Criteria + +The engineering gate is met when: + +- all workstreams are mapped to real files, contracts, and schema +- no parallel schema center is introduced +- phone edge scope is technically defensible +- communication-memory flow exists for supported channels and fallback paths +- calendar and transcript-intelligence flow exist as first-class technical requirements +- Oracle template DB seed path is explicit diff --git a/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/04 - Sprint Plan_ Multi-Surface Platform and Oracle Expansion.md b/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/04 - Sprint Plan_ Multi-Surface Platform and Oracle Expansion.md new file mode 100644 index 00000000..ae8e1078 --- /dev/null +++ b/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/04 - Sprint Plan_ Multi-Surface Platform and Oracle Expansion.md @@ -0,0 +1,73 @@ +# Sprint Plan_ Multi-Surface Platform and Oracle Expansion + +**Date:** 2026-04-16 +**Status:** Sprint definition +**Owner:** Sayan +**Reviewers:** Sagnik and integration reviewers +**Scope:** Sequence the implementation so contracts and backend truths land before broad UI divergence. + +## 1. Sprint Goal + +Create a safe execution path for the multi-surface expansion by landing shared contracts, schema direction, and ownership boundaries before device-specific implementation fragments. + +## 2. Sprint Milestones + +### Milestone 1: Orientation and inventory of current truth + +- audit current iPad app +- audit WebOS modules +- audit Oracle routes and schema +- audit inventory corpus and current backend shapes + +### Milestone 2: Shared contract and schema baseline + +- define shared entities +- define Oracle template chapter model +- define inventory ingest contracts +- define admin auth and role boundaries + +### Milestone 3: Tablet and phone architecture split + +- finalize iPad residual list +- define Android tablet parity map +- define phone edge MVP scope + +### Milestone 4: Backend route and data readiness + +- extend current backend routes and schema +- avoid parallel service drift + +### Milestone 5: Surface implementation starts + +- iPad residual completion +- Android tablet scaffold +- phone edge MVP scaffold +- admin control plane scaffold + +## 3. Mandatory Order + +The team should not start with broad mobile UI polish. + +Correct order: + +1. contracts +2. schema +3. backend routes +4. tablet parity map +5. phone edge MVP map +6. UI implementation + +## 4. Deliverables This Sprint Must Produce + +- filled architectural packet +- real file ownership map +- Oracle schema extension plan +- template book outline +- inventory ingest plan +- admin surface plan + +## 5. Known Dependencies + +- inventory source files from Sagnik are still pending +- communications ingestion provider choices may still need final confirmation +- Android implementation should not start deep UI work before iPad parity mapping is complete diff --git a/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/05 - Implementation Blueprint_ Multi-Surface Apps WebOS Edge and Oracle Delivery.md b/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/05 - Implementation Blueprint_ Multi-Surface Apps WebOS Edge and Oracle Delivery.md new file mode 100644 index 00000000..7e1cfaf4 --- /dev/null +++ b/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/05 - Implementation Blueprint_ Multi-Surface Apps WebOS Edge and Oracle Delivery.md @@ -0,0 +1,90 @@ +# Implementation Blueprint_ Multi-Surface Apps WebOS Edge and Oracle Delivery + +**Date:** 2026-04-16 +**Status:** Implementation map +**Owner:** Sayan +**Reviewers:** Sagnik, frontend/backend integration reviewers +**Scope:** Repository mapping and execution boundaries for the multi-surface workstream. + +## 1. Repository Centers To Respect + +- WebOS: `Project_Velocity/app/` +- backend: `Project_Velocity/backend/` +- iPad native: `Project_Velocity/iOS/velocity/velocity/` +- inventory assets: `Project_Velocity/db assets/Inventory/` + +## 2. Recommended Workstream Ownership + +### WebOS and shared web contracts + +- `Project_Velocity/app/src/` +- focus: + - shared UI semantics + - Oracle and inventory contracts + - admin control plane surface + +### iPad residual completion + +- `Project_Velocity/iOS/velocity/velocity/Features/` +- focus: + - Dashboard + - Inventory + - Oracle + - Sentinel + - Settings + +### Android tablet + +Recommended new path: + +- `Project_Velocity/android-tablet/` + +### iPhone edge + +Recommended new path: + +- `Project_Velocity/iOS/velocity-edge-phone/` + +### Android phone edge + +Recommended new path: + +- `Project_Velocity/android-edge-phone/` + +### backend extension + +- `Project_Velocity/backend/api/` +- `Project_Velocity/backend/oracle/` +- `Project_Velocity/backend/services/` + +## 3. Backend Extension Areas + +Recommended route families: + +- `routes_mobile_edge.py` +- `routes_inventory.py` +- existing Oracle route extension instead of Oracle route replacement +- `routes_admin_surface.py` + +## 4. Schema Extension Areas + +Existing Oracle schema reference: + +- `Project_Velocity/backend/oracle/schema_oracle.sql` + +Recommended additions: + +- template chapter tables +- template example tables +- inventory property and ingestion tables +- transcription job and communication event tables where approved +- admin action audit tables + +## 5. Merge Safety Rule + +Keep ownership disjoint: + +- tablet native work isolated by platform directory +- backend route additions isolated by route family +- schema changes staged and reviewed centrally +- WebOS changes limited to the specific modules under Sayan scope diff --git a/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/06 - Execution Backlog_ Multi-Surface Apps Oracle Templates Inventory Admin.md b/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/06 - Execution Backlog_ Multi-Surface Apps Oracle Templates Inventory Admin.md new file mode 100644 index 00000000..8e17a7cc --- /dev/null +++ b/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/06 - Execution Backlog_ Multi-Surface Apps Oracle Templates Inventory Admin.md @@ -0,0 +1,33 @@ +# Execution Backlog_ Multi-Surface Apps Oracle Templates Inventory Admin + +**Date:** 2026-04-16 +**Status:** Ordered delivery backlog +**Owner:** Sayan +**Purpose:** Convert the workstream into a sequence that minimizes conflict and ambiguity. + +## Backlog Order + +1. Audit current iPad residual pages and components. +2. Audit current iOS app structure and create feature parity matrix. +3. Audit WebOS modules and identify reusable contracts. +4. Extend Oracle schema plan for component coverage across all surfaces. +5. Design template chapter and subchapter taxonomy. +6. Define seed JSON examples for each chapter and subchapter. +7. Define Kimi Synthetic Data downstream generation process. +8. Define inventory property ingest contract and edit contract. +9. Define admin control plane scope and role boundaries. +10. Define phone edge MVP capability set with platform-safe capture constraints. +11. Define communication capture matrix by channel: PSTN, SMS, email, Facebook or Instagram business messaging, WhatsApp business messaging, WhatsApp voice, WhatsApp video. +12. Define memory extraction and reminder pipeline from transcripts, messages, and operator notes. +13. Define Android tablet parity implementation structure. +14. Begin iPad residual implementation. +15. Begin backend route and schema implementation. +16. Begin Android tablet scaffold. +17. Begin iPhone and Android phone edge app scaffolds. +18. Begin admin surface implementation. + +## Known Blockers + +- full property file handoff is pending +- final communications provider approval may still be pending +- some mobile communication expectations may require product narrowing diff --git a/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/07 - Contracts and JSON Schemas_ Templates Inventory Edge Capture.md b/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/07 - Contracts and JSON Schemas_ Templates Inventory Edge Capture.md new file mode 100644 index 00000000..81bd889e --- /dev/null +++ b/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/07 - Contracts and JSON Schemas_ Templates Inventory Edge Capture.md @@ -0,0 +1,175 @@ +# Contracts and JSON Schemas_ Templates Inventory Edge Capture + +**Date:** 2026-04-16 +**Status:** Contract baseline +**Owner:** Sayan +**Purpose:** Define the minimum JSON and contract entities for this workstream. + +## 1. Template Catalog Contracts + +### TemplateChapter + +- `chapterId` +- `name` +- `description` +- `sortOrder` + +### TemplateSubchapter + +- `subchapterId` +- `chapterId` +- `name` +- `description` +- `sortOrder` + +### OracleComponentTemplate + +- `templateId` +- `chapterId` +- `subchapterId` +- `name` +- `componentType` +- `acceptedShapes` +- `jsonTemplate` +- `origin` +- `status` +- `version` + +### OracleTemplateSeedExample + +- `exampleId` +- `templateId` +- `title` +- `exampleJson` +- `qualityNotes` + +## 2. Kimi Synthetic Data Contracts + +### SyntheticExpansionJob + +- `jobId` +- `templateId` +- `chapterId` +- `subchapterId` +- `model` +- `status` +- `requestedCount` +- `acceptedCount` + +## 3. Inventory Contracts + +### InventoryProperty + +- `propertyId` +- `sourceId` +- `projectName` +- `developerName` +- `location` +- `propertyType` +- `priceBands` +- `unitMix` +- `amenities` +- `mediaRefs` +- `status` + +### InventoryImportBatch + +- `batchId` +- `sourceType` +- `submittedBy` +- `status` +- `startedAt` +- `completedAt` + +## 4. Edge Capture Contracts + +### CommunicationEvent + +- `eventId` +- `leadId` +- `channel` +- `direction` +- `provider` +- `captureMode` +- `timestamp` +- `summary` +- `rawReference` +- `recordingRef` + +### CommunicationMemoryFact + +- `factId` +- `leadId` +- `eventId` +- `factType` +- `factText` +- `effectiveDate` +- `confidence` +- `extractedFrom` + +### TranscriptSegment + +- `segmentId` +- `eventId` +- `speakerLabel` +- `startMs` +- `endMs` +- `text` +- `confidence` + +### CalendarEvent + +- `calendarEventId` +- `ownerUserId` +- `leadId` +- `sourceEventId` +- `title` +- `description` +- `startAt` +- `endAt` +- `status` +- `createdBy` + +### InsightRecommendation + +- `recommendationId` +- `leadId` +- `sourceEventId` +- `recommendationType` +- `summary` +- `suggestedAction` +- `targetSystem` +- `status` + +### TranscriptionJob + +- `transcriptionJobId` +- `eventId` +- `mediaType` +- `status` +- `transcriptRef` +- `consentState` + +## 5. Admin Contracts + +### AdminActionRequest + +- `actionId` +- `actionType` +- `targetType` +- `targetId` +- `requestedBy` +- `payload` + +### AdminActionResult + +- `actionId` +- `status` +- `message` +- `artifacts` + +## 6. Contract Rules + +- all contracts must be backend-owned +- mobile and web surfaces consume, not redefine +- communication capture contracts must include consent and provider provenance diff --git a/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/08 - Adapter Detailed Implementation Spec_ Mobile Edge Inventory Admin.md b/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/08 - Adapter Detailed Implementation Spec_ Mobile Edge Inventory Admin.md new file mode 100644 index 00000000..032c77b3 --- /dev/null +++ b/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/08 - Adapter Detailed Implementation Spec_ Mobile Edge Inventory Admin.md @@ -0,0 +1,85 @@ +# Adapter Detailed Implementation Spec_ Mobile Edge Inventory Admin + +**Date:** 2026-04-16 +**Status:** Implementation-focused adapter spec +**Owner:** Sayan +**Purpose:** Define the first adapter surfaces that need careful treatment. + +## 1. Mobile Edge Adapter + +The phone edge apps should adapt backend communication state into mobile-safe views. + +They should expose: + +- alerts +- lead summary +- note entry +- transcription status +- communication memory reminders +- personal calendar entries and follow-up suggestions +- business WhatsApp conversation and call surface where supported +- quick actions + +They should not assume: + +- unrestricted audio interception +- hidden OS access to every message channel +- unsupported WhatsApp or telephony capture flows + +They should support three capture modes: + +- direct supported API ingestion +- provider-routed server-side ingestion +- operator-assisted import or note confirmation + +They should also surface post-capture intelligence: + +- diarized transcript review +- reminder suggestions +- calendar creation or edit suggestions +- CRM and QD-score recommendation review + +## 2. Inventory Adapter + +The inventory adapter should normalize incoming property material into: + +- canonical property rows +- media references +- amenity sets +- pricing and unit data + +It must support: + +- create +- update +- validation errors +- ingest batch status + +## 3. Oracle Template Adapter + +The Oracle template adapter should convert: + +- chapter and subchapter metadata +- template JSON +- seed examples + +into: + +- catalog query outputs +- synthesis seeds +- reviewable template records + +## 4. Admin Surface Adapter + +The admin surface should adapt backend system health and operational actions into: + +- health cards +- deployment and install state +- queue visibility +- debug event timelines + +## 5. Integration Rules + +- adapters translate existing domain truth +- adapters do not become domain owners +- adapters remain thin where possible diff --git a/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/09 - Oracle Schema and Root API Spec_ Multi-Surface Platform.md b/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/09 - Oracle Schema and Root API Spec_ Multi-Surface Platform.md new file mode 100644 index 00000000..6e31cb42 --- /dev/null +++ b/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/09 - Oracle Schema and Root API Spec_ Multi-Surface Platform.md @@ -0,0 +1,75 @@ +# Oracle Schema and Root API Spec_ Multi-Surface Platform + +**Date:** 2026-04-16 +**Status:** Schema extension direction +**Owner:** Sayan +**Reviewers:** backend and schema reviewers +**Purpose:** Define how the current Oracle and root API shape should be extended for this workstream. + +## 1. Current Truth + +Existing Oracle schema already includes: + +- canvas pages +- revisions +- components +- prompt executions +- component templates +- forks +- merge requests +- lineage +- audit + +Reference: + +- `Project_Velocity/backend/oracle/schema_oracle.sql` + +## 2. Required Extensions + +Recommended additions: + +- `oracle_template_chapters` +- `oracle_template_subchapters` +- `oracle_template_seed_examples` +- `oracle_synthetic_generation_jobs` +- `inventory_properties` +- `inventory_import_batches` +- `inventory_media_assets` +- `edge_communication_events` +- `edge_communication_memory_facts` +- `edge_transcription_jobs` +- `edge_transcript_segments` +- `user_calendar_events` +- `insight_recommendations` +- `admin_action_events` + +## 3. Root API Additions + +Recommended route families: + +- `GET /oracle/template-chapters` +- `GET /oracle/template-subchapters` +- `GET /oracle/component-templates` +- `POST /oracle/component-templates/seed` +- `POST /oracle/component-templates/synthetic-jobs` +- `POST /inventory/import-batches` +- `PATCH /inventory/properties/{propertyId}` +- `GET /mobile-edge/events` +- `GET /mobile-edge/memory` +- `POST /mobile-edge/imports` +- `POST /mobile-edge/notes` +- `GET /mobile-edge/calendar` +- `POST /mobile-edge/calendar` +- `PATCH /mobile-edge/calendar/{calendarEventId}` +- `GET /mobile-edge/transcripts/{eventId}` +- `GET /mobile-edge/insights/{leadId}` +- `GET /admin-surface/health` +- `POST /admin-surface/actions` + +## 4. Schema Rules + +- do not replace current Oracle tables +- extend cleanly +- keep lifecycle and audit fields +- keep provider provenance for communication records +- keep role and tenancy fields where current Oracle patterns already use them diff --git a/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/10 - Shared Surface Module Spec_ WebOS iPad Android Edge.md b/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/10 - Shared Surface Module Spec_ WebOS iPad Android Edge.md new file mode 100644 index 00000000..89064660 --- /dev/null +++ b/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/10 - Shared Surface Module Spec_ WebOS iPad Android Edge.md @@ -0,0 +1,58 @@ +# Shared Surface Module Spec_ WebOS iPad Android Edge + +**Date:** 2026-04-16 +**Status:** Internal module mapping +**Owner:** Sayan +**Purpose:** Define how the multi-surface work should be decomposed at module level. + +## 1. WebOS Modules To Respect + +Current broad modules already exist: + +- Dashboard +- Oracle +- Sentinel +- Inventory +- Catalyst +- Settings + +## 2. iPad Module Families + +Current native families: + +- `Features/Dashboard` +- `Features/Inventory` +- `Features/Oracle` +- `Features/Sentinel` +- `Features/Settings` + +## 3. Android Tablet Module Recommendation + +Mirror the same families: + +- dashboard +- inventory +- oracle +- sentinel +- settings + +## 4. Phone Edge Module Recommendation + +Keep smaller families: + +- auth +- alerts +- lead-summary +- communications +- notes +- transcriptions +- settings + +## 5. Admin Surface Module Recommendation + +- health +- queues +- installs +- debugging +- logs +- operator actions diff --git a/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/11 - Delivery Roles and Ownership Spec_ Mobile Oracle Admin.md b/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/11 - Delivery Roles and Ownership Spec_ Mobile Oracle Admin.md new file mode 100644 index 00000000..f60d3ffa --- /dev/null +++ b/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/11 - Delivery Roles and Ownership Spec_ Mobile Oracle Admin.md @@ -0,0 +1,34 @@ +# Delivery Roles and Ownership Spec_ Mobile Oracle Admin + +**Date:** 2026-04-16 +**Status:** Ownership guide +**Owner:** Sayan +**Purpose:** Prevent merge chaos and ambiguous responsibility across the workstream. + +## 1. Sayan Primary Ownership + +Sayan owns: + +- iPad residual scope definition and completion plan +- Android tablet parity plan and implementation base +- phone edge app product framing and initial implementation +- Oracle template book and seed template DB direction +- inventory ingest pipeline planning +- admin control surface planning + +## 2. Integration Ownership + +Backend owners retain authority over: + +- auth +- route architecture +- schema review +- Oracle canonical persistence + +## 3. Merge Discipline + +- narrow PRs by workstream +- do not reformat unrelated files +- do not rename broad app trees without review +- isolate backend route additions by route family +- isolate schema changes and get review before stacking UI on top diff --git a/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/12 - Deployment Operations and Release Readiness_ Multi-Surface Platform.md b/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/12 - Deployment Operations and Release Readiness_ Multi-Surface Platform.md new file mode 100644 index 00000000..6b92e2a9 --- /dev/null +++ b/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/12 - Deployment Operations and Release Readiness_ Multi-Surface Platform.md @@ -0,0 +1,31 @@ +# Deployment Operations and Release Readiness_ Multi-Surface Platform + +**Date:** 2026-04-16 +**Status:** Operations planning artifact +**Owner:** Sayan +**Purpose:** Define what release readiness means for this work. + +## 1. Minimum Deployment Readiness + +- backend schema migrations reviewed +- route additions tested +- tablet builds run locally +- phone edge builds run locally +- admin surface gated by auth + +## 2. Operational Safety + +- communication ingestion features must have explicit provider and consent assumptions documented +- admin actions must be auditable +- inventory imports must be replayable + +## 3. Demo Readiness + +- tablets must show coherent flows +- phone edge apps must show real operator value +- Oracle template book must look like a governed catalog, not raw JSON dumps +- admin control panel must show health and bounded actions + +## 4. Rollout Rule + +Do not ship phone communication features before the supported provider path is technically proven. diff --git a/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/13 - Implementation Ticket Breakdown and Dependency Matrix_ Multi-Surface Platform.md b/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/13 - Implementation Ticket Breakdown and Dependency Matrix_ Multi-Surface Platform.md new file mode 100644 index 00000000..7947d33d --- /dev/null +++ b/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/13 - Implementation Ticket Breakdown and Dependency Matrix_ Multi-Surface Platform.md @@ -0,0 +1,72 @@ +# Implementation Ticket Breakdown and Dependency Matrix_ Multi-Surface Platform + +**Date:** 2026-04-16 +**Status:** Ticket slicing artifact +**Owner:** Sayan +**Purpose:** Break the work into merge-safe chunks. + +## Ticket Matrix + +### T-01 Current iPad residual audit + +- depends on: current iOS repo review +- produces: residual page list and component gap list + +### T-02 Shared contract package + +- depends on: backend and Oracle route review +- produces: shared JSON and API contracts + +### T-03 Oracle template taxonomy + +- depends on: shared contract package +- produces: chapter and subchapter map + +### T-04 Oracle template seed DB + +- depends on: taxonomy +- produces: seed examples for all current categories + +### T-05 Kimi Synthetic Data downstream plan + +- depends on: seed DB +- produces: synthetic expansion job definition + +### T-06 Inventory ingest schema and API + +- depends on: backend schema review +- produces: import and edit route plan + +### T-07 Admin control plane schema and routes + +- depends on: shared contracts +- produces: admin route and action model + +### T-08 Android tablet parity scaffold + +- depends on: iPad audit +- produces: Android tablet app structure + +### T-09 iPhone edge app MVP + +- depends on: phone edge contract and provider constraints +- produces: iPhone edge scaffold + +### T-10 Android phone edge app MVP + +- depends on: phone edge contract and provider constraints +- produces: Android phone scaffold + +### T-11 iPad residual implementation + +- depends on: iPad audit +- produces: actual page and component completion + +### T-12 Admin surface implementation + +- depends on: admin route definitions +- produces: operable admin UI + +## Dependency Rule + +Contract and schema tickets must land before broad UI implementation tickets. diff --git a/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/14 - Platform Reality and Communications Capture Strategy.md b/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/14 - Platform Reality and Communications Capture Strategy.md new file mode 100644 index 00000000..bc3711ec --- /dev/null +++ b/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/14 - Platform Reality and Communications Capture Strategy.md @@ -0,0 +1,233 @@ +# Platform Reality and Communications Capture Strategy + +**Date:** 2026-04-17 +**Status:** Second-pass reality check and execution guardrail +**Owner:** Sagnik +**Primary Reader:** Sayan +**Purpose:** Keep the communication-memory requirement fully in scope while removing technically false assumptions about what iPhone, Android, telephony, and messaging platforms will actually allow. + +## 1. Business Requirement + +The requirement is valid and important: + +- remember who called or messaged +- remember what they said +- remember what was promised +- remember when to follow up +- surface this back to the operator later as reminders and context + +This requirement stays in scope. + +What changes is the implementation assumption. + +The product should not be specified as "the app can always directly record every call and read every message thread on the phone." That assumption is false on multiple platforms. + +For this Sprint 1 planning pass, the active priority channels are: + +- cellular calls +- WhatsApp messages +- WhatsApp voice calls +- optional later-stage email ingestion + +SMS is not a Sprint 1 focus. + +## 2. What Actually Stops Direct Capture + +### iPhone + +iPhone does not expose a public general-purpose API for third-party apps to directly read iMessage or standard SMS thread history the way a device owner may imagine. CallKit integrates calling apps with the system and lets apps observe call state for app-appropriate scenarios, but it does not turn a third-party business assistant app into a universal recorder for carrier calls or a universal reader for all message content. + +Practical implication: + +- do not design the iPhone edge app around unrestricted Phone app call audio capture +- do not design it around unrestricted iMessage or SMS history extraction +- do design it around supported provider integrations, explicit imports, notes, reminders, and app-owned or business-owned channels +- if cellular call recording is required on iPhone, treat provider-routed or explicit import workflows as the baseline, not silent in-app interception + +### Android + +Android is less restrictive in some areas, but it is still not correct to assume universal call or message access. Deeper integration depends on whether the app is the default dialer, default SMS app, enterprise-managed, paired companion, or using an approved provider path. Even then, capture and storage behavior must be treated as channel-specific and permission-sensitive. + +Practical implication: + +- Android phone edge can go deeper than iPhone in some approved scenarios +- it still must be designed around explicit capability gates, not blanket assumptions + +### WhatsApp + +WhatsApp business integration is real, but it must be treated as business-platform integration, not as a magical full-device backdoor. Messaging support is a first-class supported path. Voice and video capabilities must be treated as provider- and rollout-dependent, with business API constraints checked explicitly before product promises are made. + +Practical implication: + +- supported WhatsApp business messaging is in scope +- use a dedicated business messaging and calling surface in our edge apps and WebOS where WhatsApp Business Platform integration supports it +- do not promise universal WhatsApp call recording as the default implementation assumption +- if voice or video calling support is used, it must be routed through the approved business stack and documented as such + +### Cellular Calling + +Cellular calling remains a valid business requirement, but its implementation path should be treated as channel engineering, not as a generic mobile feature toggle. + +Practical implication: + +- if office-assigned numbers can be moved behind a business telephony provider, that path is preferable because call recording, transcription, and metadata become server-governed +- if office-assigned numbers remain plain carrier numbers, iPhone and Android behavior must be handled separately and any recording path must be explicit and compliant +- an audible recording disclosure is acceptable from a product standpoint, but the recording architecture still needs to be supported by the chosen channel setup + +### Email, Facebook, Instagram, Business Telephony + +These are more realistic channels for durable ingestion because they already support server-side, provider-side, or webhook-based integration patterns. + +Practical implication: + +- these channels should be treated as priority sources for communication memory + +## 3. Correct Product Framing + +The product feature is: + +- communication memory + +The product feature is not: + +- universal hidden interception of all device communications + +That means the app stack should be designed to answer: + +- what happened with this lead +- what reminders should exist +- what transcript, message, or note supports that reminder + +instead of assuming every communication path can be captured identically. + +## 4. Required Three-Mode Strategy + +All communication ingestion must fall into one of these modes. + +### Mode A: Direct Supported Ingestion + +Use this where the platform or provider explicitly supports it. + +Examples: + +- business messaging webhooks +- approved provider call metadata +- app-owned VoIP or in-app communication flows +- approved default-handler scenarios on Android where the team intentionally accepts that role + +### Mode B: Provider-Routed Server-Side Ingestion + +This is the most important path for durable enterprise behavior. + +Examples: + +- business telephony provider records call metadata and recordings server-side +- WhatsApp business messaging enters through provider or business API webhooks +- WhatsApp business calling events and recordings enter through approved business-calling infrastructure where available +- email is mirrored or ingested through mailbox integration +- Facebook and Instagram business messaging arrive through official business APIs + +### Mode C: Operator-Assisted Import and Confirmation + +This is the fallback that keeps the business requirement alive when direct capture is blocked. + +Examples: + +- user uploads a recording +- user confirms a note after a call +- user marks a follow-up promise manually +- system converts note plus metadata into a communication memory fact + +## 5. Product Consequences for Sayan + +### iPhone Edge App + +Treat the iPhone app as: + +- control surface +- lead context viewer +- reminder surface +- business WhatsApp surface where supported +- note and import surface +- provider-routed communication memory viewer + +Do not treat it as a guaranteed universal recorder or inbox scraper. + +### Android Phone Edge App + +Treat the Android app as: + +- the same baseline as iPhone +- plus optional deeper integration only when a specific supported pathway is chosen and documented + +### Backend + +Backend must own: + +- communication event persistence +- transcription job state +- speaker-separated transcript storage +- memory fact extraction +- reminder scheduling +- calendar action suggestions and confirmed calendar writes +- provider provenance +- import workflows + +### Oracle + +Oracle must be able to reason over: + +- communication events +- extracted memory facts +- follow-up dates +- reminder confidence and provenance + +## 6. Channel Matrix Sayan Must Produce + +Sayan should explicitly map, for each channel: + +- can we directly ingest it +- do we need provider routing +- do we only support import or notes +- what consent or policy gate applies +- what reminder data can be extracted + +Minimum channels: + +- PSTN or business telephony calls +- WhatsApp business messages +- WhatsApp voice calls +- optional email integration +- calendar events created from communication-derived follow-ups +- CRM and QD score side effects from confirmed insights +- PSTN or business telephony calls +- WhatsApp business messages +- WhatsApp voice calls + +## 7. Calendar and Insight Consequences + +The communications stack is not complete if it only stores recordings and transcripts. It must also drive: + +- user-exclusive calendar events +- follow-up reminders +- CRM updates +- QD score updates +- operator-facing insight summaries + +NemoClaw should operate on top of stored recordings, transcripts, and extracted memory facts, but only confirmed actions should write into critical systems such as calendar, CRM, and QD score unless product explicitly allows autopilot behavior later. + +## 8. Bottom Line + +Nothing about platform restrictions removes the business need. + +What it removes is lazy product wording. + +The correct plan is: + +- keep communication memory as a critical feature +- model channel reality explicitly +- route what can be routed through supported providers +- capture what can be captured directly +- import and confirm the rest + +That is the version of this feature that can actually ship. diff --git a/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/Introduction for Sayan.md b/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/Introduction for Sayan.md new file mode 100644 index 00000000..4e845975 --- /dev/null +++ b/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/Introduction for Sayan.md @@ -0,0 +1,88 @@ +# Introduction for Sayan_ Multi-Surface Platform and Oracle Expansion + +**Date:** 2026-04-16 +**Status:** Team onboarding artifact +**Primary Reader:** Sayan +**Purpose:** Provide one entry point into the multi-surface expansion work so you can understand what this project is, where it lives, what already exists, and how to approach it without causing architectural drift. + +## 1. What This Project Is + +This project is the next product expansion layer for Project Velocity. + +It is not a greenfield app exercise. It is not a mobile hack sprint. It is not a backend rewrite. + +It is a structured effort to extend the current Velocity product into: + +- a completed iPad field app +- an Android tablet equivalent +- a narrow iPhone edge app +- a narrow Android phone edge app +- a communication-memory and calendar loop for assigned office phones +- a broader Oracle schema and template system +- an inventory loading pipeline +- an admin control plane + +## 2. What Already Exists + +The current repo already has real centers of gravity: + +- WebOS frontend in `Project_Velocity/app/` +- FastAPI backend in `Project_Velocity/backend/` +- native iPad app foundation in `Project_Velocity/iOS/velocity/velocity/` +- Oracle route and schema direction in `Project_Velocity/backend/oracle/` +- inventory seed assets in `Project_Velocity/db assets/Inventory/` + +This means your job is integration-first. + +## 3. Architectural Truth You Should Assume + +Assume these decisions are already closed: + +1. FastAPI backend remains canonical. +2. Oracle remains the current analytical center. +3. iPad is the tablet reference implementation. +4. Android tablet should mirror iPad structure rather than inventing a new product model. +5. Phone edge apps are narrow control and capture surfaces, not full WebOS clones. +6. Template catalog and inventory data must live under current backend ownership. +7. Admin control must be bounded and auditable. +8. Communication memory is a first-class product outcome, but capture must follow supported channels and approved imports. +9. Sprint 1 communication scope is centered on cellular calls, WhatsApp messages, WhatsApp voice calls, and calendar-driven follow-up. + +## 4. Why This Work Matters + +Right now Velocity has asymmetry: + +- WebOS is broad +- iPad is partial but real +- Android tablet is absent +- phone edge surfaces are absent +- Oracle template strategy is incomplete +- inventory ingestion is not yet normalized +- admin operations are too diffuse + +If those remain unresolved, the product stays harder to deploy, demo, and scale. + +## 5. Where To Focus First + +The highest-value docs for you are: + +- [Sayan Work Assignment_ Multi-Surface Platform and Oracle Expansion.md](./Sayan%20Work%20Assignment_%20Multi-Surface%20Platform%20and%20Oracle%20Expansion.md) +- [Sayan Work Assignment_ Sprint 1 Execution Slice.md](./Sayan%20Work%20Assignment_%20Sprint%201%20Execution%20Slice.md) +- [14 - Platform Reality and Communications Capture Strategy.md](./14%20-%20Platform%20Reality%20and%20Communications%20Capture%20Strategy.md) +- [05 - Implementation Blueprint_ Multi-Surface Apps WebOS Edge and Oracle Delivery.md](./05%20-%20Implementation%20Blueprint_%20Multi-Surface%20Apps%20WebOS%20Edge%20and%20Oracle%20Delivery.md) +- [09 - Oracle Schema and Root API Spec_ Multi-Surface Platform.md](./09%20-%20Oracle%20Schema%20and%20Root%20API%20Spec_%20Multi-Surface%20Platform.md) +- [07 - Contracts and JSON Schemas_ Templates Inventory Edge Capture.md](./07%20-%20Contracts%20and%20JSON%20Schemas_%20Templates%20Inventory%20Edge%20Capture.md) +- [13 - Implementation Ticket Breakdown and Dependency Matrix_ Multi-Surface Platform.md](./13%20-%20Implementation%20Ticket%20Breakdown%20and%20Dependency%20Matrix_%20Multi-Surface%20Platform.md) + +## 6. What You Must Not Do + +- do not create a second backend authority +- do not create a second schema center +- do not assume unsupported mobile call or message capture paths +- do not confuse "critical feature" with "unsupported direct OS interception" +- do not build merge-hostile broad rewrites +- do not treat phone edge apps as mini WebOS copies + +## 7. Bottom Line + +Your task is to make the current Velocity product more complete across tablets, phones, Oracle data assets, and admin operations without breaking the current root. diff --git a/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/Sayan Work Assignment_ Multi-Surface Platform and Oracle Expansion.md b/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/Sayan Work Assignment_ Multi-Surface Platform and Oracle Expansion.md new file mode 100644 index 00000000..20098fe5 --- /dev/null +++ b/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/Sayan Work Assignment_ Multi-Surface Platform and Oracle Expansion.md @@ -0,0 +1,183 @@ +# Sayan Work Assignment_ Multi-Surface Platform and Oracle Expansion + +**Date:** 2026-04-16 +**Status:** Active assignment handoff +**Assignee:** Sayan +**Purpose:** Define the exact work Sayan owns in this run, including current repository truths, scope boundaries, sequence, and merge discipline. + +## 1. Assignment Summary + +You are being assigned the multi-surface product expansion lane for Project Velocity. + +This includes: + +1. iPad app residual pages and component updates +2. iPhone edge app +3. Android tablet parity app +4. Android phone edge app +5. Oracle schema coverage for these component classes +6. Oracle template book and seed JSON template DB +7. Kimi Synthetic Data downstream plan +8. inventory loading and edit pipeline +9. admin control panel surface +10. communication-memory strategy across supported business channels +11. user calendar and transcript-intelligence path for NemoClaw-assisted follow-up + +This is a broad assignment, but it is not a freeform rewrite. + +## 2. Current Repo Truth You Must Respect + +### WebOS + +- `Project_Velocity/app/` + +Relevant current files: + +- `Project_Velocity/app/src/App.tsx` +- `Project_Velocity/app/src/app/oracle/page.tsx` +- `Project_Velocity/app/src/hooks/useCrmBootstrap.ts` +- `Project_Velocity/app/src/components/modules/` + +### iPad app + +- `Project_Velocity/iOS/velocity/velocity/` + +Relevant current feature folders: + +- `Features/Dashboard` +- `Features/Inventory` +- `Features/Oracle` +- `Features/Sentinel` +- `Features/Settings` + +### Backend and Oracle + +- `Project_Velocity/backend/` + +Relevant files: + +- `Project_Velocity/backend/main.py` +- `Project_Velocity/backend/api/routes_oracle.py` +- `Project_Velocity/backend/api/routes_crm.py` +- `Project_Velocity/backend/oracle/router_v1.py` +- `Project_Velocity/backend/oracle/schema_oracle.sql` + +### Inventory source corpus + +- `Project_Velocity/db assets/Inventory/` + +## 3. Your Concrete Scope + +### 3.1 iPad App + +You own: + +- residual page audit +- component gap audit +- completion plan +- implementation of the open gaps you can safely land + +### 3.2 Android Tablet App + +You own: + +- parity map against current iPad information architecture +- recommended project structure +- initial implementation scaffold + +### 3.3 Phone Edge Apps + +You own: + +- iPhone edge app definition +- Android phone edge app definition +- shared edge MVP scope +- explicit platform-safe feature boundaries + +These phone apps are not full WebOS copies. They are compact operator surfaces for: + +- quick lead and communication awareness +- alerts +- note capture +- transcription visibility +- communication-memory reminders +- calendar follow-up actions +- approved communication control hooks + +### 3.4 Oracle Template System + +You own: + +- template chapter taxonomy +- subchapter taxonomy +- seed JSON examples +- DB model proposal for storing them + +### 3.5 Kimi Synthetic Data + +You own the downstream plan. The expected output is a separate Kimi Synthetic Data workstream definition that consumes the template seed DB. + +### 3.6 Inventory Loading Pipeline + +You own: + +- schema and contract planning +- operator-friendly add/edit flow planning +- ingest pipeline planning + +Sagnik still needs to provide additional property source files. Build the pipeline shape now, not fake completeness. + +### 3.7 Admin Control Panel + +You own: + +- admin surface scope +- route and action plan +- implementation blueprint + +## 4. What You Must Read First + +1. [Introduction for Sayan.md](./Introduction%20for%20Sayan.md) +2. [Sayan Work Assignment_ Sprint 1 Execution Slice.md](./Sayan%20Work%20Assignment_%20Sprint%201%20Execution%20Slice.md) +3. [14 - Platform Reality and Communications Capture Strategy.md](./14%20-%20Platform%20Reality%20and%20Communications%20Capture%20Strategy.md) +4. [01 - First Principles_ Multi-Surface Platform and Oracle Expansion.md](./01%20-%20First%20Principles_%20Multi-Surface%20Platform%20and%20Oracle%20Expansion.md) +5. [05 - Implementation Blueprint_ Multi-Surface Apps WebOS Edge and Oracle Delivery.md](./05%20-%20Implementation%20Blueprint_%20Multi-Surface%20Apps%20WebOS%20Edge%20and%20Oracle%20Delivery.md) +6. [07 - Contracts and JSON Schemas_ Templates Inventory Edge Capture.md](./07%20-%20Contracts%20and%20JSON%20Schemas_%20Templates%20Inventory%20Edge%20Capture.md) +7. [09 - Oracle Schema and Root API Spec_ Multi-Surface Platform.md](./09%20-%20Oracle%20Schema%20and%20Root%20API%20Spec_%20Multi-Surface%20Platform.md) +8. [13 - Implementation Ticket Breakdown and Dependency Matrix_ Multi-Surface Platform.md](./13%20-%20Implementation%20Ticket%20Breakdown%20and%20Dependency%20Matrix_%20Multi-Surface%20Platform.md) + +## 5. What You Must Not Do + +- do not create a second backend center +- do not create a second Oracle schema truth +- do not build phone capture around unsupported or hidden OS behavior +- do not assume a managed work phone automatically grants system-level message or call access +- do not casually rewrite active WebOS modules owned by others +- do not block other branches by broad refactors + +## 6. Collaboration and Merge Discipline + +Respect each other's code. + +- narrow PRs by workstream +- do not reformat unrelated files +- do not rename broad folders casually +- isolate backend route additions by route family +- isolate schema changes and get review before stacking UI on top + +## 7. Recommended Execution Order + +1. audit current truth +2. define shared contracts +3. define schema additions +4. define iPad residual list +5. define Android tablet parity map +6. define phone edge MVPs with platform-safe constraints +7. define template book and DB seed plan +8. define inventory ingest path +9. define admin control plane +10. implement in bounded slices + +## 8. Bottom Line + +Your job is to turn the current Velocity codebase into a more complete multi-surface product without breaking the existing center of gravity. Start from contracts and schema. Then build the surfaces. diff --git a/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/Sayan Work Assignment_ Sprint 1 Execution Slice.md b/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/Sayan Work Assignment_ Sprint 1 Execution Slice.md new file mode 100644 index 00000000..f729c50a --- /dev/null +++ b/.Agent Context/Sayan Multi-Surface and Oracle Delivery Pack/Sayan Work Assignment_ Sprint 1 Execution Slice.md @@ -0,0 +1,170 @@ +# Sayan Work Assignment_ Sprint 1 Execution Slice + +**Date:** 2026-04-17 +**Status:** Active execution slice +**Assignee:** Sayan +**Purpose:** Reduce a broad multi-surface assignment into the Sprint 1 slice that must be completed before broader implementation fans out. + +## 1. Why This Slice Exists + +Your broader assignment is intentionally large. Sprint 1 should not try to fully build every tablet, every phone surface, the entire Oracle template library, the complete inventory pipeline, and the full admin system simultaneously. + +Sprint 1 must establish the execution truth that later implementation will build on. + +The active communications priority for this slice is: + +- cellular calls +- WhatsApp messages +- WhatsApp voice calls +- user calendar integration +- transcript and insight pipeline + +SMS is deferred. + +## 2. Sprint 1 Deliverables + +### 2.1 iPad Residual Audit + +Produce: + +- page-by-page residual inventory +- component gap list +- acceptance criteria for each unfinished area +- safe implementation order + +### 2.2 Android Tablet Parity Map + +Produce: + +- information-architecture parity against iPad +- screen map +- shared modules versus Android-specific modules +- implementation scaffold recommendation + +### 2.3 Phone Edge MVP Definition + +Produce: + +- shared iPhone and Android phone edge MVP +- channel-by-channel communication capture matrix for cellular calls and WhatsApp +- control-surface scope +- reminder and follow-up scope +- note and import flows +- business WhatsApp inbox and calling surface scope + +This is a product-definition and contract-definition step first, not a blind UI sprint. + +### 2.4 Oracle Template Taxonomy + +Produce: + +- chapter tree +- subchapter tree +- component families +- two to three seed JSON examples per representative class + +### 2.5 Inventory Ingest Contract Draft + +Produce: + +- property ingest entity model +- media ingest model +- operator edit model +- batch import lifecycle +- validation and error surfaces + +### 2.6 Admin Bounded Action List + +Produce: + +- admin roles +- supported actions +- unsafe actions explicitly excluded +- required audit trail events + +### 2.7 Backend Schema Extension Draft + +Produce: + +- Oracle schema additions +- mobile-edge route additions +- template DB route additions +- communication-memory entities +- calendar entities and action contracts +- transcript and diarization entities + +### 2.8 Calendar and Agent Action Slice + +Produce: + +- user-exclusive calendar model +- NemoClaw write permissions and confirmation rules +- event creation, edit, and reminder contract +- relationship between communications, reminders, and calendar entries + +## 3. Communication Capture Matrix You Must Deliver + +For each of these channels, classify: + +- supported direct ingestion +- provider-routed ingestion +- operator-assisted import only +- blocked or deferred + +Channels: + +- PSTN or business telephony calls +- WhatsApp business messages +- WhatsApp voice calls +- optional email follow-on note +- calendar writeback trigger path +- CRM or QD-score side effect path +- WhatsApp business messages +- WhatsApp voice calls + +For each channel, also define: + +- consent model +- transcription path +- speaker segregation path +- reminder extraction path +- backend provenance field requirements + +## 4. Mandatory Sprint 1 Output Shape + +By the end of Sprint 1, Sayan should have produced: + +- a concrete parity matrix for iPad and Android tablet +- a bounded edge-app MVP with channel reality accounted for +- schema and contract extensions for communication memory +- schema and contract extensions for calendar and transcript intelligence +- Oracle template taxonomy and seed examples +- inventory ingest contract shape +- admin action boundary list + +## 5. What Sprint 1 Must Not Attempt + +- full cross-platform implementation all at once +- unsupported mobile interception experiments +- broad backend rewrites +- speculative schema expansion without route and contract alignment + +## 6. Sequence + +1. Audit current iPad and WebOS truth. +2. Produce Android tablet parity map. +3. Produce communication capture matrix for cellular and WhatsApp. +4. Produce communication-memory contract additions. +5. Produce calendar model and NemoClaw action rules. +6. Produce Oracle template taxonomy and seed JSON structure. +7. Produce inventory ingest contract draft. +8. Produce admin action boundary list. +9. Only then begin bounded implementation slices. + +## 7. Bottom Line + +Sprint 1 is the execution-shaping sprint. + +If this slice is done correctly, later implementation will be parallelizable and reviewable. + +If this slice is skipped, the broader assignment will fragment into conflicting mobile assumptions, weak schemas, and hard-to-merge surface work. diff --git a/android-edge-phone/app/build.gradle.kts b/android-edge-phone/app/build.gradle.kts new file mode 100644 index 00000000..c26d0fac --- /dev/null +++ b/android-edge-phone/app/build.gradle.kts @@ -0,0 +1,60 @@ +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") + id("org.jetbrains.kotlin.plugin.compose") +} + +android { + namespace = "com.desineuron.velocity.edgephone" + compileSdk = 35 + + defaultConfig { + applicationId = "com.desineuron.velocity.edgephone" + minSdk = 28 + targetSdk = 35 + versionCode = 1 + versionName = "1.0.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = "17" + } + + buildFeatures { + compose = true + } +} + +dependencies { + val composeBom = platform("androidx.compose:compose-bom:2025.02.00") + + implementation(composeBom) + androidTestImplementation(composeBom) + + implementation("androidx.core:core-ktx:1.15.0") + implementation("androidx.activity:activity-compose:1.10.1") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7") + implementation("androidx.navigation:navigation-compose:2.8.5") + implementation("androidx.compose.material3:material3") + implementation("androidx.compose.ui:ui") + implementation("androidx.compose.ui:ui-tooling-preview") + + debugImplementation("androidx.compose.ui:ui-tooling") +} diff --git a/android-edge-phone/app/proguard-rules.pro b/android-edge-phone/app/proguard-rules.pro new file mode 100644 index 00000000..3937d4c2 --- /dev/null +++ b/android-edge-phone/app/proguard-rules.pro @@ -0,0 +1 @@ +# MVP scaffold: no custom rules yet. diff --git a/android-edge-phone/app/src/main/AndroidManifest.xml b/android-edge-phone/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..b12e8ba2 --- /dev/null +++ b/android-edge-phone/app/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + diff --git a/android-edge-phone/app/src/main/java/com/desineuron/velocity/edgephone/MainActivity.kt b/android-edge-phone/app/src/main/java/com/desineuron/velocity/edgephone/MainActivity.kt new file mode 100644 index 00000000..25067502 --- /dev/null +++ b/android-edge-phone/app/src/main/java/com/desineuron/velocity/edgephone/MainActivity.kt @@ -0,0 +1,96 @@ +package com.desineuron.velocity.edgephone + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.navigation.NavDestination.Companion.hierarchy +import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.ui.Modifier +import com.desineuron.velocity.edgephone.features.AlertsScreen +import com.desineuron.velocity.edgephone.features.CommunicationsScreen +import com.desineuron.velocity.edgephone.features.LeadSummaryScreen +import com.desineuron.velocity.edgephone.features.NotesScreen +import com.desineuron.velocity.edgephone.features.SettingsScreen +import com.desineuron.velocity.edgephone.features.TranscriptionsScreen +import com.desineuron.velocity.edgephone.ui.theme.VelocityEdgePhoneTheme + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + VelocityEdgePhoneTheme { + Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) { + EdgePhoneApp() + } + } + } + } +} + +private data class PhoneDestination(val route: String, val label: String) + +private val phoneDestinations = listOf( + PhoneDestination("alerts", "Alerts"), + PhoneDestination("lead-summary", "Lead"), + PhoneDestination("communications", "Comms"), + PhoneDestination("notes", "Notes"), + PhoneDestination("transcriptions", "Transcripts"), + PhoneDestination("settings", "Settings"), +) + +@Composable +private fun EdgePhoneApp() { + val navController = rememberNavController() + val backStackEntry by navController.currentBackStackEntryAsState() + val currentDestination = backStackEntry?.destination + + Scaffold( + bottomBar = { + NavigationBar { + phoneDestinations.forEach { destination -> + val selected = currentDestination?.hierarchy?.any { it.route == destination.route } == true + NavigationBarItem( + selected = selected, + onClick = { + navController.navigate(destination.route) { + popUpTo(navController.graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = true + } + }, + icon = {}, + label = { Text(destination.label) }, + ) + } + } + }, + ) { innerPadding -> + NavHost( + navController = navController, + startDestination = "alerts", + modifier = Modifier.fillMaxSize(), + ) { + composable("alerts") { AlertsScreen(innerPadding) } + composable("lead-summary") { LeadSummaryScreen(innerPadding) } + composable("communications") { CommunicationsScreen(innerPadding) } + composable("notes") { NotesScreen(innerPadding) } + composable("transcriptions") { TranscriptionsScreen(innerPadding) } + composable("settings") { SettingsScreen(innerPadding) } + } + } +} diff --git a/android-edge-phone/app/src/main/java/com/desineuron/velocity/edgephone/features/AlertsScreen.kt b/android-edge-phone/app/src/main/java/com/desineuron/velocity/edgephone/features/AlertsScreen.kt new file mode 100644 index 00000000..6fc5c5fd --- /dev/null +++ b/android-edge-phone/app/src/main/java/com/desineuron/velocity/edgephone/features/AlertsScreen.kt @@ -0,0 +1,14 @@ +package com.desineuron.velocity.edgephone.features + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.Composable + +@Composable +fun AlertsScreen(paddingValues: PaddingValues) { + PhoneScaffold( + paddingValues = paddingValues, + title = "Alerts", + subtitle = "High-urgency nudges for unread responses, callbacks, and showroom events.", + actionLabel = "Respond to whale-lead unread thread", + ) +} diff --git a/android-edge-phone/app/src/main/java/com/desineuron/velocity/edgephone/features/CommunicationsScreen.kt b/android-edge-phone/app/src/main/java/com/desineuron/velocity/edgephone/features/CommunicationsScreen.kt new file mode 100644 index 00000000..a6e74259 --- /dev/null +++ b/android-edge-phone/app/src/main/java/com/desineuron/velocity/edgephone/features/CommunicationsScreen.kt @@ -0,0 +1,14 @@ +package com.desineuron.velocity.edgephone.features + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.Composable + +@Composable +fun CommunicationsScreen(paddingValues: PaddingValues) { + PhoneScaffold( + paddingValues = paddingValues, + title = "Communications", + subtitle = "Call, WhatsApp, and operator-import touchpoints on a single edge rail.", + actionLabel = "Log a manual note after callback", + ) +} diff --git a/android-edge-phone/app/src/main/java/com/desineuron/velocity/edgephone/features/LeadSummaryScreen.kt b/android-edge-phone/app/src/main/java/com/desineuron/velocity/edgephone/features/LeadSummaryScreen.kt new file mode 100644 index 00000000..5abd886f --- /dev/null +++ b/android-edge-phone/app/src/main/java/com/desineuron/velocity/edgephone/features/LeadSummaryScreen.kt @@ -0,0 +1,14 @@ +package com.desineuron.velocity.edgephone.features + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.Composable + +@Composable +fun LeadSummaryScreen(paddingValues: PaddingValues) { + PhoneScaffold( + paddingValues = paddingValues, + title = "Lead Summary", + subtitle = "Compact account memory, qualification, and next-best action.", + actionLabel = "Review Mohammed Al-Rashid context", + ) +} diff --git a/android-edge-phone/app/src/main/java/com/desineuron/velocity/edgephone/features/NotesScreen.kt b/android-edge-phone/app/src/main/java/com/desineuron/velocity/edgephone/features/NotesScreen.kt new file mode 100644 index 00000000..97913fd8 --- /dev/null +++ b/android-edge-phone/app/src/main/java/com/desineuron/velocity/edgephone/features/NotesScreen.kt @@ -0,0 +1,14 @@ +package com.desineuron.velocity.edgephone.features + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.Composable + +@Composable +fun NotesScreen(paddingValues: PaddingValues) { + PhoneScaffold( + paddingValues = paddingValues, + title = "Notes", + subtitle = "Fast capture for memory facts, objections, and promised follow-ups.", + actionLabel = "Create note with memory extraction hint", + ) +} diff --git a/android-edge-phone/app/src/main/java/com/desineuron/velocity/edgephone/features/PhoneScaffold.kt b/android-edge-phone/app/src/main/java/com/desineuron/velocity/edgephone/features/PhoneScaffold.kt new file mode 100644 index 00000000..8ba71729 --- /dev/null +++ b/android-edge-phone/app/src/main/java/com/desineuron/velocity/edgephone/features/PhoneScaffold.kt @@ -0,0 +1,52 @@ +package com.desineuron.velocity.edgephone.features + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp + +@Composable +fun PhoneScaffold( + paddingValues: PaddingValues, + title: String, + subtitle: String, + actionLabel: String, +) { + Column( + modifier = Modifier + .fillMaxSize() + .background(Color(0xFF05070B)) + .padding(paddingValues) + .padding(18.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Text(title, style = MaterialTheme.typography.headlineSmall, color = Color.White) + Text(subtitle, style = MaterialTheme.typography.bodyMedium, color = Color(0xFF94A3B8)) + + Column( + modifier = Modifier + .fillMaxWidth() + .background(Color(0xFF0B1220), RoundedCornerShape(24.dp)) + .padding(18.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + Text("Edge action", style = MaterialTheme.typography.labelLarge, color = Color(0xFF38BDF8)) + Text(actionLabel, style = MaterialTheme.typography.titleMedium, color = Color.White) + Text( + "This narrow surface is ready for backend hookup to `/api/mobile-edge` once auth and install registration are connected.", + style = MaterialTheme.typography.bodySmall, + color = Color(0xFF94A3B8), + ) + } + } +} diff --git a/android-edge-phone/app/src/main/java/com/desineuron/velocity/edgephone/features/SettingsScreen.kt b/android-edge-phone/app/src/main/java/com/desineuron/velocity/edgephone/features/SettingsScreen.kt new file mode 100644 index 00000000..7e31b6a2 --- /dev/null +++ b/android-edge-phone/app/src/main/java/com/desineuron/velocity/edgephone/features/SettingsScreen.kt @@ -0,0 +1,14 @@ +package com.desineuron.velocity.edgephone.features + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.Composable + +@Composable +fun SettingsScreen(paddingValues: PaddingValues) { + PhoneScaffold( + paddingValues = paddingValues, + title = "Settings", + subtitle = "Install registration, operator identity, and API connection state.", + actionLabel = "Verify surface heartbeat and app version", + ) +} diff --git a/android-edge-phone/app/src/main/java/com/desineuron/velocity/edgephone/features/TranscriptionsScreen.kt b/android-edge-phone/app/src/main/java/com/desineuron/velocity/edgephone/features/TranscriptionsScreen.kt new file mode 100644 index 00000000..7b9a1444 --- /dev/null +++ b/android-edge-phone/app/src/main/java/com/desineuron/velocity/edgephone/features/TranscriptionsScreen.kt @@ -0,0 +1,14 @@ +package com.desineuron.velocity.edgephone.features + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.Composable + +@Composable +fun TranscriptionsScreen(paddingValues: PaddingValues) { + PhoneScaffold( + paddingValues = paddingValues, + title = "Transcriptions", + subtitle = "Imported voice artifacts and segment-level summaries for the field operator.", + actionLabel = "Review pending recording import", + ) +} diff --git a/android-edge-phone/app/src/main/java/com/desineuron/velocity/edgephone/ui/theme/Theme.kt b/android-edge-phone/app/src/main/java/com/desineuron/velocity/edgephone/ui/theme/Theme.kt new file mode 100644 index 00000000..fcdf5f0e --- /dev/null +++ b/android-edge-phone/app/src/main/java/com/desineuron/velocity/edgephone/ui/theme/Theme.kt @@ -0,0 +1,26 @@ +package com.desineuron.velocity.edgephone.ui.theme + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color + +private val EdgeColors = darkColorScheme( + primary = Color(0xFF38BDF8), + secondary = Color(0xFF14B8A6), + tertiary = Color(0xFF22C55E), + background = Color(0xFF05070B), + surface = Color(0xFF0B1220), + onPrimary = Color.White, + onSecondary = Color.White, + onBackground = Color.White, + onSurface = Color.White, +) + +@Composable +fun VelocityEdgePhoneTheme(content: @Composable () -> Unit) { + MaterialTheme( + colorScheme = EdgeColors, + content = content, + ) +} diff --git a/android-edge-phone/build.gradle.kts b/android-edge-phone/build.gradle.kts new file mode 100644 index 00000000..1b4481ac --- /dev/null +++ b/android-edge-phone/build.gradle.kts @@ -0,0 +1,5 @@ +plugins { + id("com.android.application") version "8.7.3" apply false + id("org.jetbrains.kotlin.android") version "2.0.21" apply false + id("org.jetbrains.kotlin.plugin.compose") version "2.0.21" apply false +} diff --git a/android-edge-phone/gradle.properties b/android-edge-phone/gradle.properties new file mode 100644 index 00000000..f0a2e55f --- /dev/null +++ b/android-edge-phone/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +android.useAndroidX=true +kotlin.code.style=official +android.nonTransitiveRClass=true diff --git a/android-edge-phone/settings.gradle.kts b/android-edge-phone/settings.gradle.kts new file mode 100644 index 00000000..e3baae52 --- /dev/null +++ b/android-edge-phone/settings.gradle.kts @@ -0,0 +1,18 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "velocity-android-edge-phone" +include(":app") diff --git a/android-tablet/app/build.gradle.kts b/android-tablet/app/build.gradle.kts new file mode 100644 index 00000000..7a07845f --- /dev/null +++ b/android-tablet/app/build.gradle.kts @@ -0,0 +1,63 @@ +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") + id("org.jetbrains.kotlin.plugin.compose") +} + +android { + namespace = "com.desineuron.velocity.tablet" + compileSdk = 35 + + defaultConfig { + applicationId = "com.desineuron.velocity.tablet" + minSdk = 28 + targetSdk = 35 + versionCode = 1 + versionName = "1.0.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { + useSupportLibrary = true + } + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = "17" + } + + buildFeatures { + compose = true + } +} + +dependencies { + val composeBom = platform("androidx.compose:compose-bom:2025.02.00") + + implementation(composeBom) + androidTestImplementation(composeBom) + + implementation("androidx.core:core-ktx:1.15.0") + implementation("androidx.activity:activity-compose:1.10.1") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7") + implementation("androidx.navigation:navigation-compose:2.8.5") + implementation("androidx.compose.material3:material3") + implementation("androidx.compose.ui:ui") + implementation("androidx.compose.ui:ui-tooling-preview") + + debugImplementation("androidx.compose.ui:ui-tooling") +} diff --git a/android-tablet/app/proguard-rules.pro b/android-tablet/app/proguard-rules.pro new file mode 100644 index 00000000..3937d4c2 --- /dev/null +++ b/android-tablet/app/proguard-rules.pro @@ -0,0 +1 @@ +# MVP scaffold: no custom rules yet. diff --git a/android-tablet/app/src/main/AndroidManifest.xml b/android-tablet/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..fb171bca --- /dev/null +++ b/android-tablet/app/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + diff --git a/android-tablet/app/src/main/java/com/desineuron/velocity/tablet/MainActivity.kt b/android-tablet/app/src/main/java/com/desineuron/velocity/tablet/MainActivity.kt new file mode 100644 index 00000000..94ca1780 --- /dev/null +++ b/android-tablet/app/src/main/java/com/desineuron/velocity/tablet/MainActivity.kt @@ -0,0 +1,117 @@ +package com.desineuron.velocity.tablet + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationRail +import androidx.compose.material3.NavigationRailItem +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.navigation.NavDestination.Companion.hierarchy +import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController +import com.desineuron.velocity.tablet.features.DashboardScreen +import com.desineuron.velocity.tablet.features.InventoryScreen +import com.desineuron.velocity.tablet.features.OracleScreen +import com.desineuron.velocity.tablet.features.SettingsScreen +import com.desineuron.velocity.tablet.features.SentinelScreen +import com.desineuron.velocity.tablet.ui.theme.VelocityTabletTheme + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + VelocityTabletTheme { + Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) { + TabletApp() + } + } + } + } +} + +private data class TabletDestination(val route: String, val label: String) + +private val tabletDestinations = listOf( + TabletDestination("dashboard", "Dashboard"), + TabletDestination("inventory", "Inventory"), + TabletDestination("oracle", "Oracle"), + TabletDestination("sentinel", "Sentinel"), + TabletDestination("settings", "Settings"), +) + +@Composable +private fun TabletApp() { + val navController = rememberNavController() + val backStackEntry by navController.currentBackStackEntryAsState() + val currentDestination = backStackEntry?.destination + + Row( + modifier = Modifier + .fillMaxSize() + .background(Color(0xFF05070B)), + ) { + NavigationRail( + modifier = Modifier.padding(vertical = 20.dp, horizontal = 12.dp), + containerColor = Color(0xFF0D1118), + header = { + Column( + modifier = Modifier.padding(bottom = 20.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text("Velocity", color = Color.White, style = MaterialTheme.typography.titleMedium) + Text("Tablet parity", color = Color(0xFF8A93A6), style = MaterialTheme.typography.labelSmall) + } + }, + ) { + tabletDestinations.forEach { destination -> + val selected = currentDestination?.hierarchy?.any { it.route == destination.route } == true + NavigationRailItem( + selected = selected, + onClick = { + navController.navigate(destination.route) { + popUpTo(navController.graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = true + } + }, + icon = { Box(modifier = Modifier.padding(4.dp)) }, + label = { Text(destination.label) }, + ) + } + } + + NavHost( + navController = navController, + startDestination = "dashboard", + modifier = Modifier + .fillMaxWidth() + .padding(24.dp), + ) { + composable("dashboard") { DashboardScreen() } + composable("inventory") { InventoryScreen() } + composable("oracle") { OracleScreen() } + composable("sentinel") { SentinelScreen() } + composable("settings") { SettingsScreen() } + } + } +} diff --git a/android-tablet/app/src/main/java/com/desineuron/velocity/tablet/features/DashboardScreen.kt b/android-tablet/app/src/main/java/com/desineuron/velocity/tablet/features/DashboardScreen.kt new file mode 100644 index 00000000..491addcd --- /dev/null +++ b/android-tablet/app/src/main/java/com/desineuron/velocity/tablet/features/DashboardScreen.kt @@ -0,0 +1,12 @@ +package com.desineuron.velocity.tablet.features + +import androidx.compose.runtime.Composable + +@Composable +fun DashboardScreen() { + FeatureScaffold( + title = "Dashboard", + subtitle = "Sales, sentiment, and operational posture for the field team.", + chips = listOf("Visitors live", "Revenue outlook", "Queue health"), + ) +} diff --git a/android-tablet/app/src/main/java/com/desineuron/velocity/tablet/features/FeatureScaffold.kt b/android-tablet/app/src/main/java/com/desineuron/velocity/tablet/features/FeatureScaffold.kt new file mode 100644 index 00000000..06efa675 --- /dev/null +++ b/android-tablet/app/src/main/java/com/desineuron/velocity/tablet/features/FeatureScaffold.kt @@ -0,0 +1,64 @@ +package com.desineuron.velocity.tablet.features + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp + +@Composable +fun FeatureScaffold( + title: String, + subtitle: String, + chips: List, +) { + Column( + modifier = Modifier + .fillMaxSize() + .background(Color(0xFF05070B)) + .padding(8.dp), + verticalArrangement = Arrangement.spacedBy(18.dp), + ) { + Column(verticalArrangement = Arrangement.spacedBy(6.dp)) { + Text(title, style = MaterialTheme.typography.headlineMedium, color = Color.White) + Text(subtitle, style = MaterialTheme.typography.bodyMedium, color = Color(0xFF94A3B8)) + } + + Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { + chips.forEach { chip -> + Text( + text = chip, + color = Color.White, + style = MaterialTheme.typography.labelMedium, + modifier = Modifier + .background(Color(0xFF111827), RoundedCornerShape(14.dp)) + .padding(horizontal = 14.dp, vertical = 10.dp), + ) + } + } + + Column( + modifier = Modifier + .fillMaxWidth() + .background(Color(0xFF0B1220), RoundedCornerShape(28.dp)) + .padding(22.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + Text("Scaffold state", style = MaterialTheme.typography.titleMedium, color = Color.White) + Text( + "This screen is wired into the tablet navigation graph and is ready for the shared contract package once the API clients are connected.", + style = MaterialTheme.typography.bodyMedium, + color = Color(0xFF94A3B8), + ) + } + } +} diff --git a/android-tablet/app/src/main/java/com/desineuron/velocity/tablet/features/InventoryScreen.kt b/android-tablet/app/src/main/java/com/desineuron/velocity/tablet/features/InventoryScreen.kt new file mode 100644 index 00000000..a2af1c3f --- /dev/null +++ b/android-tablet/app/src/main/java/com/desineuron/velocity/tablet/features/InventoryScreen.kt @@ -0,0 +1,12 @@ +package com.desineuron.velocity.tablet.features + +import androidx.compose.runtime.Composable + +@Composable +fun InventoryScreen() { + FeatureScaffold( + title = "Inventory", + subtitle = "Property catalog, media assets, and ingest lifecycle visibility.", + chips = listOf("Import batches", "Listings", "Validation state"), + ) +} diff --git a/android-tablet/app/src/main/java/com/desineuron/velocity/tablet/features/OracleScreen.kt b/android-tablet/app/src/main/java/com/desineuron/velocity/tablet/features/OracleScreen.kt new file mode 100644 index 00000000..ca1f3c18 --- /dev/null +++ b/android-tablet/app/src/main/java/com/desineuron/velocity/tablet/features/OracleScreen.kt @@ -0,0 +1,12 @@ +package com.desineuron.velocity.tablet.features + +import androidx.compose.runtime.Composable + +@Composable +fun OracleScreen() { + FeatureScaffold( + title = "Oracle", + subtitle = "Template-guided intelligence views for pipeline and scheduling.", + chips = listOf("Pipeline", "Lead map", "Calendar tasks"), + ) +} diff --git a/android-tablet/app/src/main/java/com/desineuron/velocity/tablet/features/SentinelScreen.kt b/android-tablet/app/src/main/java/com/desineuron/velocity/tablet/features/SentinelScreen.kt new file mode 100644 index 00000000..2b89c54d --- /dev/null +++ b/android-tablet/app/src/main/java/com/desineuron/velocity/tablet/features/SentinelScreen.kt @@ -0,0 +1,12 @@ +package com.desineuron.velocity.tablet.features + +import androidx.compose.runtime.Composable + +@Composable +fun SentinelScreen() { + FeatureScaffold( + title = "Sentinel", + subtitle = "Biometric and sentiment awareness stream for visitor sessions.", + chips = listOf("Live session", "Journey river", "QD overlays"), + ) +} diff --git a/android-tablet/app/src/main/java/com/desineuron/velocity/tablet/features/SettingsScreen.kt b/android-tablet/app/src/main/java/com/desineuron/velocity/tablet/features/SettingsScreen.kt new file mode 100644 index 00000000..bd2b4d94 --- /dev/null +++ b/android-tablet/app/src/main/java/com/desineuron/velocity/tablet/features/SettingsScreen.kt @@ -0,0 +1,12 @@ +package com.desineuron.velocity.tablet.features + +import androidx.compose.runtime.Composable + +@Composable +fun SettingsScreen() { + FeatureScaffold( + title = "Settings", + subtitle = "Surface registration, connection state, and operator preferences.", + chips = listOf("Install info", "API endpoint", "Operator profile"), + ) +} diff --git a/android-tablet/app/src/main/java/com/desineuron/velocity/tablet/ui/theme/Theme.kt b/android-tablet/app/src/main/java/com/desineuron/velocity/tablet/ui/theme/Theme.kt new file mode 100644 index 00000000..3c1329d6 --- /dev/null +++ b/android-tablet/app/src/main/java/com/desineuron/velocity/tablet/ui/theme/Theme.kt @@ -0,0 +1,26 @@ +package com.desineuron.velocity.tablet.ui.theme + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color + +private val VelocityDarkColors = darkColorScheme( + primary = Color(0xFF3B82F6), + secondary = Color(0xFF14B8A6), + tertiary = Color(0xFF22C55E), + background = Color(0xFF05070B), + surface = Color(0xFF0B1220), + onPrimary = Color.White, + onSecondary = Color.White, + onBackground = Color.White, + onSurface = Color.White, +) + +@Composable +fun VelocityTabletTheme(content: @Composable () -> Unit) { + MaterialTheme( + colorScheme = VelocityDarkColors, + content = content, + ) +} diff --git a/android-tablet/build.gradle.kts b/android-tablet/build.gradle.kts new file mode 100644 index 00000000..1b4481ac --- /dev/null +++ b/android-tablet/build.gradle.kts @@ -0,0 +1,5 @@ +plugins { + id("com.android.application") version "8.7.3" apply false + id("org.jetbrains.kotlin.android") version "2.0.21" apply false + id("org.jetbrains.kotlin.plugin.compose") version "2.0.21" apply false +} diff --git a/android-tablet/gradle.properties b/android-tablet/gradle.properties new file mode 100644 index 00000000..f0a2e55f --- /dev/null +++ b/android-tablet/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +android.useAndroidX=true +kotlin.code.style=official +android.nonTransitiveRClass=true diff --git a/android-tablet/settings.gradle.kts b/android-tablet/settings.gradle.kts new file mode 100644 index 00000000..08b7b036 --- /dev/null +++ b/android-tablet/settings.gradle.kts @@ -0,0 +1,18 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "velocity-android-tablet" +include(":app") 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..8eb97bd1 100644 --- a/app/node_modules/.vite/deps/@radix-ui_react-avatar.js +++ b/app/node_modules/.vite/deps/@radix-ui_react-avatar.js @@ -2,23 +2,23 @@ import { useCallbackRef, useLayoutEffect2 -} from "./chunk-GRXJTWBV.js"; -import { - require_react_dom -} from "./chunk-YLZ34CCM.js"; -import { - require_shim -} from "./chunk-642Z5WD3.js"; +} from "./chunk-J4JAFMOP.js"; import { createSlot -} from "./chunk-5HUACAZ7.js"; -import "./chunk-HPBHRBIF.js"; +} from "./chunk-YWBEB5PG.js"; +import "./chunk-2VUH7NEY.js"; +import { + require_shim +} from "./chunk-TXHHHGR3.js"; +import { + require_react_dom +} from "./chunk-YF4B4G2L.js"; import { require_jsx_runtime -} from "./chunk-USXRE7Q2.js"; +} from "./chunk-2YVA4HRZ.js"; import { require_react -} from "./chunk-ZNKPWGXJ.js"; +} from "./chunk-WUR7D6NS.js"; import { __toESM } from "./chunk-G3PMV62Z.js"; 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..406e8a78 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,20 +2,20 @@ import { useCallbackRef, useLayoutEffect2 -} from "./chunk-GRXJTWBV.js"; -import { - require_react_dom -} from "./chunk-YLZ34CCM.js"; +} from "./chunk-J4JAFMOP.js"; import { composeRefs, useComposedRefs -} from "./chunk-HPBHRBIF.js"; +} from "./chunk-2VUH7NEY.js"; +import { + require_react_dom +} from "./chunk-YF4B4G2L.js"; import { require_jsx_runtime -} from "./chunk-USXRE7Q2.js"; +} from "./chunk-2YVA4HRZ.js"; import { require_react -} from "./chunk-ZNKPWGXJ.js"; +} from "./chunk-WUR7D6NS.js"; import { __toESM } from "./chunk-G3PMV62Z.js"; 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 index 6158e38c..bede1ace 100644 --- 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 @@ -1,7 +1,7 @@ { "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