diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..7995d3d
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,18 @@
+name: CI
+on:
+ push:
+ branches: [main]
+ pull_request:
+permissions:
+ contents: read
+jobs:
+ verify:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ cache: npm
+ - run: npm ci
+ - run: npm run check
diff --git a/.gitignore b/.gitignore
index 308f91d..b218da0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,5 @@ playwright-report/
*.log
.DS_Store
Thumbs.db
+release/*.zip
+test-results/
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..97c2e53
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,11 @@
+# Changelog
+
+## 0.1.0 — 2026-08-10
+
+First public-ready developer-mode alpha.
+
+- Added local-first MV3 service worker, schedule engine, rule classifier, run state machine, storage, tab preservation, notifications, and generated audio.
+- Added scheduled/recurring and passive watch profiles, guarded-refresh/observe-only modes, optional per-origin access, watch copy, and automatic schedule start.
+- Added command center, live workspace, watch builder, activity ledger, Queue Lab, settings, and compact side panel.
+- Added queue/challenge navigation lock, prior-queue admission guard, position/ETA extraction, durable absolute ETA, low-ETA notification, and background-discard protection.
+- Added synthetic Queue Lab, 26 unit tests, real extension E2E, CI, package verification, release packaging, and public documentation.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 551e2a0..44d8e33 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,14 +1,31 @@
# Contributing
-QueueScope is being prepared privately before its first public release. Contributions are not yet open.
+QueueScope welcomes retailer-neutral fixes, tests, documentation, accessibility work, and capability-bounded generic features after the repository becomes public.
-Once public development begins, changes should:
+## Local setup
-- Remain retailer-neutral in the core repository.
-- Preserve the automation and privacy boundaries.
-- Add or update tests for every state transition.
-- Include visual evidence for command-center or side-panel changes.
-- Avoid committing captured production pages, responses, credentials, or session material.
-- Keep adapters capability-bounded and documented.
+```powershell
+npm ci
+npm run check
+```
-Detailed development commands will be added with the first implementation phase.
+Useful commands:
+
+- `npm run dev` — Vite UI preview.
+- `npm run test:watch` — focused unit tests.
+- `npm run build` — production MV3 build plus artifact verification.
+- `npm run test:e2e` — load the extension in a disposable visible Chromium profile.
+- `npm run package` — create the unpacked release ZIP and SHA-256 file.
+
+## Change requirements
+
+- Keep the core retailer-neutral.
+- Preserve the privacy and automation boundaries.
+- Add or update tests for state/schedule/rule changes.
+- Use only synthetic fixtures committed to this repository.
+- Include fresh browser screenshots for meaningful UI changes and inspect them at full resolution.
+- Do not commit captured production pages, private responses, credentials, cookies, session material, product identifiers, or provider endpoints.
+- Explain new permissions and keep origin access optional.
+- Do not weaken the queue-lock or prior-queue admission invariants.
+
+Run `npm run check` before opening a pull request. UI changes should also pass `npm run test:e2e` on Windows or document why the visible-browser gate could not run.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..59d870b
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 Noah Hicks
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/PLAN.md b/PLAN.md
index 2771302..f4623da 100644
--- a/PLAN.md
+++ b/PLAN.md
@@ -1,437 +1,78 @@
-# QueueScope public repository plan
+# QueueScope public-release plan
## Objective
-Build a technically real, installable, and visually strong public QueueScope repository without publishing retailer-specific integrations, captured production data, private queue behavior, or operational implementation history.
-
-The repository begins private. Public visibility is a manual owner decision after the final release audit.
-
-## Repository separation
-
-QueueScope uses two independent repositories with different trust boundaries.
-
-| Repository | Visibility | Responsibility |
-|---|---|---|
-| Operational QueueScope | Private | Complete working extension, site-specific adapters, production research, and operational fixtures |
-| QueueScope | Private during development; public only after owner approval | Generic core, adapter SDK, synthetic demonstrations, documentation, reviewed screenshots, and public releases |
-
-Rules:
-
-1. The public repository has fresh Git history.
-2. The operational repository is never added as a Git remote or submodule.
-3. Code moves across the boundary only through a reviewed clean-room extraction.
-4. Retailer names, domains, selectors, item identifiers, provider endpoints, captured responses, and queue identifiers are not copied.
-5. Public screenshots must show neutral Queue Lab or empty-workspace states only.
-6. The owner manually changes repository visibility after the publication checklist passes.
-
-## Product thesis
-
-QueueScope is a local-first browser operations console for scheduled page observation and virtual-queue awareness. It turns inconsistent visible webpage signals into a durable run state, coordinates refresh safely, preserves sensitive tabs, records evidence, and brings the human back when attention is useful.
-
-It assists observation and coordination. It does not automate purchases or circumvent access controls.
-
-## Public feature scope
-
-### Command center
-
-- Overview with current posture, active runs, upcoming watches, passive monitors, and recent outcomes.
-- Live Runs workspace with collapsed-by-default run cards and an expandable evidence timeline.
-- Watches library with copy, edit, delete, arm, pause, and per-watch auto-start controls.
-- Passive Monitors for long-horizon observation without a fixed event window.
-- Insights for local run history, observed timing, and outcome summaries.
-- Queue Lab for deterministic rehearsals.
-- Settings for notifications, sounds, motion, density, history retention, and queue-alert thresholds.
-
-### Scheduling
-
-- One-time schedules.
-- Daily schedules.
-- Weekday and weekend schedules.
-- Custom weekday selection.
-- Optional start and end dates for recurring schedules.
-- Explicit monitoring start and stop times.
-- Optional expected-event time.
-- Baseline cadence.
-- High-attention cadence before and after the expected event.
-- Jitter within user-defined bounds.
-- No arbitrary attempt ceiling inside a valid schedule.
-
-### Run coordination
-
-- One preserved tab per independent watched resource.
-- Parallel runs on the same origin when their resource identities differ.
-- Per-origin refresh serialization to prevent reload bursts.
-- Per-run safety locks for resource-specific queues.
-- Origin-wide safety locks for shared challenges or rate limits.
-- Tab focus and explicit end-run actions.
-- Background discard protection after a queue-sensitive state is detected.
-- Observer recovery after extension restart without automatic navigation.
-
-### Generic observation
-
-- URL-pattern matching.
-- Visible-text rules.
-- CSS-selector presence rules.
-- Attribute and enabled/disabled-state rules.
-- Regex-based position and duration extraction.
-- Queue-entry, waiting, attention, admitted, expired, unavailable, and available states.
-- Evidence confidence and provenance.
-- Absolute provider timestamps when available.
-- Explicit unknown values when the page does not expose a signal.
-
-### Attention and history
-
-- Desktop notifications.
-- Optional local notification sounds.
-- Configurable near-admission threshold.
-- Focus-on-admission preference.
-- Local event timeline.
-- Position and ETA samples.
-- Outcome history with configurable retention.
-- Export and import of neutral watch definitions.
-
-## Explicit non-goals
-
-The public project will not include:
-
-- Automatic cart actions or checkout.
-- CAPTCHA solving or challenge automation.
-- Queue bypassing or multiple queue-entry creation.
-- Proxy rotation, fingerprint spoofing, or identity manipulation.
-- Credential, cookie, or session-token extraction.
-- Undocumented provider API calls.
-- Retailer-specific hidden endpoints.
-- Site-specific purchase automation.
-- Claims that a queue position necessarily equals people ahead.
-
-## Architecture
-
-### Planned workspace
-
-```text
-queuescope/
-├── apps/
-│ ├── extension/
-│ └── demo-lab/
-├── packages/
-│ ├── core/
-│ ├── adapter-sdk/
-│ ├── generic-dom-adapter/
-│ ├── scheduling/
-│ ├── storage/
-│ ├── telemetry/
-│ └── ui/
-├── examples/
-│ ├── product-event/
-│ ├── virtual-queue/
-│ └── multi-resource-event/
-├── assets/
-│ └── screenshots/
-├── docs/
-└── tests/
-```
-
-### Generic adapter contract
-
-The public SDK should make capabilities explicit rather than allow arbitrary privileged behavior.
-
-```ts
-interface QueueScopeAdapter {
- id: string;
- version: string;
- match(context: PageContext): MatchResult;
- observe(context: ReadonlyPageContext): Observation;
- capabilities: {
- readsVisibleDom: boolean;
- supportsGuardedRefresh: boolean;
- supportsPosition: boolean;
- supportsProviderEta: boolean;
- supportsAvailability: boolean;
- };
-}
-```
-
-An adapter returns observations. It cannot purchase, solve challenges, issue private network requests, or silently expand its host permissions.
-
-### Observation model
-
-Every observation should contain:
-
-- Observation timestamp.
-- Canonical page URL with tracking parameters removed.
-- Adapter identifier and version.
-- Classification.
-- Confidence.
-- Human-readable evidence.
-- Optional resource identity.
-- Optional position and position label.
-- Optional absolute provider ETA.
-- Optional availability state.
-- Optional queue phase.
-- Safety-lock recommendation.
-
-Provider ETA values should be stored as absolute timestamps. The UI derives a local countdown so rerenders do not reset the estimate. New provider timestamps may replace old ones only when their provenance is at least as authoritative.
-
-## Generic visual rule builder
-
-The primary public integration surface should be a visual rule builder opened from **Create watch from this tab**.
-
-Initial rule types:
-
-- Element exists or disappears.
-- Exact text appears or disappears.
-- Element becomes enabled or disabled.
-- Attribute changes.
-- Text matches a regular expression.
-- Position is extracted from a capture group.
-- Duration or timestamp is extracted from a capture group.
-- A safety-lock signal appears.
-- An admitted-state signal appears after a prior queue lock.
-
-The builder should provide:
-
-- Live preview against the current tab.
-- Highlighting of the matched element.
-- A plain-language explanation of the rule.
-- A raw JSON editor for advanced users.
-- Validation before saving.
-- Portable import and export.
-- A permission summary before activation.
-
-## Queue Lab
-
-Queue Lab is both a product feature and the public demonstration environment.
-
-Synthetic scenarios:
-
-1. Product page before an event.
-2. Availability transition.
-3. Waiting room with position and provider ETA.
-4. Queue state that forbids refresh.
-5. Challenge or rate-limit safety lock.
-6. Admission transition.
-7. Purchase-window timer without purchase automation.
-8. Expired session.
-9. Several independent resources on one origin.
-10. Shared-origin challenge affecting several runs.
-
-The demo must run entirely on localhost and must be included in automated E2E coverage.
-
-## Design direction
-
-### Visual thesis
-
-A dark operational instrument panel with restrained mint signal lighting, dense but legible typography, and state transitions that feel like a live control room rather than a marketing dashboard.
-
-### Content plan
-
-1. The command center leads with system posture and active attention.
-2. Live Runs explains the current state, evidence, timing, and next human action.
-3. Queue Lab proves safety behavior without external traffic.
-4. The side panel compresses the same operational model without losing critical detail.
-
-### Interaction thesis
-
-- Short staged entrances establish hierarchy without delaying operation.
-- State changes use restrained signal pulses and shared-layout transitions.
-- Expand/collapse, drawers, and hover explanations clarify dense settings while respecting reduced-motion preferences.
-
-### Visual constraints
-
-- One mint action/state accent.
-- Two typefaces maximum.
-- Cards only when the card is an interaction or independent run.
-- Live-run cards collapsed by default with a persistent user preference.
-- No decorative dashboard-card mosaic.
-- Side panel must remain usable at 400–420 CSS pixels.
-- All screenshots must be captured after animations settle and the viewport resets to the top.
-
-## Privacy and permission model
-
-- Local-first storage by default.
-- No cloud account requirement.
-- Optional host permission requested per origin when the user creates or arms a watch.
-- No broad host access on first install unless technically unavoidable and explicitly justified.
-- No cookie permission.
-- No browsing-history permission.
-- No remote code execution.
-- No analytics or telemetry in the first public release.
-- Export is a deliberate user action.
-- Stored URLs are canonicalized and tracking parameters removed.
-- Evidence strings are bounded and sanitized.
-
-## Test strategy
-
-### Unit tests
-
-- Schedule calculation across time zones and daylight-saving boundaries.
-- Recurring schedule normalization.
-- Run-state transitions.
-- Queue-lock monotonicity.
-- Refresh authorization and per-origin coordination.
-- Absolute provider ETA behavior.
-- Position parsing and unknown-state handling.
-- Generic rule validation.
-- URL canonicalization and tracking-parameter removal.
-- Runtime-message validation.
-- Snapshot migration.
-
-### Integration tests
-
-- Extension storage and service-worker restart.
-- Observer reinjection.
-- Optional host-permission request and revocation.
-- Multi-tab run isolation.
-- Side-panel and command-center state synchronization.
-- Notifications and local sound playback.
-
-### Real extension E2E
-
-- Load the production build as an unpacked Manifest V3 extension.
-- Confirm a stable extension identity in the deterministic test build.
-- Exercise every Queue Lab transition.
-- Verify guarded refresh stops before another reload after safety evidence.
-- Verify queue-sensitive tabs become non-discardable.
-- Verify live-run cards start collapsed on both surfaces.
-- Verify the compact side panel at its target width.
-- Verify recurring watch creation, copy, auto-start, and deletion.
-- Capture and manually review release screenshots.
-
-## Implementation phases
-
-### Phase 0 — Repository and trust boundary
-
-- [x] Keep the operational source in a separate private repository.
-- [x] Start the showcase repository with fresh history.
-- [x] Add public-safe screenshots from the live tested prototype.
-- [x] Document the architecture and safety boundary.
-- [ ] Select the final open-source license.
-- [ ] Add repository topics and social preview image.
-
-Exit criterion: the repository contains no retailer-specific source, URLs, fixtures, or private history.
-
-### Phase 1 — Generic core extraction
-
-- [ ] Reimplement the schedule model in the new repository.
-- [ ] Reimplement the run state machine.
-- [ ] Reimplement refresh authorization and safety locks.
-- [ ] Reimplement storage schemas and migrations.
-- [ ] Add unit tests before UI integration.
-
-Exit criterion: generic Queue Lab state transitions pass without browser-specific code.
-
-### Phase 2 — Adapter SDK
-
-- [ ] Define the adapter interface and capability declaration.
-- [ ] Implement bounded visible-DOM observation helpers.
-- [ ] Implement evidence sanitization.
-- [ ] Add adapter conformance tests.
-- [ ] Publish example adapter definitions using only synthetic pages.
-
-Exit criterion: a third party can create a safe adapter without changing the core state machine.
-
-### Phase 3 — Extension runtime
-
-- [ ] Create the Manifest V3 service worker.
-- [ ] Add optional per-origin permissions.
-- [ ] Add observer injection and recovery.
-- [ ] Add preserved-tab management.
-- [ ] Add side-panel registration.
-- [ ] Add local notification and offscreen-audio support.
-
-Exit criterion: the extension completes the Queue Lab journey after a service-worker restart.
-
-### Phase 4 — Public command center
-
-- [ ] Port the neutral design system.
-- [ ] Build Overview, Live Runs, Watches, Passive Monitors, Insights, Queue Lab, and Settings.
-- [ ] Build the compact side panel.
-- [ ] Add the visual rule builder.
-- [ ] Support comfortable and compact density.
-- [ ] Support full and reduced motion.
-- [ ] Verify keyboard and screen-reader operation.
-
-Exit criterion: command-center and side-panel workflows pass E2E at desktop and compact widths.
-
-### Phase 5 — Demonstration suite
-
-- [ ] Build synthetic product-event fixture.
-- [ ] Build virtual-queue fixture.
-- [ ] Build multi-resource fixture.
-- [ ] Add one-click Queue Lab presets.
-- [ ] Record a 30–45 second public demonstration.
-- [ ] Capture final README screenshots and social preview.
-
-Exit criterion: the entire value proposition can be demonstrated without external traffic.
-
-### Phase 6 — Public hardening
-
-- [ ] Run dependency, secret, and full-history scans.
-- [ ] Review extension permissions against actual code paths.
-- [ ] Add Content Security Policy checks.
-- [ ] Add dependency update automation.
-- [ ] Add CI for lint, types, unit tests, build, artifact verification, and E2E.
-- [ ] Verify the packaged extension from a clean browser profile.
-- [ ] Complete accessibility and reduced-motion review.
-
-Exit criterion: every CI and manual release gate passes from a clean clone.
-
-### Phase 7 — Owner review and publication
-
-- [ ] Review the README as an outside visitor.
-- [ ] Review every tracked image at full resolution.
-- [ ] Confirm no retailer or provider appears in tracked text, source, image metadata, or Git history.
-- [ ] Confirm repository visibility is still private.
-- [ ] Choose and add the license.
-- [ ] Create the first signed or annotated release tag.
-- [ ] Prepare the release notes and demonstration video.
+Ship a real, installable, retailer-neutral QueueScope alpha without publishing site-specific integrations, production captures, private queue research, or operational repository history. Keep this repository private until the owner completes the visibility change manually.
+
+## Trust boundary
+
+- The operational build remains in a separate private repository.
+- This repository has independent history and no operational remote or submodule.
+- The public core contains generic scheduling, visible-DOM rules, a safety state machine, and synthetic Queue Lab only.
+- No retailer names, domains, selectors, product identifiers, captured responses, provider endpoints, credentials, cookies, or session data cross the boundary.
+- QueueScope assists observation and attention; consequential actions stay human-controlled.
+
+## v0.1.0 completed scope
+
+- [x] Manifest V3 extension with full command center and compact side panel.
+- [x] Scheduled and passive watch profiles.
+- [x] One-time, daily, weekday, weekend, and custom-weekday recurrence.
+- [x] Start, expected, and stop times with baseline/fast cadence windows.
+- [x] No attempt ceiling inside a valid window or passive run.
+- [x] Guarded-refresh and observe-only modes.
+- [x] Per-origin optional host permission requested on arm/attach.
+- [x] Parallel run model with one preserved tab per watch.
+- [x] Attach to an already-open tab without navigation.
+- [x] Generic visible-text, CSS-selector, position-pattern, and ETA-pattern rules.
+- [x] Monotonic queue/challenge navigation lock.
+- [x] Admission requires prior queue evidence.
+- [x] Absolute provider ETA and local countdown.
+- [x] Non-discardable queue-locked tabs and restart rehydration.
+- [x] Local notifications and generated audio profiles.
+- [x] Copy/edit/delete/arm controls and collapsed older-watch shelf.
+- [x] Collapsed live cards with timing, evidence, position, ETA, and tab actions.
+- [x] Settings for card expansion, density, motion, sounds, retention, and ETA alert.
+- [x] Queue Lab product, queue, challenge, available, and admitted rehearsal.
+- [x] MIT license, CI, unit tests, real extension E2E, package verification, and release archive.
+- [x] Fresh E2E screenshots manually reviewed at desktop and side-panel sizes.
+- [x] Public README, architecture, rule guide, privacy contract, security review, test report, and release checklist.
+
+## Verified product invariants
+
+1. Configured queue or challenge evidence prevents another guarded refresh for that run.
+2. A page cannot become admitted without previous queue/challenge lock evidence.
+3. An attached tab is observe-only and never navigated by QueueScope.
+4. Provider ETA is stored as an absolute target, so UI rerenders do not restart it.
+5. Unknown position or timing remains `Not exposed` / `—`; it is never coerced to zero.
+6. Queue-locked tabs are protected from background discard until the user ends the run.
+7. Runtime messages, evidence, selectors, regular expressions, URLs, and settings are bounded.
+8. The extension has no purchase, checkout, challenge-solving, credential, cookie, history, remote-code, or telemetry capability.
+
+## Publication gate
+
+- [x] Fresh public-intended history only.
+- [x] No operational remote or submodule.
+- [x] No site-specific source, fixtures, screenshots, URLs, or identifiers.
+- [x] No local absolute paths or usernames in tracked product files.
+- [x] Dependency audit reports zero known vulnerabilities.
+- [x] Secrets and remote-code patterns scanned.
+- [x] Manifest permissions and CSP reviewed against implementation.
+- [x] Lint, TypeScript, 26 unit tests, build, artifact verification, and E2E pass.
+- [x] Clean disposable browser profile validates the unpacked extension.
+- [x] Side panel reviewed at 420×840 with pinned footer and collapsed run.
+- [x] README screenshots regenerated from the release build and reviewed.
+- [x] MIT license selected and present.
+- [x] Repository remains private for owner review.
- [ ] Owner manually changes visibility to public.
-- [ ] Verify the anonymous public view immediately afterward.
-
-Exit criterion: the repository is public only after explicit owner approval and anonymous readback.
-
-## Final publication checklist
-
-The owner should not make the repository public until all items below are complete.
-
-### Source and history
-
-- [ ] Fresh history contains only public-intended files.
-- [ ] No operational repository remote or submodule exists.
-- [ ] Full-history secret scan passes.
-- [ ] No retailer domains, names, selectors, item IDs, endpoints, or captured responses are present.
-- [ ] No local absolute paths or usernames are present.
-
-### Product and safety
-
-- [ ] The extension never purchases or completes a consequential action.
-- [ ] The extension does not bypass a queue or challenge.
-- [ ] Optional permissions match documented behavior.
-- [ ] Queue and challenge evidence always defeats refresh cadence.
-- [ ] Unknown values remain visibly unknown.
-
-### Quality
-
-- [ ] Lint, types, unit tests, build, artifact verification, and E2E pass.
-- [ ] Clean unpacked installation passes.
-- [ ] Side panel is reviewed at 400–420 CSS pixels.
-- [ ] Screenshots are current and visually reviewed.
-- [ ] README links and image paths work from GitHub.
-- [ ] Accessibility and reduced-motion checks pass.
-
-### Repository settings
-
-- [ ] Repository remains private until the owner approves publication.
-- [ ] Default branch protection is configured.
-- [ ] Secret scanning and dependency alerts are enabled where available.
-- [ ] Issues, Discussions, and security reporting are configured intentionally.
-- [ ] License is selected and present.
-- [ ] Social preview and repository description are set.
-
-## Decisions reserved for owner review
-
-1. Final license: Apache-2.0 is recommended for portfolio visibility and adapter adoption; a source-available license is an alternative if commercial reuse should be restricted.
-2. Whether to accept third-party adapters in the main repository or require separate packages.
-3. Whether the first public release is a functional alpha or a documented architecture preview.
-4. Whether to publish a Chrome Web Store listing or keep installation developer-only initially.
-5. Whether anonymous local diagnostics should ever be added; the default recommendation is no telemetry.
+- [ ] Owner verifies the anonymous public view and repository settings after publication.
+
+## Post-alpha roadmap (not a publication blocker)
+
+- Export/import portable watch definitions.
+- Visual element picker and live rule highlighting.
+- Multi-resource and shared-origin Queue Lab scenarios.
+- Storage schema migrations and richer outcome analytics.
+- Cross-platform packaging scripts.
+- Accessibility audit with external assistive-technology testing.
+- Optional adapter SDK after its capability boundary has a separate security review.
+- Chrome Web Store packaging only after developer-mode field feedback.
diff --git a/README.md b/README.md
index 1ec2243..cbd1a5c 100644
--- a/README.md
+++ b/README.md
@@ -4,34 +4,41 @@
# QueueScope
-**A browser-native control plane for time-sensitive web events.**
+**A local-first browser control plane for time-sensitive web events.**
-QueueScope coordinates scheduled page watches, preserved browser tabs, queue-sensitive safety locks, state history, and human-facing alerts in one local workspace. The public edition is being rebuilt as a retailer-neutral platform with an adapter SDK and synthetic Queue Lab rather than shipping integrations tied to specific websites.
+QueueScope turns visible page signals into durable, human-readable runs. Schedule a page watch or attach an already-open tab, keep parallel pages independent, stop guarded refresh when a queue or challenge appears, and bring the right tab forward when attention matters.
-> Private preview. This repository is intentionally private while the generic architecture, permissions model, documentation, and release package are prepared for public review.
+This is the retailer-neutral public edition. It contains no site-specific adapters, selectors, URLs, captured production pages, provider endpoints, checkout automation, or private operational history.

-## What QueueScope is designed to do
-
-- Schedule one-time, daily, weekday, weekend, and custom-day watches.
-- Coordinate multiple independent tabs and page-level state machines.
-- Shift between baseline, high-attention, and passive observation cadences.
-- Stop automated refresh as soon as queue, challenge, admission, or other safety evidence appears.
-- Preserve queue-sensitive tabs and keep the operator in control of any consequential action.
-- Record local state transitions, provider timing, position evidence, and outcomes.
-- Present the same operating picture in a full command center and compact browser side panel.
-- Let users define page behavior through portable, retailer-neutral adapters and visual rules.
-
-QueueScope is an observation and coordination tool. It is not an auto-checkout system, CAPTCHA solver, identity spoofer, queue bypass, proxy rotator, or purchasing bot.
+## What works in v0.1.0
+
+- One-time, daily, weekday, weekend, and custom-weekday schedules.
+- Passive watches with user-controlled cadence and no attempt ceiling.
+- Baseline and fast cadences around an optional expected-event time.
+- Guarded refresh or observe-only operation per watch.
+- Multiple independent runs and preserved tabs, including pages on the same origin.
+- Attach to an open HTTP(S) tab without refreshing it.
+- Plain-text queue, admitted, challenge, and unavailable rules.
+- CSS availability selectors plus position and ETA extraction patterns.
+- A one-way navigation safety lock after configured queue or challenge evidence.
+- Admission detection only after prior queue evidence.
+- Durable provider ETA stored as an absolute timestamp.
+- Local notifications, generated sounds, configurable low-ETA warning, and optional focus-on-admission.
+- Collapsed-by-default run cards in the command center and compact side panel.
+- Local run history, copy/edit/delete/arm watch controls, old-watch shelf, reduced motion, and compact density.
+- A built-in synthetic Queue Lab and real unpacked-extension E2E test.
+
+QueueScope observes, coordinates, and alerts. It does not click purchase controls, add products to carts, check out, solve challenges, create extra queue identities, rotate proxies, spoof fingerprints, or call undocumented provider APIs.
## Live workspace
-The screenshots below come from a real unpacked-extension E2E run, not static design mockups. The public repository uses only neutral Queue Lab states; site-specific fixtures and adapters remain private.
+These images are captured from the packaged Manifest V3 extension during its browser test—not static mockups.
-### Queue-aware live run
+### Queue-locked run
-
+
### Compact side panel
@@ -39,40 +46,96 @@ The screenshots below come from a real unpacked-extension E2E run, not static de
### Queue Lab
-Queue Lab is the deterministic local simulator used to prove product, queue, challenge, admission, and purchase-window transitions without generating external page traffic.
+Queue Lab proves product, queue, challenge, availability, position/ETA, and post-queue admission transitions without contacting an external website.

-## Public architecture direction
+## Install locally
+
+### From the release archive
+
+1. Download `QueueScope-0.1.0-unpacked.zip` from the release assets.
+2. Extract it to a permanent local folder.
+3. Open your Chromium browser's extensions page (for Chrome, `chrome://extensions`).
+4. Enable **Developer mode**.
+5. Select **Load unpacked** and choose the extracted folder.
+6. Pin QueueScope. Its toolbar action opens the side panel; the extension details page links to the full command center.
+7. Open **Queue Lab** first to validate the installation safely.
+
+For Comet or another Chromium browser, use that browser's equivalent extensions page and the same unpacked folder.
+
+### Build from source
+
+Requirements: Node.js 20 or newer and PowerShell on Windows for the packaging command.
+
+```powershell
+npm ci
+npm run check
+npm run build
+```
+
+Load the generated `dist` folder as the unpacked extension. To run the real browser workflow and create screenshots:
+
+```powershell
+npm run test:e2e
+npm run package
+```
+
+## First watch
+
+1. Select **New watch**.
+2. Enter an HTTP(S) page URL and choose **Scheduled drop** or **Passive scout**.
+3. Choose **Guarded refresh** only when periodic reloads are appropriate; use **Observe only** for a live queue or sensitive tab.
+4. Set start, expected, and stop times. Start/stop authorize checking; expected time only controls the fast-cadence window.
+5. Describe the visible page phrases/selectors that mean queue, challenge, availability, or admitted.
+6. Save, then arm. QueueScope requests optional access to that origin at this point.
-The public edition will be organized around a small generic core and explicit capability boundaries:
+Page markup changes. Treat a rule as an explicit hypothesis, rehearse it when possible, and keep consequential actions manual.
+
+## Safety model
```text
-Browser UI
- -> watch scheduler
- -> run state machine
- -> refresh safety coordinator
- -> observation history
- -> adapter SDK
- -> generic DOM rules
- -> synthetic Queue Lab adapters
- -> user-authored local adapters
+scheduled or passive watch
+ -> preserved tab
+ -> bounded visible-page observation
+ -> normalized local evidence
+ -> per-run state machine
+ -> queue/challenge: permanent navigation lock
+ -> availability: attention, no clicking
+ -> admitted: valid only after a queue lock
+ -> local notification + human handoff
```
-The default installation will not contain retailer-specific selectors, names, URLs, hidden endpoints, or captured production responses. Access to arbitrary pages will use optional host permissions granted by the user for an individual origin.
+- Arbitrary-site access is optional and requested per origin.
+- There is no cookie or browsing-history permission.
+- No analytics, account, cloud backend, or remote code is used.
+- Evidence text, selectors, patterns, URLs, and runtime messages are validated and bounded.
+- Queue-sensitive tabs are marked non-discardable until the run ends.
+- Extension restart rehydrates alarms, observers, and tab protection without navigating a locked run.
-## Repository status
+See [Privacy and safety](docs/PRIVACY_AND_SAFETY.md), [Architecture](docs/ARCHITECTURE.md), and [Rule builder guide](docs/RULE_BUILDER.md).
-This repository currently contains the public-facing plan, architecture boundary, safety contract, and reviewed visual evidence. Implementation will proceed phase by phase after the private review gate.
+## Validation
-- [Implementation plan](PLAN.md)
-- [Architecture](docs/ARCHITECTURE.md)
-- [Privacy and safety contract](docs/PRIVACY_AND_SAFETY.md)
-- [Contributing](CONTRIBUTING.md)
-- [Security policy](SECURITY.md)
+The v0.1.0 release gate covers:
-The operational site-specific build is maintained separately in a private repository and will not be merged into this history.
+- ESLint and TypeScript.
+- 26 deterministic unit tests.
+- Production MV3 build and artifact inspection.
+- Disposable-profile Chromium E2E.
+- Queue lock, position `428`, durable ETA, post-queue admission, and non-discardable-tab assertions.
+- Command-center and 420×840 side-panel screenshots.
+- Dependency audit, CSP/permission audit, secret scan, source/history neutrality scan, and manual visual review.
-## Public-release gate
+The detailed evidence is in [Test report](docs/TEST_REPORT.md), [Security review](docs/SECURITY_REVIEW.md), and [Release checklist](docs/RELEASE_CHECKLIST.md).
+
+## Project status
+
+v0.1.0 is a functional developer-mode alpha. The private repository is ready for its owner to review and manually change to public. The current scope intentionally favors transparent, user-authored visible-page rules over bundled site integrations.
+
+- [Plan and completed scope](PLAN.md)
+- [Changelog](CHANGELOG.md)
+- [Contributing](CONTRIBUTING.md)
+- [Security policy](SECURITY.md)
-Do not change this repository to public until every item in the final publication checklist in [PLAN.md](PLAN.md) has been verified, including a full-history secret scan, retailer-neutrality audit, permission review, clean installation test, and visual inspection.
+Licensed under the [MIT License](LICENSE).
diff --git a/SECURITY.md b/SECURITY.md
index 9aa3ba6..3dfc291 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -2,19 +2,23 @@
## Supported versions
-QueueScope has not yet published a public release. Security support begins with the first tagged release.
+Security fixes are provided for the latest `0.1.x` alpha until a newer release line replaces it.
## Reporting a vulnerability
-Once the repository is public, report vulnerabilities through GitHub's private vulnerability reporting feature rather than a public issue. Do not include credentials, tokens, queue identifiers, or captured private page data in an issue.
+Use GitHub private vulnerability reporting. Do not open a public issue containing credentials, tokens, queue identifiers, private page captures, or session material.
-## Scope
+Include the affected version, browser/version, reproduction steps using synthetic data where possible, security impact, and any suggested mitigation. Please allow a reasonable remediation window before public disclosure.
-High-priority reports include:
+## High-priority scope
- Browser-permission escalation.
- Credential, cookie, session, or browsing-data exposure.
-- Remote-code execution.
-- Unsafe refresh behavior after a queue or challenge signal.
+- Remote-code execution or unexpected network transmission.
+- Unsafe guarded refresh after configured queue or challenge evidence.
+- Admission without prior queue evidence.
- Cross-origin observation outside a user-granted permission.
-- Leakage of locally stored watch or run history.
+- Leakage or corruption of locally stored watch/run history.
+- An extension action that becomes consequential on the observed page.
+
+General feature requests and rule-authoring questions belong in normal issues after the repository is public.
diff --git a/assets/screenshots/command-center.png b/assets/screenshots/command-center.png
index ff7c1ea..ee18008 100644
Binary files a/assets/screenshots/command-center.png and b/assets/screenshots/command-center.png differ
diff --git a/assets/screenshots/live-run.png b/assets/screenshots/live-run.png
index 12ba18e..2e979b0 100644
Binary files a/assets/screenshots/live-run.png and b/assets/screenshots/live-run.png differ
diff --git a/assets/screenshots/queue-lab-waiting.png b/assets/screenshots/queue-lab-waiting.png
new file mode 100644
index 0000000..e695ad2
Binary files /dev/null and b/assets/screenshots/queue-lab-waiting.png differ
diff --git a/assets/screenshots/queue-lab.png b/assets/screenshots/queue-lab.png
index 2fcc24f..132b768 100644
Binary files a/assets/screenshots/queue-lab.png and b/assets/screenshots/queue-lab.png differ
diff --git a/assets/screenshots/side-panel-watches.png b/assets/screenshots/side-panel-watches.png
new file mode 100644
index 0000000..6bf2e38
Binary files /dev/null and b/assets/screenshots/side-panel-watches.png differ
diff --git a/assets/screenshots/side-panel.png b/assets/screenshots/side-panel.png
index e9dd164..5d52f44 100644
Binary files a/assets/screenshots/side-panel.png and b/assets/screenshots/side-panel.png differ
diff --git a/assets/screenshots/watch-builder.png b/assets/screenshots/watch-builder.png
new file mode 100644
index 0000000..376658d
Binary files /dev/null and b/assets/screenshots/watch-builder.png differ
diff --git a/dashboard.html b/dashboard.html
new file mode 100644
index 0000000..7bcbf54
--- /dev/null
+++ b/dashboard.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+ QueueScope
+
+
+
+
+
+
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index 2c8bcdd..0eddf74 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -1,56 +1,75 @@
# QueueScope architecture
-## System boundary
-
-QueueScope converts page observations into a durable local run state. The scheduler decides when a run may check; the safety coordinator decides whether a refresh is allowed; adapters only describe what the current page visibly exposes.
+## Runtime map
```mermaid
flowchart LR
UI["Command center and side panel"] --> SW["Manifest V3 service worker"]
SW --> SCHEDULE["Schedule engine"]
SW --> STATE["Run state machine"]
- SW --> STORE["Local storage"]
+ SW --> STORE["chrome.storage.local"]
SW --> TABS["Preserved tab coordinator"]
- TABS --> OBSERVER["Read-only content observer"]
- OBSERVER --> ADAPTER["Capability-bounded adapter"]
- ADAPTER --> STATE
- STATE --> SAFETY["Refresh safety coordinator"]
+ TABS --> OBSERVER["Dynamically injected observer"]
+ OBSERVER --> RULES["Bounded visible-DOM rules"]
+ RULES --> STATE
+ STATE --> SAFETY["Navigation safety decision"]
SAFETY --> TABS
- STATE --> ATTENTION["Local notifications and sounds"]
+ STATE --> ATTENTION["Local notifications and generated tones"]
```
+The extension has no backend. The service worker serializes snapshot mutations, rehydrates alarms and tab protection after restart, and injects the observer only into a tab owned by an active run.
+
+## Major modules
+
+- `src/core/schedule.ts`: timezone-aware one-time and recurring windows.
+- `src/core/rules.ts`: bounded text, selector, position, and ETA classification.
+- `src/core/state-machine.ts`: cadence, state transitions, queue-lock monotonicity, and refresh decisions.
+- `src/core/validation.ts`: URL canonicalization, rule validation, settings bounds, and runtime-message allowlist.
+- `src/background/service-worker.ts`: alarms, optional permissions, tabs, notifications, storage, and restart recovery.
+- `src/content/observer-standalone.js`: self-contained visible-page observer injected into authorized run tabs.
+- `src/ui`: React command center, watch builder, and side-panel surface.
+- `harness`: synthetic Queue Lab used by people and E2E.
+
## Core invariants
-1. Queue, challenge, rate-limit, admission, and purchase-window evidence disables guarded refresh before another reload is authorized.
-2. A resource-specific queue locks only its run unless the evidence is origin-wide.
-3. Site-wide challenges and rate limits lock every active run on that origin.
-4. Provider timing is stored as an absolute timestamp whenever possible.
-5. Weak observations cannot replace stronger evidence without an explicit transition rule.
-6. Unknown position, timing, and availability values remain unknown.
+1. Configured queue or challenge evidence engages a one-way lock before another guarded refresh can be scheduled.
+2. Admission is accepted only after the run already holds a queue/challenge lock.
+3. Attached tabs are always observe-only.
+4. Position is provider-exposed evidence, not a claim about people ahead.
+5. Provider ETA is persisted as an absolute timestamp.
+6. Unknown position, ETA, and page state stay explicitly unknown.
7. Consequential actions remain human-controlled.
-## Adapter boundary
+## Observation boundary
+
+The page observer reads at most 100,000 characters of visible text, checks one validated CSS selector, and evaluates bounded user patterns. It emits a normalized observation with a bounded title, URL, evidence list, classification, confidence, optional position, and optional ETA.
+
+It does not receive or read cookies, authorization headers, credentials, browser history, network responses, local storage belonging to the page, or private APIs. It cannot click or submit page controls.
+
+## Scheduling and navigation
+
+A scheduled run has explicit `startAt`, optional `expectedAt`, and `stopAt` values. Start/stop authorize activity. Expected time only selects the fast cadence inside the configured before/after window. A passive run starts immediately and ends only when the user stops it.
-Public adapters are declarative observation components. They receive a read-only, bounded representation of the page and return normalized evidence. They do not receive cookies, browser credentials, arbitrary network access, or purchase capabilities.
+The service worker creates a dedicated background tab for an armed saved watch. A user can instead attach an existing tab, which forces observe-only mode. Before a guarded reload, the worker consults the current run state; a queue lock, attention state, stop boundary, or future next-check time defeats navigation.
-The initial public adapter SDK should support visible DOM evidence and user-authored selectors. Any future capability must be documented, permission-scoped, and covered by conformance tests.
+## Storage and recovery
-## Storage
+One versioned snapshot in `chrome.storage.local` contains saved watches, active runs, bounded history, and settings. Each mutation is serialized. On worker startup QueueScope:
-The first public release stores data only in extension-local storage:
+1. reloads the snapshot;
+2. reapplies `autoDiscardable: false` to locked tabs;
+3. reinjects observers into active HTTP(S) run tabs;
+4. reconstructs run and recurrence alarms;
+5. starts only schedules whose active window is open.
-- User settings.
-- Saved watch definitions.
-- Active run snapshots.
-- Bounded event history.
-- Optional local outcome history.
+Recovery does not navigate a locked tab.
-Export is explicit. No cloud synchronization or analytics service is planned for the initial release.
+## Permissions
-## Runtime recovery
+Core permissions are alarms, notifications, offscreen audio, side panel, local storage, scripting, and tab coordination. Arbitrary HTTP(S) host access is optional. The UI requests only the selected page origin when the user arms or attaches it. Localhost is included for Queue Lab development. There is no cookie or browsing-history permission.
-Manifest V3 service workers are ephemeral. QueueScope must rebuild alarms, observers, and tab protection from the stored snapshot whenever the worker restarts. Recovery must not navigate or refresh an already queue-sensitive tab.
+## Build and test boundary
-## Test architecture
+Vite builds extension pages and the module service worker. The content observer is intentionally self-contained and copied as an audited IIFE. The build verifier checks required assets, MV3, optional host access, absence of globally declared content scripts, remote executable/data requests, and dynamic code execution.
-The demo lab is the reference provider. It exposes deterministic visible state markers and verifies that the production extension can move through the full state machine without external traffic. Release validation uses a disposable browser profile and manually reviewed screenshots.
+The real E2E launches the built `dist` folder in a disposable Chromium profile and drives Queue Lab through queue lock and admission.
diff --git a/docs/PRIVACY_AND_SAFETY.md b/docs/PRIVACY_AND_SAFETY.md
index 8f8c562..48b2046 100644
--- a/docs/PRIVACY_AND_SAFETY.md
+++ b/docs/PRIVACY_AND_SAFETY.md
@@ -1,46 +1,45 @@
# Privacy and safety contract
-QueueScope is designed as a local observation and attention tool.
+QueueScope is a local observation and attention tool.
## Data handling
-- Configuration, observations, and history remain on the user's device by default.
-- URLs are canonicalized and common tracking parameters are removed before storage.
-- Evidence strings are bounded and sanitized.
-- Queue identifiers, credentials, authorization values, cookies, and session tokens are not collected.
-- No analytics or cloud telemetry is included in the initial public release.
+- Watches, observations, active runs, settings, and history stay in `chrome.storage.local`.
+- Common campaign parameters and URL fragments are removed before a saved URL is stored.
+- Page text is processed in the page and is not retained wholesale; only bounded evidence is sent to the extension.
+- Credentials, authorization values, cookies, session tokens, provider responses, and browser history are not collected.
+- There is no account, analytics, cloud synchronization, telemetry, or remote backend.
## Browser permissions
-- Request access only when the user creates or arms a watch for an origin.
-- Explain the permission immediately before requesting it.
-- Do not request cookie or browsing-history access.
-- Do not load or execute remote code.
-- Keep the extension Content Security Policy strict.
+- HTTP(S) host access is optional and requested for one origin when the user arms or attaches a page.
+- QueueScope does not declare a global content script; it injects the observer only into active, authorized run tabs.
+- Cookie and browsing-history permissions are absent.
+- Extension pages use a strict self-only Content Security Policy.
+- Audio is generated locally with the Web Audio API.
## Automation boundary
-QueueScope may observe visible state, manage its own alarms, focus a preserved tab, issue local notifications, and perform a user-configured guarded refresh while no safety lock exists.
+QueueScope may read visible page text, evaluate user-authored visible-DOM rules, manage its own alarms, perform a configured guarded refresh while the run is safe, focus a preserved tab, prevent discard of a queue-sensitive tab, and issue local alerts.
-QueueScope must not:
+QueueScope does not:
-- Add products to a cart or complete checkout.
-- Solve or automate a challenge.
-- Create multiple queue identities or attempt to bypass a queue.
-- Rotate proxies or spoof browser identity.
-- Extract credentials or session data.
-- Call undocumented private provider endpoints.
+- click purchase or cart controls;
+- complete checkout;
+- click through a waiting room or hold a spot;
+- solve or automate a challenge;
+- create multiple identities or bypass a queue;
+- rotate proxies or spoof browser identity;
+- read credentials, cookies, or session data;
+- call undocumented provider endpoints;
+- claim that a position identifier equals people ahead.
## Safety precedence
-Safety evidence always takes precedence over cadence. When a queue, challenge, rate limit, admission state, or purchase timer is detected, QueueScope stops guarded refresh for the affected scope before scheduling another reload.
+Configured queue or challenge evidence engages a monotonic navigation lock. Once locked, that run cannot refresh again until the user ends it. Availability enters an attention state without clicking. Admission is valid only after prior queue evidence.
-## Honest uncertainty
+The page-specific rules are user-authored. A false negative is possible if a site changes its visible text or markup, so observe-only mode is the conservative option for already-live or sensitive tabs.
-The UI must distinguish:
+## Honest uncertainty
-- Provider position from locally inferred progress.
-- Exact provider ETA from an upper bound or local estimate.
-- A ticket identifier from a count of people ahead.
-- Confirmed availability from an ambiguous page signal.
-- Unknown data from zero.
+The UI distinguishes provider-exposed position from unknown position, an absolute provider ETA from no ETA, a visible availability signal from an ambiguous product page, and unknown values from zero.
diff --git a/docs/RELEASE_CHECKLIST.md b/docs/RELEASE_CHECKLIST.md
new file mode 100644
index 0000000..9ded581
--- /dev/null
+++ b/docs/RELEASE_CHECKLIST.md
@@ -0,0 +1,40 @@
+# v0.1.0 release checklist
+
+## Source and trust boundary
+
+- [x] Independent repository and history.
+- [x] No operational repository remote/submodule.
+- [x] Retailer-neutral source, tests, fixtures, screenshots, and documentation.
+- [x] No captured provider response, private endpoint, product identifier, credential, cookie, or session material.
+- [x] No username or local absolute path in tracked product files.
+
+## Product and safety
+
+- [x] No cart, checkout, challenge-solving, queue-bypass, proxy, fingerprint, or identity automation.
+- [x] Optional page-origin access requested on arm/attach.
+- [x] No cookie or browsing-history permission.
+- [x] Queue/challenge evidence engages a monotonic navigation lock.
+- [x] Admission requires prior queue evidence.
+- [x] Unknown position and ETA remain unknown.
+- [x] Queue-locked tabs are protected from discard.
+
+## Quality
+
+- [x] `npm run check` passes.
+- [x] 26/26 unit tests pass.
+- [x] Production artifact verifier passes.
+- [x] Disposable-profile unpacked-extension E2E passes.
+- [x] Side panel reviewed at 420×840.
+- [x] Screenshots regenerated from v0.1.0 and manually reviewed.
+- [x] Dependency audit reports zero vulnerabilities.
+- [x] Security, CSP, permissions, secret, and neutrality reviews complete.
+
+## Repository handoff
+
+- [x] MIT license present.
+- [x] CI workflow present.
+- [x] Release archive and SHA-256 generated.
+- [x] README install, usage, safety, and validation instructions current.
+- [x] Repository remains private.
+- [ ] Owner changes visibility to public.
+- [ ] Owner checks anonymous README/images and enables desired repository settings.
diff --git a/docs/RULE_BUILDER.md b/docs/RULE_BUILDER.md
new file mode 100644
index 0000000..bea0780
--- /dev/null
+++ b/docs/RULE_BUILDER.md
@@ -0,0 +1,62 @@
+# Rule builder guide
+
+QueueScope intentionally ships without site-specific rules. A watch describes visible signals on the page you choose.
+
+## Rule priority
+
+The classifier evaluates signals in this order:
+
+1. challenge phrase;
+2. queue phrase;
+3. admitted phrase, only if the run already holds a queue lock;
+4. enabled availability selector;
+5. unavailable phrase;
+6. generic loaded product page;
+7. unknown.
+
+Challenge and queue states engage the navigation safety lock. Availability asks for attention but never clicks. Admission without earlier queue evidence is ignored as a product-page observation, which prevents phrases such as “wait until admitted” from becoming false positives.
+
+## Text phrases
+
+Enter one phrase per line. QueueScope lowercases and trims each value, removes duplicates, keeps at most 20 values per category, and limits each phrase to 120 characters. Prefer a distinctive visible sentence over a broad word such as `ready` or `line`.
+
+## Availability selector
+
+The selector should identify the enabled action or visible state that means availability. A match is ignored when the element has `disabled` or `aria-disabled="true"`. Selectors are limited to 200 characters and validated before save.
+
+Example for a synthetic page:
+
+```css
+[data-queuescope-available='true']
+```
+
+QueueScope does not click the element.
+
+## Position pattern
+
+Use a case-insensitive regular expression whose first capture group is the numeric position. For example:
+
+```regex
+position\s+(\d+)
+```
+
+Commas are removed before numeric parsing. Values outside `0..1,000,000,000` remain unknown.
+
+## ETA pattern
+
+The first capture group may contain `HH:MM:SS`, a minute label, or a second label:
+
+```regex
+estimated wait\s+(\d{2}:\d{2}:\d{2})
+```
+
+QueueScope converts the duration to an absolute `providerEtaAt` timestamp when observed. The UI counts down from that timestamp rather than restarting the duration on every render.
+
+## Conservative setup
+
+- Use observe-only mode while developing a rule.
+- Attach to a harmless synthetic/local page first.
+- Confirm the phrase is visible, distinctive, and stable.
+- Prefer several precise variants over one broad substring.
+- Never encode credentials, cookies, tokens, ticket identifiers, or private response data in a watch.
+- If a live queue has already appeared, attach the tab rather than creating a guarded-refresh watch.
diff --git a/docs/SECURITY_REVIEW.md b/docs/SECURITY_REVIEW.md
new file mode 100644
index 0000000..8e7cd81
--- /dev/null
+++ b/docs/SECURITY_REVIEW.md
@@ -0,0 +1,37 @@
+# v0.1.0 security review
+
+Date: 2026-08-10
+
+## Reviewed surface
+
+- Manifest V3 permissions and Content Security Policy.
+- Runtime message allowlist and sender checks.
+- URL, schedule, settings, selector, pattern, and observation bounds.
+- Content observer capabilities and data flow.
+- Guarded-refresh decision and queue-lock monotonicity.
+- Extension-local storage and restart recovery.
+- Notification/offscreen-audio path.
+- Built assets for remote requests and dynamic code execution.
+- Dependencies, secrets, retailer-specific content, absolute local paths, and Git history.
+
+## Findings
+
+No release-blocking finding remains.
+
+- Arbitrary HTTP(S) access is optional; localhost alone is granted for development.
+- The extension has no cookie or history permission and no backend/telemetry path.
+- Runtime messages are structurally allowlisted. Normal observations require a content-script tab sender; synthetic Lab observations require the extension-owned harness URL.
+- URLs are limited to HTTP(S), credentials/fragments are removed, and common tracking parameters are stripped.
+- Rules and evidence are bounded. Suspicious nested-quantifier patterns and invalid/complex selectors are rejected.
+- Queue/challenge evidence permanently inhibits navigation for the run. Admission requires the existing lock.
+- Built assets contain no remote executable/data request and no `eval`/`Function` construction.
+- `npm audit` reports zero known vulnerabilities.
+
+## Residual risks
+
+- Visible page markup can change, creating false negatives or ambiguous signals. Users should prefer observe-only while validating rules.
+- Regular-expression safety uses conservative structural checks rather than a formal complexity proof; input length and observed text are bounded to constrain impact.
+- `tabs` is a powerful permission required for preserved-tab coordination and attachment. It is documented, and page observation still requires optional host access.
+- Developer-mode installation lacks store review and automatic update guarantees.
+
+The release remains an alpha and does not claim compatibility with any specific external site.
diff --git a/docs/TEST_REPORT.md b/docs/TEST_REPORT.md
new file mode 100644
index 0000000..00b059d
--- /dev/null
+++ b/docs/TEST_REPORT.md
@@ -0,0 +1,40 @@
+# v0.1.0 test report
+
+Date: 2026-08-10
+Environment: Windows, Node.js 24.11.1, disposable Chromium profile, 1440×1000 dashboard, 420×840 side panel.
+
+## Automated gates
+
+| Gate | Result |
+|---|---|
+| ESLint | Pass, zero warnings |
+| TypeScript `--noEmit` | Pass |
+| Unit tests | 26/26 pass across rules, schedules, state machine, and validation |
+| Production MV3 build | Pass |
+| Artifact verification | Pass, 20 required/self-contained release files |
+| Chromium unpacked-extension E2E | Pass |
+| Dependency audit | 0 known vulnerabilities |
+
+## Browser assertions
+
+- Loaded the production `dist` folder in a new temporary Chromium profile.
+- Opened the dashboard with tab title `QueueScope`.
+- Confirmed the Queue Lab orbit marker remained within 3 CSS pixels of the orbit radius.
+- Opened the synthetic Queue Lab and transitioned from product to queue.
+- Persisted classification `queue`, position `428`, and provider ETA `00:08:30`.
+- Confirmed `queueLocked: true` and browser tab `autoDiscardable: false`.
+- Confirmed live cards begin collapsed in command center and side panel.
+- Expanded the live run and rendered timing, evidence, position, ETA, and actions.
+- Transitioned to admitted and confirmed the run reached `ADMITTED` only after queue lock.
+- Confirmed side-panel footer was pinned to the viewport bottom.
+- Seeded a future watch in the side panel, toggled auto-start, copied it as a paused watch, and deleted the copy.
+- Opened the watch builder and confirmed passive mode displays no attempt ceiling.
+- Captured browser-generated release screenshots without console/page errors.
+
+## Manual visual review
+
+Reviewed command center, expanded live run, Queue Lab overview, Queue Lab waiting state, watch builder, and 420×840 side panel at original resolution. Verified hierarchy, no clipped status indicators, collapsed-card density, pinned panel footer, and responsive orbit alignment. The screenshots tracked in `assets/screenshots` are the reviewed release captures.
+
+## Boundaries
+
+The E2E uses only the extension's synthetic Queue Lab. No assertion in this report claims compatibility with an external website or future page markup.
diff --git a/eslint.config.js b/eslint.config.js
new file mode 100644
index 0000000..882f07f
--- /dev/null
+++ b/eslint.config.js
@@ -0,0 +1,29 @@
+import js from "@eslint/js";
+import globals from "globals";
+import reactHooks from "eslint-plugin-react-hooks";
+import reactRefresh from "eslint-plugin-react-refresh";
+import tseslint from "typescript-eslint";
+
+export default tseslint.config(
+ { ignores: ["dist", "coverage", "test-results", "release"] },
+ {
+ extends: [js.configs.recommended, ...tseslint.configs.recommended],
+ files: ["**/*.{ts,tsx}"],
+ languageOptions: {
+ ecmaVersion: 2022,
+ globals: {
+ ...globals.browser,
+ ...globals.webextensions,
+ ...globals.node
+ }
+ },
+ plugins: {
+ "react-hooks": reactHooks,
+ "react-refresh": reactRefresh
+ },
+ rules: {
+ ...reactHooks.configs.recommended.rules,
+ "react-refresh/only-export-components": ["warn", { "allowConstantExport": true }]
+ }
+ }
+);
diff --git a/harness/index.html b/harness/index.html
new file mode 100644
index 0000000..fa22c06
--- /dev/null
+++ b/harness/index.html
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+ QueueScope · Queue Lab
+
+
+
+
+
+
+ CONTROLLED REHEARSAL Product page observed The page is loaded and no configured attention signal is present.
CLASSIFICATION product
POSITION —
PROVIDER ETA —
+
+
+ 01 Product Baseline page
+ 02 Queue Lock navigation
+ 03 Challenge Manual attention
+ 04 Available Visible selector
+ 05 Admitted After queue only
+
+ What this proves Queue Lab uses the same local rule engine as attached pages. Move through the stages, then inspect the live run in QueueScope. No external site or network service is involved.
+ A neutral product page is visible.
+ Configured action
+
+
+
+
diff --git a/harness/lab.css b/harness/lab.css
new file mode 100644
index 0000000..7109539
--- /dev/null
+++ b/harness/lab.css
@@ -0,0 +1,9 @@
+:root { color-scheme: dark; font-family: Inter, "Segoe UI Variable", "Segoe UI", system-ui, sans-serif; background: #050b09; color: #f1f8f5; --mint: #49f5bd; --line: #1c322b; --muted: #8ca29a; }
+* { box-sizing: border-box; } body { margin: 0; min-height: 100vh; background: radial-gradient(circle at 45% 20%, rgba(73,245,189,.09), transparent 30rem), #050b09; } button { font: inherit; color: inherit; }
+main { width: min(1180px, calc(100% - 34px)); margin: auto; padding: 24px 0 50px; } header { display: flex; justify-content: space-between; align-items: center; padding-bottom: 22px; border-bottom: 1px solid var(--line); }.lab-brand { display: flex; align-items: center; gap: 11px; }.lab-brand strong { display: block; font-size: 15px; }.lab-brand span { display: block; color: var(--mint); font: 600 8px/1 monospace; letter-spacing: .15em; margin-top: 4px; }.scope-mark { width: 37px; height: 37px; border: 1px solid #2b5548; border-radius: 11px; position: relative; }.scope-mark::before { content: ""; position: absolute; inset: 8px; border: 2px solid var(--mint); border-right-color: transparent; border-radius: 50%; }.scope-mark i { position: absolute; width: 5px; height: 5px; right: 6px; top: 7px; border-radius: 50%; background: var(--mint); box-shadow: 0 0 9px var(--mint); }.local-pill { border: 1px solid #2c5b4b; color: var(--mint); padding: 7px 9px; font: 600 8px monospace; letter-spacing: .1em; }
+.lab-console { min-height: 480px; display: grid; grid-template-columns: 1fr 1.1fr; align-items: center; gap: 70px; padding: 42px clamp(20px,5vw,70px); border: 1px solid var(--line); border-top: 0; background: linear-gradient(135deg, rgba(73,245,189,.035), transparent 60%); }.radar { width: min(360px, 75vw); aspect-ratio: 1; border: 1px solid #2a594a; border-radius: 50%; position: relative; display: grid; place-items: center; justify-self: center; overflow: hidden; background: repeating-radial-gradient(circle, transparent 0 58px, rgba(73,245,189,.06) 59px 60px); }.radar::before, .radar::after { content: ""; position: absolute; background: linear-gradient(90deg, transparent, #2e6251, transparent); height: 1px; left: 0; right: 0; top: 50%; }.radar::after { transform: rotate(90deg); }.sweep { position: absolute; inset: 0; border-radius: 50%; background: conic-gradient(from 0deg, rgba(73,245,189,.25), transparent 20%, transparent); animation: sweep 4s linear infinite; }.ring { position: absolute; border-radius: 50%; border: 1px solid rgba(73,245,189,.16); }.ring-one { inset: 18%; }.ring-two { inset: 36%; }.center { width: 92px; height: 92px; border-radius: 50%; position: relative; z-index: 2; background: #0b1814; border: 1px solid var(--mint); display: grid; place-items: center; align-content: center; gap: 4px; box-shadow: 0 0 38px rgba(73,245,189,.12); }.center strong { font: 500 28px/1 monospace; }.center span { color: var(--mint); font: 600 8px monospace; letter-spacing: .15em; }.lab-node { width: 10px; height: 10px; position: absolute; top: -5px; left: calc(50% - 5px); background: var(--mint); border-radius: 50%; box-shadow: 0 0 14px var(--mint); transform-origin: 5px 185px; animation: orbit 7s linear infinite; z-index: 3; } @keyframes orbit { to { transform: rotate(360deg); } } @keyframes sweep { to { transform: rotate(360deg); } }
+.kicker { color: var(--mint); font: 600 9px monospace; letter-spacing: .13em; }.stage-copy h1 { font-size: clamp(35px,5vw,56px); line-height: .95; letter-spacing: -.055em; margin: 13px 0 17px; }.stage-copy > p { color: var(--muted); font-size: 13px; line-height: 1.65; max-width: 540px; }.telemetry { display: grid; grid-template-columns: repeat(3, 1fr); border: 1px solid var(--line); margin-top: 25px; }.telemetry > div { padding: 14px; display: grid; gap: 7px; border-right: 1px solid var(--line); }.telemetry > div:last-child { border: 0; }.telemetry span { color: #60776e; font: 600 8px monospace; }.telemetry strong { font-size: 11px; text-transform: capitalize; }
+.stage-controls { display: grid; grid-template-columns: repeat(5, 1fr); margin-top: 12px; border: 1px solid var(--line); }.stage-controls button { min-height: 100px; border: 0; border-right: 1px solid var(--line); background: #091310; text-align: left; padding: 16px; display: grid; grid-template-columns: auto 1fr; gap: 4px 9px; cursor: pointer; }.stage-controls button:last-child { border-right: 0; }.stage-controls button:hover { background: #0d1b16; }.stage-controls button.active { background: rgba(73,245,189,.09); box-shadow: inset 0 -2px var(--mint); }.stage-controls button > span { color: var(--mint); font: 600 9px monospace; grid-row: 1 / span 2; }.stage-controls strong { font-size: 11px; }.stage-controls small { color: var(--muted); font-size: 8px; }.explanation { border: 1px solid var(--line); margin-top: 12px; padding: 17px; display: flex; gap: 16px; align-items: start; }.explanation strong { color: var(--mint); font: 600 9px monospace; letter-spacing: .08em; white-space: nowrap; }.explanation p { margin: 0; color: var(--muted); font-size: 10px; line-height: 1.6; } #signal, #available-control { position: fixed; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); opacity: .001; }
+body[data-stage="queue"] .center, body[data-stage="challenge"] .center { border-color: #ffc865; box-shadow: 0 0 42px rgba(255,200,101,.17); } body[data-stage="queue"] .lab-node, body[data-stage="challenge"] .lab-node { background: #ffc865; box-shadow: 0 0 14px #ffc865; }
+@media (max-width: 800px) { .lab-console { grid-template-columns: 1fr; gap: 30px; }.stage-controls { grid-template-columns: 1fr; }.stage-controls button { min-height: 66px; border-right: 0; border-bottom: 1px solid var(--line); }.telemetry { grid-template-columns: 1fr; }.telemetry > div { border-right: 0; border-bottom: 1px solid var(--line); }.lab-node { transform-origin: 5px calc(min(360px,75vw) / 2 + 5px); } }
+@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; } }
diff --git a/harness/lab.ts b/harness/lab.ts
new file mode 100644
index 0000000..52b2410
--- /dev/null
+++ b/harness/lab.ts
@@ -0,0 +1,58 @@
+import { classifyWithRules } from "../src/core/rules";
+import { DEFAULT_RULES } from "../src/core/defaults";
+import type { Observation, RuntimeResponse } from "../src/core/types";
+import "./lab.css";
+
+type Stage = "product" | "queue" | "challenge" | "available" | "admitted";
+const copy: Record = {
+ product: { code: "01", title: "Product page observed", body: "The page is loaded and no configured attention signal is present.", signal: "A neutral product page is visible.", position: "—", eta: "—" },
+ queue: { code: "02", title: "Waiting room detected", body: "Configured queue evidence is visible. Guarded navigation should lock immediately.", signal: "You are now in the waiting room. Position 428. Estimated wait 00:08:30.", position: "428", eta: "00:08:30" },
+ challenge: { code: "03", title: "Manual verification needed", body: "A challenge signal requires attention. QueueScope preserves the tab and does not interact.", signal: "Please verify you are human before continuing.", position: "—", eta: "—" },
+ available: { code: "04", title: "Configured action is available", body: "The availability selector is present and enabled. QueueScope should notify, not click it.", signal: "The configured product view has changed.", position: "—", eta: "—" },
+ admitted: { code: "05", title: "Post-queue admission detected", body: "Admission is valid only because the run previously observed queue evidence.", signal: "Access granted. Your session is ready.", position: "—", eta: "—" }
+};
+
+let stage: Stage = "product";
+let priorQueueLock = false;
+
+function element(id: string): T { return document.getElementById(id) as T; }
+
+async function report() {
+ const available = element("available-control");
+ const observation: Observation = classifyWithRules(DEFAULT_RULES, {
+ url: location.href,
+ title: document.title,
+ text: document.body.innerText,
+ availableSelectorMatched: !available.disabled,
+ priorQueueLock
+ });
+ if (["queue", "challenge"].includes(observation.classification)) priorQueueLock = true;
+ element("classification").textContent = observation.classification;
+ try {
+ const result = await chrome.runtime.sendMessage({ type: "LAB_OBSERVATION", observation }) as RuntimeResponse;
+ if (!result?.ok) throw new Error(result?.error || "Observation was not accepted.");
+ } catch (error) {
+ document.body.dataset.runtime = "unavailable";
+ console.info("Queue Lab preview mode", error);
+ }
+}
+
+function setStage(next: Stage) {
+ stage = next;
+ const value = copy[next];
+ element("stage-code").textContent = value.code;
+ element("stage-title").textContent = value.title;
+ element("stage-body").textContent = value.body;
+ element("signal").textContent = value.signal;
+ element("position").textContent = value.position;
+ element("eta").textContent = value.eta;
+ const available = element("available-control");
+ available.disabled = next !== "available";
+ available.dataset.queuescopeAvailable = String(next === "available");
+ document.querySelectorAll("[data-stage]").forEach((button) => button.classList.toggle("active", button.dataset.stage === next));
+ document.body.dataset.stage = next;
+ void report();
+}
+
+document.querySelectorAll("[data-stage]").forEach((button) => button.addEventListener("click", () => setStage(button.dataset.stage as Stage)));
+setStage(stage);
diff --git a/offscreen.html b/offscreen.html
new file mode 100644
index 0000000..bb1e145
--- /dev/null
+++ b/offscreen.html
@@ -0,0 +1,5 @@
+
+
+ QueueScope audio
+
+
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..8a06bf4
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,4287 @@
+{
+ "name": "queuescope",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "queuescope",
+ "version": "0.1.0",
+ "license": "MIT",
+ "dependencies": {
+ "lucide-react": "0.468.0",
+ "react": "19.0.0",
+ "react-dom": "19.0.0"
+ },
+ "devDependencies": {
+ "@eslint/js": "10.0.1",
+ "@types/chrome": "0.0.287",
+ "@types/node": "26.1.1",
+ "@types/react": "19.0.3",
+ "@types/react-dom": "19.0.2",
+ "@vitejs/plugin-react": "6.0.4",
+ "@vitest/coverage-v8": "4.1.10",
+ "esbuild": "0.28.1",
+ "eslint": "10.7.0",
+ "eslint-plugin-react-hooks": "7.1.1",
+ "eslint-plugin-react-refresh": "0.5.3",
+ "globals": "15.14.0",
+ "jsdom": "25.0.1",
+ "playwright-core": "1.54.1",
+ "typescript": "5.7.2",
+ "typescript-eslint": "8.65.0",
+ "vite": "8.1.5",
+ "vitest": "4.1.10"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@asamuzakjp/css-color": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
+ "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
+ "dev": true,
+ "dependencies": {
+ "@csstools/css-calc": "^2.1.3",
+ "@csstools/css-color-parser": "^3.0.9",
+ "@csstools/css-parser-algorithms": "^3.0.4",
+ "@csstools/css-tokenizer": "^3.0.3",
+ "lru-cache": "^10.4.3"
+ }
+ },
+ "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
+ "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
+ "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
+ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.7",
+ "@babel/helper-compilation-targets": "^7.29.7",
+ "@babel/helper-module-transforms": "^7.29.7",
+ "@babel/helpers": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/template": "^7.29.7",
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.8",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz",
+ "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/parser": "^7.29.8",
+ "@babel/types": "^7.29.8",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
+ "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
+ "dev": true,
+ "dependencies": {
+ "@babel/compat-data": "^7.29.7",
+ "@babel/helper-validator-option": "^7.29.7",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
+ "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
+ "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
+ "dev": true,
+ "dependencies": {
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
+ "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
+ "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+ "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
+ "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
+ "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.8",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz",
+ "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.29.8"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
+ "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.8",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz",
+ "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.8",
+ "@babel/helper-globals": "^7.29.7",
+ "@babel/parser": "^7.29.8",
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.8",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.8",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz",
+ "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@bcoe/v8-coverage": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz",
+ "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==",
+ "dev": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@csstools/color-helpers": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
+ "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@csstools/css-calc": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
+ "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-color-parser": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
+ "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "dependencies": {
+ "@csstools/color-helpers": "^5.1.0",
+ "@csstools/css-calc": "^2.1.4"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-parser-algorithms": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
+ "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-tokenizer": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
+ "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@emnapi/core": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
+ "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.2",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
+ "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
+ "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
+ "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
+ "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
+ "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
+ "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
+ "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
+ "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
+ "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
+ "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
+ "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
+ "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
+ "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
+ "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
+ "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
+ "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
+ "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
+ "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
+ "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
+ "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
+ "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
+ "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
+ "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
+ "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
+ "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.10.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz",
+ "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==",
+ "dev": true,
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+ "dev": true,
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.23.5",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz",
+ "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==",
+ "dev": true,
+ "dependencies": {
+ "@eslint/object-schema": "^3.0.5",
+ "debug": "^4.3.1",
+ "minimatch": "^10.2.4"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz",
+ "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==",
+ "dev": true,
+ "dependencies": {
+ "@eslint/core": "^1.2.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz",
+ "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz",
+ "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==",
+ "dev": true,
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "eslint": "^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "eslint": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz",
+ "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==",
+ "dev": true,
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz",
+ "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==",
+ "dev": true,
+ "dependencies": {
+ "@eslint/core": "^1.2.1",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
+ "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==",
+ "dev": true,
+ "dependencies": {
+ "@humanfs/types": "^0.15.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.8",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz",
+ "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==",
+ "dev": true,
+ "dependencies": {
+ "@humanfs/core": "^0.19.2",
+ "@humanfs/types": "^0.15.0",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/types": {
+ "version": "0.15.0",
+ "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz",
+ "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.2.tgz",
+ "integrity": "sha512-JfB4kuJQjaoHuCTseIINHtHWeJnvgEcxjwA5t/Y00ZgaOO1Crz3fjT/p8kT28zA/Caz7oiUMn3d6H2yOVCVwuw==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "@tybys/wasm-util": "^0.10.3"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=23.5.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ },
+ "peerDependencies": {
+ "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.3",
+ "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.3"
+ }
+ },
+ "node_modules/@oxc-project/types": {
+ "version": "0.139.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz",
+ "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz",
+ "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz",
+ "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz",
+ "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz",
+ "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz",
+ "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz",
+ "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz",
+ "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz",
+ "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz",
+ "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz",
+ "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz",
+ "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz",
+ "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-wasm32-wasi": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz",
+ "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==",
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "1.11.1",
+ "@emnapi/runtime": "1.11.1",
+ "@napi-rs/wasm-runtime": "^1.1.6"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz",
+ "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz",
+ "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
+ "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
+ "dev": true
+ },
+ "node_modules/@standard-schema/spec": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+ "dev": true
+ },
+ "node_modules/@tybys/wasm-util": {
+ "version": "0.10.3",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
+ "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@types/chai": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+ "dev": true,
+ "dependencies": {
+ "@types/deep-eql": "*",
+ "assertion-error": "^2.0.1"
+ }
+ },
+ "node_modules/@types/chrome": {
+ "version": "0.0.287",
+ "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.287.tgz",
+ "integrity": "sha512-wWhBNPNXZHwycHKNYnexUcpSbrihVZu++0rdp6GEk5ZgAglenLx+RwdEouh6FrHS0XQiOxSd62yaujM1OoQlZQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/filesystem": "*",
+ "@types/har-format": "*"
+ }
+ },
+ "node_modules/@types/deep-eql": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "dev": true
+ },
+ "node_modules/@types/esrecurse": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
+ "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==",
+ "dev": true
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+ "dev": true
+ },
+ "node_modules/@types/filesystem": {
+ "version": "0.0.36",
+ "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.36.tgz",
+ "integrity": "sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==",
+ "dev": true,
+ "dependencies": {
+ "@types/filewriter": "*"
+ }
+ },
+ "node_modules/@types/filewriter": {
+ "version": "0.0.33",
+ "resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.33.tgz",
+ "integrity": "sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==",
+ "dev": true
+ },
+ "node_modules/@types/har-format": {
+ "version": "1.2.16",
+ "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.16.tgz",
+ "integrity": "sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==",
+ "dev": true
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true
+ },
+ "node_modules/@types/node": {
+ "version": "26.1.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz",
+ "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==",
+ "dev": true,
+ "dependencies": {
+ "undici-types": "~8.3.0"
+ }
+ },
+ "node_modules/@types/react": {
+ "version": "19.0.3",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.3.tgz",
+ "integrity": "sha512-UavfHguIjnnuq9O67uXfgy/h3SRJbidAYvNjLceB+2RIKVRBzVsh0QO+Pw6BCSQqFS9xwzKfwstXx0m6AbAREA==",
+ "dev": true,
+ "dependencies": {
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.0.2",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.2.tgz",
+ "integrity": "sha512-c1s+7TKFaDRRxr1TxccIX2u7sfCnc3RxkVyBIUA2lCpyqCF+QoAwQ/CBg7bsMdVwP120HEH143VQezKtef5nCg==",
+ "dev": true,
+ "peerDependencies": {
+ "@types/react": "^19.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.65.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz",
+ "integrity": "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.12.2",
+ "@typescript-eslint/scope-manager": "8.65.0",
+ "@typescript-eslint/type-utils": "8.65.0",
+ "@typescript-eslint/utils": "8.65.0",
+ "@typescript-eslint/visitor-keys": "8.65.0",
+ "ignore": "^7.0.5",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.65.0",
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz",
+ "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.65.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.65.0.tgz",
+ "integrity": "sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.65.0",
+ "@typescript-eslint/types": "8.65.0",
+ "@typescript-eslint/typescript-estree": "8.65.0",
+ "@typescript-eslint/visitor-keys": "8.65.0",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.65.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz",
+ "integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.65.0",
+ "@typescript-eslint/types": "^8.65.0",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.65.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz",
+ "integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "8.65.0",
+ "@typescript-eslint/visitor-keys": "8.65.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.65.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz",
+ "integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==",
+ "dev": true,
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.65.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.65.0.tgz",
+ "integrity": "sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "8.65.0",
+ "@typescript-eslint/typescript-estree": "8.65.0",
+ "@typescript-eslint/utils": "8.65.0",
+ "debug": "^4.4.3",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.65.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz",
+ "integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==",
+ "dev": true,
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.65.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz",
+ "integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.65.0",
+ "@typescript-eslint/tsconfig-utils": "8.65.0",
+ "@typescript-eslint/types": "8.65.0",
+ "@typescript-eslint/visitor-keys": "8.65.0",
+ "debug": "^4.4.3",
+ "minimatch": "^10.2.2",
+ "semver": "^7.7.3",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.65.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.65.0.tgz",
+ "integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/scope-manager": "8.65.0",
+ "@typescript-eslint/types": "8.65.0",
+ "@typescript-eslint/typescript-estree": "8.65.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.65.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz",
+ "integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "8.65.0",
+ "eslint-visitor-keys": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.4.tgz",
+ "integrity": "sha512-XcCQz0TBpBgljhj0gMuuDj49i6Ytqh5q1osT/Gp5uAVJUCTWxyskk/l1jwYYiu2xcNHHipdMz40EGfM1VdamVg==",
+ "dev": true,
+ "dependencies": {
+ "@rolldown/pluginutils": "^1.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0",
+ "babel-plugin-react-compiler": "^1.0.0",
+ "vite": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@rolldown/plugin-babel": {
+ "optional": true
+ },
+ "babel-plugin-react-compiler": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/coverage-v8": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.10.tgz",
+ "integrity": "sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==",
+ "dev": true,
+ "dependencies": {
+ "@bcoe/v8-coverage": "^1.0.2",
+ "@vitest/utils": "4.1.10",
+ "ast-v8-to-istanbul": "^1.0.0",
+ "istanbul-lib-coverage": "^3.2.2",
+ "istanbul-lib-report": "^3.0.1",
+ "istanbul-reports": "^3.2.0",
+ "magicast": "^0.5.2",
+ "obug": "^2.1.1",
+ "std-env": "^4.0.0-rc.1",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@vitest/browser": "4.1.10",
+ "vitest": "4.1.10"
+ },
+ "peerDependenciesMeta": {
+ "@vitest/browser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/expect": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz",
+ "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==",
+ "dev": true,
+ "dependencies": {
+ "@standard-schema/spec": "^1.1.0",
+ "@types/chai": "^5.2.2",
+ "@vitest/spy": "4.1.10",
+ "@vitest/utils": "4.1.10",
+ "chai": "^6.2.2",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/mocker": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz",
+ "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==",
+ "dev": true,
+ "dependencies": {
+ "@vitest/spy": "4.1.10",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.21"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/pretty-format": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz",
+ "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==",
+ "dev": true,
+ "dependencies": {
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz",
+ "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==",
+ "dev": true,
+ "dependencies": {
+ "@vitest/utils": "4.1.10",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz",
+ "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==",
+ "dev": true,
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.10",
+ "@vitest/utils": "4.1.10",
+ "magic-string": "^0.30.21",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz",
+ "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==",
+ "dev": true,
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz",
+ "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==",
+ "dev": true,
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.10",
+ "convert-source-map": "^2.0.0",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz",
+ "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==",
+ "dev": true,
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
+ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
+ "dev": true,
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/ast-v8-to-istanbul": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.5.tgz",
+ "integrity": "sha512-UPAgKJFSEGMWSDr3LX4tqnAb4f7KGT8O40Tyx8wbYmmZ/yn58lNCm8h3svs3eXgiGd5AXxz8NDOvXWvicq+rJA==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.31",
+ "estree-walker": "^3.0.3",
+ "js-tokens": "^10.0.0"
+ }
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "dev": true
+ },
+ "node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.11.13",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.13.tgz",
+ "integrity": "sha512-k9HNuUVMlqVjQ9UHzfPjIqiDbWw7WqT1AoT7GL8VwvF3r0ZfArtgiSPAlmupyNquNgOJHTuH4CKYf8ttMTWBTQ==",
+ "dev": true,
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "5.0.9",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
+ "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.8",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz",
+ "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "baseline-browser-mapping": "^2.11.12",
+ "caniuse-lite": "^1.0.30001809",
+ "electron-to-chromium": "^1.5.402",
+ "node-releases": "^2.0.53",
+ "update-browserslist-db": "^1.3.0"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "dev": true,
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001809",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz",
+ "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ]
+ },
+ "node_modules/chai": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
+ "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
+ "dev": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "dev": true,
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/cssstyle": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
+ "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
+ "dev": true,
+ "dependencies": {
+ "@asamuzakjp/css-color": "^3.2.0",
+ "rrweb-cssom": "^0.8.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/cssstyle/node_modules/rrweb-cssom": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
+ "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
+ "dev": true
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "dev": true
+ },
+ "node_modules/data-urls": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
+ "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
+ "dev": true,
+ "dependencies": {
+ "whatwg-mimetype": "^4.0.0",
+ "whatwg-url": "^14.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decimal.js": {
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
+ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
+ "dev": true
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "dev": true,
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.403",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.403.tgz",
+ "integrity": "sha512-MQsYmdaLzvaCX5j+ZZBr5Fm6uCCnPQcRtlvmvRlWqrXy+BH2O4ffXIAScF+JQznQWB9brWp4lSD9Z4yNmaf2BA==",
+ "dev": true
+ },
+ "node_modules/entities": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz",
+ "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==",
+ "dev": true
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
+ "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
+ "dev": true,
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "dev": true,
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
+ "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.28.1",
+ "@esbuild/android-arm": "0.28.1",
+ "@esbuild/android-arm64": "0.28.1",
+ "@esbuild/android-x64": "0.28.1",
+ "@esbuild/darwin-arm64": "0.28.1",
+ "@esbuild/darwin-x64": "0.28.1",
+ "@esbuild/freebsd-arm64": "0.28.1",
+ "@esbuild/freebsd-x64": "0.28.1",
+ "@esbuild/linux-arm": "0.28.1",
+ "@esbuild/linux-arm64": "0.28.1",
+ "@esbuild/linux-ia32": "0.28.1",
+ "@esbuild/linux-loong64": "0.28.1",
+ "@esbuild/linux-mips64el": "0.28.1",
+ "@esbuild/linux-ppc64": "0.28.1",
+ "@esbuild/linux-riscv64": "0.28.1",
+ "@esbuild/linux-s390x": "0.28.1",
+ "@esbuild/linux-x64": "0.28.1",
+ "@esbuild/netbsd-arm64": "0.28.1",
+ "@esbuild/netbsd-x64": "0.28.1",
+ "@esbuild/openbsd-arm64": "0.28.1",
+ "@esbuild/openbsd-x64": "0.28.1",
+ "@esbuild/openharmony-arm64": "0.28.1",
+ "@esbuild/sunos-x64": "0.28.1",
+ "@esbuild/win32-arm64": "0.28.1",
+ "@esbuild/win32-ia32": "0.28.1",
+ "@esbuild/win32-x64": "0.28.1"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "10.7.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.7.0.tgz",
+ "integrity": "sha512-GVTD7s1vdIl6UYvAfriOPeY1Df8LIZjfofLvHwde+erDHGGuHyuM6xoxRxmHiebhYuD2p1vN4wWh0XzPARSGDQ==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.2",
+ "@eslint/config-array": "^0.23.5",
+ "@eslint/config-helpers": "^0.6.0",
+ "@eslint/core": "^1.2.1",
+ "@eslint/plugin-kit": "^0.7.2",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "ajv": "^6.14.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^9.1.2",
+ "eslint-visitor-keys": "^5.0.1",
+ "espree": "^11.2.0",
+ "esquery": "^1.7.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "minimatch": "^10.2.4",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz",
+ "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==",
+ "dev": true,
+ "dependencies": {
+ "@babel/core": "^7.24.4",
+ "@babel/parser": "^7.24.4",
+ "hermes-parser": "^0.25.1",
+ "zod": "^3.25.0 || ^4.0.0",
+ "zod-validation-error": "^3.5.0 || ^4.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-react-refresh": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.3.tgz",
+ "integrity": "sha512-5EMmLCV98Pi4o/f/3DP/v/tNqLHMIc9I8LKClNDWhZ9JTho89/kQcitCXQBMG7sAfVRK0Ie3T2EDOzp1YXYiVA==",
+ "dev": true,
+ "peerDependencies": {
+ "eslint": "^9 || ^10"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "9.1.2",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz",
+ "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/esrecurse": "^4.3.1",
+ "@types/estree": "^1.0.8",
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "dev": true,
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree": {
+ "version": "11.2.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz",
+ "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==",
+ "dev": true,
+ "dependencies": {
+ "acorn": "^8.16.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^5.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
+ "dev": true,
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/expect-type": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
+ "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.4.4",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz",
+ "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==",
+ "dev": true
+ },
+ "node_modules/form-data": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
+ "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
+ "dev": true,
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.4",
+ "mime-types": "^2.1.35"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "dev": true,
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "15.14.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-15.14.0.tgz",
+ "integrity": "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==",
+ "dev": true,
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "dev": true,
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
+ "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
+ "dev": true,
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/hermes-estree": {
+ "version": "0.25.1",
+ "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
+ "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==",
+ "dev": true
+ },
+ "node_modules/hermes-parser": {
+ "version": "0.25.1",
+ "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz",
+ "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==",
+ "dev": true,
+ "dependencies": {
+ "hermes-estree": "0.25.1"
+ }
+ },
+ "node_modules/html-encoding-sniffer": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
+ "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
+ "dev": true,
+ "dependencies": {
+ "whatwg-encoding": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/html-escaper": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
+ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
+ "dev": true
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+ "dev": true,
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "dev": true,
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dev": true,
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-potential-custom-element-name": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+ "dev": true
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true
+ },
+ "node_modules/istanbul-lib-coverage": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
+ "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-report": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
+ "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
+ "dev": true,
+ "dependencies": {
+ "istanbul-lib-coverage": "^3.0.0",
+ "make-dir": "^4.0.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-reports": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
+ "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
+ "dev": true,
+ "dependencies": {
+ "html-escaper": "^2.0.0",
+ "istanbul-lib-report": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz",
+ "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==",
+ "dev": true
+ },
+ "node_modules/jsdom": {
+ "version": "25.0.1",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz",
+ "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==",
+ "dev": true,
+ "dependencies": {
+ "cssstyle": "^4.1.0",
+ "data-urls": "^5.0.0",
+ "decimal.js": "^10.4.3",
+ "form-data": "^4.0.0",
+ "html-encoding-sniffer": "^4.0.0",
+ "http-proxy-agent": "^7.0.2",
+ "https-proxy-agent": "^7.0.5",
+ "is-potential-custom-element-name": "^1.0.1",
+ "nwsapi": "^2.2.12",
+ "parse5": "^7.1.2",
+ "rrweb-cssom": "^0.7.1",
+ "saxes": "^6.0.0",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^5.0.0",
+ "w3c-xmlserializer": "^5.0.0",
+ "webidl-conversions": "^7.0.0",
+ "whatwg-encoding": "^3.1.1",
+ "whatwg-mimetype": "^4.0.0",
+ "whatwg-url": "^14.0.0",
+ "ws": "^8.18.0",
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "canvas": "^2.11.2"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
+ "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",
+ "dev": true,
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.33.0",
+ "lightningcss-darwin-arm64": "1.33.0",
+ "lightningcss-darwin-x64": "1.33.0",
+ "lightningcss-freebsd-x64": "1.33.0",
+ "lightningcss-linux-arm-gnueabihf": "1.33.0",
+ "lightningcss-linux-arm64-gnu": "1.33.0",
+ "lightningcss-linux-arm64-musl": "1.33.0",
+ "lightningcss-linux-x64-gnu": "1.33.0",
+ "lightningcss-linux-x64-musl": "1.33.0",
+ "lightningcss-win32-arm64-msvc": "1.33.0",
+ "lightningcss-win32-x64-msvc": "1.33.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz",
+ "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz",
+ "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz",
+ "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz",
+ "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz",
+ "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz",
+ "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz",
+ "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz",
+ "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz",
+ "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz",
+ "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz",
+ "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/lucide-react": {
+ "version": "0.468.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.468.0.tgz",
+ "integrity": "sha512-6koYRhnM2N0GGZIdXzSeiNwguv1gt/FAjZOiPl76roBi3xKEXa4WmfpxgQwTTL4KipXjefrnf3oV4IsYhi4JFA==",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/magicast": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.4.tgz",
+ "integrity": "sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w==",
+ "dev": true,
+ "dependencies": {
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/make-dir": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
+ "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
+ "dev": true,
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/make-dir/node_modules/semver": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dev": true,
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "10.2.6",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
+ "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^5.0.8"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.18",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
+ "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.53",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz",
+ "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/nwsapi": {
+ "version": "2.2.24",
+ "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz",
+ "integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==",
+ "dev": true
+ },
+ "node_modules/obug": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz",
+ "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==",
+ "dev": true,
+ "funding": [
+ "https://github.com/sponsors/sxzz",
+ "https://opencollective.com/debug"
+ ],
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parse5": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
+ "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
+ "dev": true,
+ "dependencies": {
+ "entities": "^6.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "dev": true
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
+ "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.54.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.54.1.tgz",
+ "integrity": "sha512-Nbjs2zjj0htNhzgiy5wu+3w09YetDx5pkrpI/kZotDlDUaYk0HVA5xrBVPdow4SAUIlhgKcJeJg4GRKW6xHusA==",
+ "dev": true,
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.26",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
+ "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "nanoid": "^3.3.17",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/react": {
+ "version": "19.0.0",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
+ "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.0.0",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz",
+ "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==",
+ "dependencies": {
+ "scheduler": "^0.25.0"
+ },
+ "peerDependencies": {
+ "react": "^19.0.0"
+ }
+ },
+ "node_modules/rolldown": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz",
+ "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==",
+ "dev": true,
+ "dependencies": {
+ "@oxc-project/types": "=0.139.0",
+ "@rolldown/pluginutils": "^1.0.0"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-android-arm64": "1.1.5",
+ "@rolldown/binding-darwin-arm64": "1.1.5",
+ "@rolldown/binding-darwin-x64": "1.1.5",
+ "@rolldown/binding-freebsd-x64": "1.1.5",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.1.5",
+ "@rolldown/binding-linux-arm64-gnu": "1.1.5",
+ "@rolldown/binding-linux-arm64-musl": "1.1.5",
+ "@rolldown/binding-linux-ppc64-gnu": "1.1.5",
+ "@rolldown/binding-linux-s390x-gnu": "1.1.5",
+ "@rolldown/binding-linux-x64-gnu": "1.1.5",
+ "@rolldown/binding-linux-x64-musl": "1.1.5",
+ "@rolldown/binding-openharmony-arm64": "1.1.5",
+ "@rolldown/binding-wasm32-wasi": "1.1.5",
+ "@rolldown/binding-win32-arm64-msvc": "1.1.5",
+ "@rolldown/binding-win32-x64-msvc": "1.1.5"
+ }
+ },
+ "node_modules/rrweb-cssom": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz",
+ "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==",
+ "dev": true
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true
+ },
+ "node_modules/saxes": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
+ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
+ "dev": true,
+ "dependencies": {
+ "xmlchars": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=v12.22.7"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.25.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz",
+ "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA=="
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stackback": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+ "dev": true
+ },
+ "node_modules/std-env": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz",
+ "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==",
+ "dev": true
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/symbol-tree": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+ "dev": true
+ },
+ "node_modules/tinybench": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true
+ },
+ "node_modules/tinyexec": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz",
+ "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+ "dev": true,
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyrainbow": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz",
+ "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==",
+ "dev": true,
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tldts": {
+ "version": "6.1.86",
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
+ "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
+ "dev": true,
+ "dependencies": {
+ "tldts-core": "^6.1.86"
+ },
+ "bin": {
+ "tldts": "bin/cli.js"
+ }
+ },
+ "node_modules/tldts-core": {
+ "version": "6.1.86",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
+ "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
+ "dev": true
+ },
+ "node_modules/tough-cookie": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
+ "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
+ "dev": true,
+ "dependencies": {
+ "tldts": "^6.1.32"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
+ "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
+ "dev": true,
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/ts-api-utils": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
+ "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
+ "dev": true,
+ "engines": {
+ "node": ">=18.12"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "optional": true
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.7.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz",
+ "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==",
+ "dev": true,
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/typescript-eslint": {
+ "version": "8.65.0",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.65.0.tgz",
+ "integrity": "sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.65.0",
+ "@typescript-eslint/parser": "8.65.0",
+ "@typescript-eslint/typescript-estree": "8.65.0",
+ "@typescript-eslint/utils": "8.65.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
+ "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
+ "dev": true
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.0.tgz",
+ "integrity": "sha512-x/M6q3w4Ybp91CNaS4S69UnliqR3BzRpOT6LWbksjth0S/+jhfaPJsWjt/TewpT8j9eLIojUf5jr29WextHroA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "8.1.5",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz",
+ "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==",
+ "dev": true,
+ "dependencies": {
+ "lightningcss": "^1.32.0",
+ "picomatch": "^4.0.5",
+ "postcss": "^8.5.17",
+ "rolldown": "~1.1.5",
+ "tinyglobby": "^0.2.17"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "@vitejs/devtools": "^0.3.0",
+ "esbuild": "^0.27.0 || ^0.28.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "@vitejs/devtools": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitest": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz",
+ "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==",
+ "dev": true,
+ "dependencies": {
+ "@vitest/expect": "4.1.10",
+ "@vitest/mocker": "4.1.10",
+ "@vitest/pretty-format": "4.1.10",
+ "@vitest/runner": "4.1.10",
+ "@vitest/snapshot": "4.1.10",
+ "@vitest/spy": "4.1.10",
+ "@vitest/utils": "4.1.10",
+ "es-module-lexer": "^2.0.0",
+ "expect-type": "^1.3.0",
+ "magic-string": "^0.30.21",
+ "obug": "^2.1.1",
+ "pathe": "^2.0.3",
+ "picomatch": "^4.0.3",
+ "std-env": "^4.0.0-rc.1",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^1.0.2",
+ "tinyglobby": "^0.2.15",
+ "tinyrainbow": "^3.1.0",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@opentelemetry/api": "^1.9.0",
+ "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
+ "@vitest/browser-playwright": "4.1.10",
+ "@vitest/browser-preview": "4.1.10",
+ "@vitest/browser-webdriverio": "4.1.10",
+ "@vitest/coverage-istanbul": "4.1.10",
+ "@vitest/coverage-v8": "4.1.10",
+ "@vitest/ui": "4.1.10",
+ "happy-dom": "*",
+ "jsdom": "*",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser-playwright": {
+ "optional": true
+ },
+ "@vitest/browser-preview": {
+ "optional": true
+ },
+ "@vitest/browser-webdriverio": {
+ "optional": true
+ },
+ "@vitest/coverage-istanbul": {
+ "optional": true
+ },
+ "@vitest/coverage-v8": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ },
+ "vite": {
+ "optional": false
+ }
+ }
+ },
+ "node_modules/w3c-xmlserializer": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
+ "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
+ "dev": true,
+ "dependencies": {
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/whatwg-encoding": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
+ "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
+ "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
+ "dev": true,
+ "dependencies": {
+ "iconv-lite": "0.6.3"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
+ "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
+ "dev": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "14.2.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
+ "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
+ "dev": true,
+ "dependencies": {
+ "tr46": "^5.1.0",
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/why-is-node-running": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+ "dev": true,
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ws": {
+ "version": "8.21.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",
+ "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==",
+ "dev": true,
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/xml-name-validator": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
+ "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
+ "dev": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/xmlchars": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+ "dev": true
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/zod": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
+ "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/zod-validation-error": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz",
+ "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "zod": "^3.25.0 || ^4.0.0"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..79bdc6f
--- /dev/null
+++ b/package.json
@@ -0,0 +1,49 @@
+{
+ "name": "queuescope",
+ "version": "0.1.0",
+ "private": true,
+ "description": "Retailer-neutral local-first browser operations console for scheduled page observation and virtual-queue awareness.",
+ "type": "module",
+ "license": "MIT",
+ "scripts": {
+ "dev": "vite --host 127.0.0.1",
+ "build": "tsc --noEmit && vite build && npm run bundle:observer && node scripts/verify-build.mjs",
+ "bundle:observer": "node scripts/bundle-observer.mjs",
+ "test": "vitest run",
+ "test:watch": "vitest",
+ "test:coverage": "vitest run --coverage",
+ "lint": "eslint . --max-warnings 0",
+ "check": "npm run lint && npm run test && npm run build",
+ "test:e2e": "node scripts/e2e-extension.mjs",
+ "serve": "vite preview --host 127.0.0.1 --port 4175 --strictPort",
+ "package": "powershell -NoProfile -ExecutionPolicy Bypass -File scripts/package-release.ps1"
+ },
+ "dependencies": {
+ "lucide-react": "0.468.0",
+ "react": "19.0.0",
+ "react-dom": "19.0.0"
+ },
+ "devDependencies": {
+ "@eslint/js": "10.0.1",
+ "@types/chrome": "0.0.287",
+ "@types/node": "26.1.1",
+ "@types/react": "19.0.3",
+ "@types/react-dom": "19.0.2",
+ "@vitejs/plugin-react": "6.0.4",
+ "@vitest/coverage-v8": "4.1.10",
+ "esbuild": "0.28.1",
+ "eslint": "10.7.0",
+ "eslint-plugin-react-hooks": "7.1.1",
+ "eslint-plugin-react-refresh": "0.5.3",
+ "globals": "15.14.0",
+ "jsdom": "25.0.1",
+ "playwright-core": "1.54.1",
+ "typescript": "5.7.2",
+ "typescript-eslint": "8.65.0",
+ "vite": "8.1.5",
+ "vitest": "4.1.10"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+}
diff --git a/public/icons/icon-128.png b/public/icons/icon-128.png
new file mode 100644
index 0000000..4cf030f
Binary files /dev/null and b/public/icons/icon-128.png differ
diff --git a/public/icons/icon-16.png b/public/icons/icon-16.png
new file mode 100644
index 0000000..ff20e00
Binary files /dev/null and b/public/icons/icon-16.png differ
diff --git a/public/icons/icon-32.png b/public/icons/icon-32.png
new file mode 100644
index 0000000..e09a8e5
Binary files /dev/null and b/public/icons/icon-32.png differ
diff --git a/public/icons/icon-48.png b/public/icons/icon-48.png
new file mode 100644
index 0000000..110c223
Binary files /dev/null and b/public/icons/icon-48.png differ
diff --git a/public/manifest.json b/public/manifest.json
new file mode 100644
index 0000000..dc41223
--- /dev/null
+++ b/public/manifest.json
@@ -0,0 +1,34 @@
+{
+ "manifest_version": 3,
+ "name": "QueueScope",
+ "version": "0.1.0",
+ "description": "A retailer-neutral local-first console for scheduled page observation and virtual-queue awareness.",
+ "minimum_chrome_version": "120",
+ "permissions": [
+ "alarms",
+ "notifications",
+ "offscreen",
+ "sidePanel",
+ "storage",
+ "scripting",
+ "tabs"
+ ],
+ "optional_host_permissions": ["http://*/*", "https://*/*"],
+ "host_permissions": ["http://127.0.0.1/*", "http://localhost/*"],
+ "background": {
+ "service_worker": "service-worker.js",
+ "type": "module"
+ },
+ "action": { "default_title": "Open QueueScope" },
+ "side_panel": { "default_path": "sidepanel.html" },
+ "options_page": "dashboard.html",
+ "icons": {
+ "16": "icons/icon-16.png",
+ "32": "icons/icon-32.png",
+ "48": "icons/icon-48.png",
+ "128": "icons/icon-128.png"
+ },
+ "content_security_policy": {
+ "extension_pages": "script-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'"
+ }
+}
diff --git a/release/QueueScope-0.1.0-unpacked.sha256.txt b/release/QueueScope-0.1.0-unpacked.sha256.txt
new file mode 100644
index 0000000..39beaf2
--- /dev/null
+++ b/release/QueueScope-0.1.0-unpacked.sha256.txt
@@ -0,0 +1 @@
+4198d6ab42e345ffc2471cbe60165e95a4a426a61b643418a62d7165d5eacbbc QueueScope-0.1.0-unpacked.zip
diff --git a/scripts/bundle-observer.mjs b/scripts/bundle-observer.mjs
new file mode 100644
index 0000000..c378894
--- /dev/null
+++ b/scripts/bundle-observer.mjs
@@ -0,0 +1,5 @@
+import { copyFile } from "node:fs/promises";
+import { resolve } from "node:path";
+
+await copyFile(resolve("src/content/observer-standalone.js"), resolve("dist/observer.js"));
+console.log("Copied audited standalone page observer.");
diff --git a/scripts/e2e-extension.mjs b/scripts/e2e-extension.mjs
new file mode 100644
index 0000000..52075f4
--- /dev/null
+++ b/scripts/e2e-extension.mjs
@@ -0,0 +1,136 @@
+import { access, mkdir, mkdtemp, readdir, rm } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { basename, join, resolve } from "node:path";
+import { chromium } from "playwright-core";
+
+async function browserPath() {
+ if (process.env.QUEUESCOPE_CHROME_PATH) return process.env.QUEUESCOPE_CHROME_PATH;
+ const root = join(process.env.LOCALAPPDATA ?? "", "ms-playwright");
+ try {
+ for (const name of (await readdir(root)).filter((value) => value.startsWith("chromium-")).sort().reverse()) {
+ const candidate = join(root, name, "chrome-win64", "chrome.exe");
+ try { await access(candidate); return candidate; } catch { /* next */ }
+ }
+ } catch { /* use system browser */ }
+ const programFiles = process.env.PROGRAMFILES;
+ if (!programFiles) throw new Error("No Playwright Chromium or PROGRAMFILES browser root is available.");
+ return join(programFiles, "Google", "Chrome", "Application", "chrome.exe");
+}
+
+const dist = resolve("dist");
+const results = resolve("test-results");
+const profile = await mkdtemp(join(tmpdir(), "queuescope-public-e2e-"));
+if (!profile.startsWith(tmpdir()) || !basename(profile).startsWith("queuescope-public-e2e-")) throw new Error("Unsafe E2E profile path.");
+await mkdir(results, { recursive: true });
+let context;
+const browserErrors = [];
+try {
+ context = await chromium.launchPersistentContext(profile, {
+ executablePath: await browserPath(), headless: false, viewport: { width: 1440, height: 1000 },
+ ignoreDefaultArgs: ["--disable-extensions"],
+ args: [`--disable-extensions-except=${dist}`, `--load-extension=${dist}`, "--no-first-run", "--no-default-browser-check", "--disable-component-update"]
+ });
+ const worker = context.serviceWorkers()[0] ?? await context.waitForEvent("serviceworker", { timeout: 15_000 });
+ const id = new URL(worker.url()).host;
+ if (!id) throw new Error("Extension service worker did not load.");
+
+ const dashboard = await context.newPage();
+ dashboard.on("console", (message) => { if (message.type() === "error") browserErrors.push(message.text()); });
+ dashboard.on("pageerror", (error) => browserErrors.push(error.message));
+ await dashboard.goto(`chrome-extension://${id}/dashboard.html`);
+ await dashboard.getByRole("heading", { name: "Observe the moment. Preserve the tab." }).waitFor();
+ if ((await dashboard.title()) !== "QueueScope") throw new Error("Dashboard tab title is not QueueScope.");
+ await dashboard.screenshot({ path: join(results, "command-center.png"), fullPage: true });
+
+ await dashboard.getByRole("button", { name: "Queue Lab", exact: true }).click();
+ await dashboard.getByRole("heading", { name: "Rehearse the state machine before it matters." }).waitFor();
+ await dashboard.waitForTimeout(500);
+ const orbit = await dashboard.locator(".lab-orbit").boundingBox();
+ const dot = await dashboard.locator(".orbiter").boundingBox();
+ if (!orbit || !dot) throw new Error("Queue Lab orbit geometry is missing.");
+ const distance = Math.hypot(dot.x + dot.width / 2 - (orbit.x + orbit.width / 2), dot.y + dot.height / 2 - (orbit.y + orbit.height / 2));
+ if (Math.abs(distance - orbit.width / 2) > 3) throw new Error(`Queue Lab orbit dot drifted by ${Math.abs(distance - orbit.width / 2).toFixed(2)}px.`);
+ await dashboard.screenshot({ path: join(results, "queue-lab.png"), fullPage: true });
+
+ const opened = await dashboard.evaluate(async () => chrome.runtime.sendMessage({ type: "OPEN_QUEUE_LAB" }));
+ if (!opened?.ok) throw new Error(`Queue Lab did not open: ${opened?.error || "unknown runtime error"}`);
+ let lab = context.pages().find((page) => page.url().includes("/harness/index.html"));
+ for (let attempt = 0; !lab && attempt < 40; attempt += 1) {
+ await dashboard.waitForTimeout(250);
+ lab = context.pages().find((page) => page.url().includes("/harness/index.html"));
+ }
+ if (!lab) throw new Error(`Queue Lab tab was not exposed to Playwright. Pages: ${context.pages().map((page) => page.url()).join(", ")}`);
+ await lab.getByRole("heading", { name: "Product page observed" }).waitFor();
+ await lab.getByRole("button", { name: /Queue Lock navigation/ }).click();
+ await lab.getByRole("heading", { name: "Waiting room detected" }).waitFor();
+ await lab.screenshot({ path: join(results, "queue-lab-waiting.png"), fullPage: true });
+
+ await dashboard.bringToFront();
+ await dashboard.getByRole("button", { name: "Live runs", exact: true }).click();
+ await dashboard.getByText("LOCKED", { exact: true }).waitFor({ timeout: 10_000 });
+ if (await dashboard.locator(".run-card.is-open").count()) throw new Error("Live runs were not collapsed by default.");
+ await dashboard.locator(".run-card-head").first().click();
+ await dashboard.getByText("Queue lock armed", { exact: true }).waitFor();
+ const stored = await dashboard.evaluate(async () => (await chrome.runtime.sendMessage({ type: "GET_SNAPSHOT" })).data);
+ if (!stored.activeRuns[0]?.queueLocked || stored.activeRuns[0].lastObservation?.position !== 428) throw new Error("Queue Lab state was not persisted with position 428.");
+ const tab = await dashboard.evaluate(async (tabId) => chrome.tabs.get(tabId), stored.activeRuns[0].tabId);
+ if (tab.autoDiscardable !== false) throw new Error("Queue-locked tab was not protected from background discard.");
+ await dashboard.screenshot({ path: join(results, "live-run.png"), fullPage: true });
+
+ await lab.bringToFront();
+ await lab.getByRole("button", { name: /Admitted After queue only/ }).click();
+ await dashboard.bringToFront();
+ await dashboard.waitForFunction(async () => (await chrome.runtime.sendMessage({ type: "GET_SNAPSHOT" })).data.activeRuns[0]?.state === "ADMITTED");
+
+ const panel = await context.newPage();
+ await panel.setViewportSize({ width: 420, height: 840 });
+ panel.on("console", (message) => { if (message.type() === "error") browserErrors.push(message.text()); });
+ panel.on("pageerror", (error) => browserErrors.push(error.message));
+ await panel.goto(`chrome-extension://${id}/sidepanel.html`);
+ await panel.getByText("ADMITTED", { exact: true }).waitFor();
+ if (await panel.locator(".run-card.is-open").count()) throw new Error("Side-panel run was not collapsed by default.");
+ const footer = await panel.locator(".panel-footer").boundingBox();
+ const viewport = panel.viewportSize();
+ if (!footer || !viewport || Math.abs(footer.y + footer.height - viewport.height) > 2) throw new Error("Side-panel footer is not pinned to the viewport.");
+ await panel.screenshot({ path: join(results, "side-panel.png"), fullPage: true });
+
+ const panelWatchName = "Synthetic recurring watch";
+ const savedWatch = await dashboard.evaluate(async (name) => {
+ const now = Date.now();
+ return chrome.runtime.sendMessage({ type: "SAVE_WATCH", watch: {
+ id: crypto.randomUUID(), name, url: "https://example.test/product", profile: "scheduled", mode: "observe-only",
+ schedule: { type: "once", enabled: false, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC", weekdays: [], startAt: now + 600_000, expectedAt: now + 900_000, stopAt: now + 1_800_000 },
+ cadenceSeconds: 60, activeCadenceSeconds: 15, fastWindowBeforeMinutes: 10, fastWindowAfterMinutes: 10, jitterPercent: 5,
+ rules: { queueText: ["you are now in the waiting room"], admittedText: ["access granted. your session is ready"], challengeText: ["please verify you are human"], unavailableText: ["not available yet"], availableSelector: "[data-queuescope-available='true']", positionPattern: "position\\s+(\\d+)", etaPattern: "estimated wait\\s+(\\d{2}:\\d{2}:\\d{2})" },
+ createdAt: now, updatedAt: now
+ } });
+ }, panelWatchName);
+ if (!savedWatch?.ok) throw new Error(`Could not seed panel watch: ${savedWatch?.error}`);
+ await panel.getByRole("button", { name: "Watches", exact: true }).click();
+ const panelWatch = panel.locator(".watch-card").filter({ hasText: panelWatchName });
+ await panelWatch.waitFor();
+ await panelWatch.getByTitle("Auto-start off; click to toggle").click();
+ await panelWatch.getByTitle("Auto-start on; click to toggle").waitFor();
+ await panelWatch.getByTitle("Copy watch").click();
+ const panelCopy = panel.locator(".watch-card").filter({ hasText: `${panelWatchName} — copy` });
+ await panelCopy.waitFor();
+ panel.once("dialog", (dialog) => dialog.accept());
+ await panelCopy.getByTitle("Delete watch").click();
+ await panelCopy.waitFor({ state: "detached" });
+ await panel.waitForTimeout(4_200);
+ await panel.evaluate(() => window.scrollTo({ top: 0, left: 0, behavior: "instant" }));
+ await panel.screenshot({ path: join(results, "side-panel-watches.png"), fullPage: true });
+
+ await dashboard.getByRole("button", { name: "Watches", exact: true }).click();
+ await dashboard.getByRole("button", { name: "New watch" }).click();
+ await dashboard.getByRole("button", { name: /Passive scout/ }).click();
+ await dashboard.getByRole("button", { name: /Continue/ }).click();
+ await dashboard.getByText("Passive watches have no attempt ceiling.").waitFor();
+ await dashboard.screenshot({ path: join(results, "watch-builder.png"), fullPage: true });
+
+ if (browserErrors.length) throw new Error(`Browser errors: ${browserErrors.join(" | ")}`);
+ console.log(JSON.stringify({ extensionId: id, queueLocked: true, position: 428, admission: "ADMITTED", screenshots: await readdir(results) }, null, 2));
+} finally {
+ if (context) await context.close();
+ await rm(profile, { recursive: true, force: true });
+}
diff --git a/scripts/package-release.ps1 b/scripts/package-release.ps1
new file mode 100644
index 0000000..f4008d1
--- /dev/null
+++ b/scripts/package-release.ps1
@@ -0,0 +1,17 @@
+$ErrorActionPreference = "Stop"
+$root = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
+$dist = (Resolve-Path (Join-Path $root "dist")).Path
+$release = Join-Path $root "release"
+$archive = Join-Path $release "QueueScope-0.1.0-unpacked.zip"
+$checksum = Join-Path $release "QueueScope-0.1.0-unpacked.sha256.txt"
+
+if ($dist -eq $root -or -not $dist.StartsWith($root, [System.StringComparison]::OrdinalIgnoreCase)) {
+ throw "Refusing to package an unsafe distribution path."
+}
+New-Item -ItemType Directory -Force -Path $release | Out-Null
+if (Test-Path -LiteralPath $archive) { Remove-Item -LiteralPath $archive -Force }
+Compress-Archive -Path (Join-Path $dist "*") -DestinationPath $archive -CompressionLevel Optimal
+$hash = (Get-FileHash -LiteralPath $archive -Algorithm SHA256).Hash.ToLowerInvariant()
+Set-Content -LiteralPath $checksum -Value "$hash QueueScope-0.1.0-unpacked.zip" -Encoding utf8
+Write-Output "Packaged $archive"
+Write-Output "SHA256 $hash"
diff --git a/scripts/verify-build.mjs b/scripts/verify-build.mjs
new file mode 100644
index 0000000..bc7c272
--- /dev/null
+++ b/scripts/verify-build.mjs
@@ -0,0 +1,33 @@
+import { access, readFile, readdir, stat } from "node:fs/promises";
+import { resolve } from "node:path";
+
+const root = resolve("dist");
+const required = [
+ "manifest.json", "dashboard.html", "sidepanel.html", "offscreen.html",
+ "service-worker.js", "observer.js", "harness/index.html",
+ "icons/icon-16.png", "icons/icon-32.png", "icons/icon-48.png", "icons/icon-128.png"
+];
+for (const file of required) await access(resolve(root, file));
+
+const manifest = JSON.parse(await readFile(resolve(root, "manifest.json"), "utf8"));
+if (manifest.manifest_version !== 3) throw new Error("Release manifest is not MV3.");
+if (manifest.version !== "0.1.0") throw new Error("Release manifest version does not match 0.1.0.");
+if (!Array.isArray(manifest.optional_host_permissions) || manifest.optional_host_permissions.length !== 2) throw new Error("Origin access is not optional and bounded as expected.");
+if (manifest.content_scripts) throw new Error("Public build must not inject globally declared content scripts.");
+
+async function walk(directory) {
+ const values = [];
+ for (const name of await readdir(directory)) {
+ const path = resolve(directory, name);
+ const info = await stat(path);
+ if (info.isDirectory()) values.push(...await walk(path)); else values.push(path);
+ }
+ return values;
+}
+const files = await walk(root);
+for (const file of files.filter((value) => /\.(?:html|js|css|json)$/.test(value))) {
+ const source = await readFile(file, "utf8");
+ if (/(?:fetch|importScripts)\s*\(\s*["'`]https?:\/\//i.test(source)) throw new Error(`Remote code or data request found in release asset: ${file}`);
+ if (/\beval\s*\(|new\s+Function\s*\(/.test(source)) throw new Error(`Dynamic code execution found in release asset: ${file}`);
+}
+console.log(`Verified ${files.length} release files; MV3 package is self-contained.`);
diff --git a/sidepanel.html b/sidepanel.html
new file mode 100644
index 0000000..2d1148f
--- /dev/null
+++ b/sidepanel.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+ QueueScope
+
+
+
+
+
+
diff --git a/src/background/offscreen.ts b/src/background/offscreen.ts
new file mode 100644
index 0000000..c24d516
--- /dev/null
+++ b/src/background/offscreen.ts
@@ -0,0 +1,37 @@
+type ToneMessage = { type: "PLAY_TONE_INTERNAL"; tone: "signal" | "soft" | "urgent" };
+
+const tones = {
+ signal: { frequencies: [660, 880], duration: 0.16, gain: 0.12 },
+ soft: { frequencies: [440, 554], duration: 0.24, gain: 0.08 },
+ urgent: { frequencies: [880, 660, 990], duration: 0.12, gain: 0.14 }
+} as const;
+
+async function play(tone: keyof typeof tones) {
+ const preset = tones[tone];
+ const context = new AudioContext();
+ const master = context.createGain();
+ master.gain.value = preset.gain;
+ master.connect(context.destination);
+ const start = context.currentTime;
+ preset.frequencies.forEach((frequency, index) => {
+ const oscillator = context.createOscillator();
+ const envelope = context.createGain();
+ const begins = start + index * preset.duration;
+ oscillator.type = "sine";
+ oscillator.frequency.value = frequency;
+ envelope.gain.setValueAtTime(0.0001, begins);
+ envelope.gain.exponentialRampToValueAtTime(1, begins + 0.015);
+ envelope.gain.exponentialRampToValueAtTime(0.0001, begins + preset.duration);
+ oscillator.connect(envelope);
+ envelope.connect(master);
+ oscillator.start(begins);
+ oscillator.stop(begins + preset.duration + 0.02);
+ });
+ await new Promise((resolve) => setTimeout(resolve, preset.frequencies.length * preset.duration * 1000 + 80));
+ await context.close();
+}
+
+chrome.runtime.onMessage.addListener((message: ToneMessage) => {
+ if (message?.type !== "PLAY_TONE_INTERNAL" || !(message.tone in tones)) return;
+ void play(message.tone);
+});
diff --git a/src/background/service-worker.ts b/src/background/service-worker.ts
new file mode 100644
index 0000000..88c0ac4
--- /dev/null
+++ b/src/background/service-worker.ts
@@ -0,0 +1,363 @@
+import { createDefaultWatch, PASSIVE_STOP_AT } from "../core/defaults";
+import { activeOccurrence, currentOrNextOccurrence } from "../core/schedule";
+import { advanceClock, applyObservation, createRun, endRun, recordCheck, refreshDecision } from "../core/state-machine";
+import { loadSnapshot, saveSnapshot } from "../core/storage";
+import type { AppSnapshot, AttachableTab, Observation, Run, RuntimeRequest, RuntimeResponse, ToneId, WatchRules } from "../core/types";
+import { canonicalizeUrl, isRuntimeRequest, normalizeSettings, normalizeWatch } from "../core/validation";
+
+const RECONCILE_ALARM = "queuescope:reconcile";
+const RUN_ALARM_PREFIX = "queuescope:run:";
+let snapshotPromise: Promise | undefined;
+let mutationQueue = Promise.resolve();
+
+function snapshot(): Promise {
+ snapshotPromise ??= loadSnapshot();
+ return snapshotPromise;
+}
+
+async function mutate(operation: (value: AppSnapshot) => Promise | T): Promise {
+ let resolveResult!: (value: T) => void;
+ let rejectResult!: (error: unknown) => void;
+ const result = new Promise((resolve, reject) => { resolveResult = resolve; rejectResult = reject; });
+ mutationQueue = mutationQueue.catch(() => undefined).then(async () => {
+ try {
+ const value = await snapshot();
+ const response = await operation(value);
+ await saveSnapshot(value);
+ resolveResult(response);
+ } catch (error) {
+ rejectResult(error);
+ }
+ });
+ return result;
+}
+
+function response(data?: T): RuntimeResponse {
+ return { ok: true, data };
+}
+
+function failure(error: unknown): RuntimeResponse {
+ return { ok: false, error: error instanceof Error ? error.message : "QueueScope could not complete the request." };
+}
+
+function isHttpUrl(value: string): boolean {
+ try { return ["http:", "https:"].includes(new URL(value).protocol); } catch { return false; }
+}
+
+async function hasPermission(url: string): Promise {
+ const origin = `${new URL(url).origin}/*`;
+ return chrome.permissions.contains({ origins: [origin] });
+}
+
+async function injectObserver(tabId: number) {
+ try {
+ await chrome.scripting.executeScript({ target: { tabId, allFrames: false }, files: ["observer.js"] });
+ } catch {
+ // Restricted pages and closing tabs are intentionally ignored.
+ }
+}
+
+function runAlarmName(runId: string): string {
+ return `${RUN_ALARM_PREFIX}${runId}`;
+}
+
+async function scheduleRun(run: Run) {
+ await chrome.alarms.clear(runAlarmName(run.id));
+ if (["COMPLETE", "EXPIRED", "FAILED", "ADMITTED"].includes(run.state) || run.queueLocked) return;
+ const when = Math.max(Date.now() + 250, run.nextCheckAt ?? run.startAt);
+ await chrome.alarms.create(runAlarmName(run.id), { when });
+}
+
+async function ensureOffscreen() {
+ if (!await chrome.offscreen.hasDocument()) {
+ await chrome.offscreen.createDocument({ url: "offscreen.html", reasons: [chrome.offscreen.Reason.AUDIO_PLAYBACK], justification: "Play local user-selected attention tones." });
+ }
+}
+
+async function playTone(tone: ToneId) {
+ await ensureOffscreen();
+ await chrome.runtime.sendMessage({ type: "PLAY_TONE_INTERNAL", tone });
+}
+
+async function notify(run: Run, title: string, message: string, priority = 1) {
+ const value = await snapshot();
+ if (!value.settings.notificationsEnabled) return;
+ await chrome.notifications.create(`queuescope:${run.id}:${Date.now()}`, {
+ type: "basic",
+ iconUrl: "icons/icon-128.png",
+ title,
+ message: message.slice(0, 240),
+ priority,
+ requireInteraction: priority === 2
+ });
+ if (value.settings.soundEnabled) await playTone(value.settings.tone);
+}
+
+async function armWatchById(watchId: string): Promise {
+ const result = await mutate(async (value) => {
+ const watch = value.watches.find((item) => item.id === watchId);
+ if (!watch) throw new Error("Watch not found.");
+ const existing = value.activeRuns.find((run) => run.watchId === watch.id || run.url === watch.url);
+ if (existing) return existing;
+ const occurrence = currentOrNextOccurrence(watch);
+ if (!occurrence) throw new Error("This watch has no current or future occurrence.");
+ if (!await hasPermission(watch.url)) throw new Error("Page access has not been granted for this origin.");
+ const tab = await chrome.tabs.create({ url: watch.url, active: false });
+ if (tab.id === undefined) throw new Error("Chrome did not create a preserved tab.");
+ const run = { ...createRun(watch, occurrence), tabId: tab.id };
+ value.activeRuns.push(run);
+ return run;
+ });
+ await scheduleRun(result);
+ if (result.tabId !== undefined) await injectObserver(result.tabId);
+ return result;
+}
+
+async function reconcileAutoStart() {
+ const value = await snapshot();
+ for (const watch of value.watches) {
+ if (!watch.schedule.enabled || !activeOccurrence(watch)) continue;
+ if (!value.activeRuns.some((run) => run.watchId === watch.id && run.occurrenceKey === currentOrNextOccurrence(watch)?.occurrenceKey)) {
+ try { await armWatchById(watch.id); } catch { /* Permission may require a future user gesture. */ }
+ }
+ }
+}
+
+async function processRun(runId: string) {
+ const result = await mutate(async (value) => {
+ const index = value.activeRuns.findIndex((run) => run.id === runId);
+ if (index < 0) return undefined;
+ let run = advanceClock(value.activeRuns[index]);
+ const decision = refreshDecision(run);
+ if (decision.action === "stop") {
+ run = endRun(run, "missed");
+ value.activeRuns.splice(index, 1);
+ value.history.unshift(run);
+ return { run, action: "stop" as const };
+ }
+ if (["refresh", "observe"].includes(decision.action)) run = recordCheck(run);
+ value.activeRuns[index] = run;
+ return { run, action: decision.action };
+ });
+ if (!result) return;
+ const { run, action } = result;
+ if (run.tabId !== undefined && action === "refresh") {
+ try { await chrome.tabs.reload(run.tabId); } catch { /* Closing tabs become recoverable errors on the next pass. */ }
+ }
+ if (run.tabId !== undefined && action === "observe") await injectObserver(run.tabId);
+ await scheduleRun(run);
+}
+
+function sanitizeObservation(input: Observation): Observation {
+ return {
+ observedAt: Math.min(Math.max(Number(input.observedAt) || Date.now(), Date.now() - 5 * 60_000), Date.now() + 10_000),
+ url: isHttpUrl(input.url) || input.url.startsWith(chrome.runtime.getURL("")) ? input.url.slice(0, 2048) : "",
+ title: String(input.title ?? "").slice(0, 180),
+ classification: ["product", "available", "unavailable", "queue", "challenge", "admitted", "unknown"].includes(input.classification) ? input.classification : "unknown",
+ confidence: Math.min(Math.max(Number(input.confidence) || 0, 0), 1),
+ evidence: Array.isArray(input.evidence) ? input.evidence.filter((item): item is string => typeof item === "string").slice(0, 8).map((item) => item.slice(0, 240)) : [],
+ position: Number.isFinite(input.position) && (input.position ?? -1) >= 0 ? Math.min(input.position!, 1_000_000_000) : undefined,
+ providerEtaAt: Number.isFinite(input.providerEtaAt) && (input.providerEtaAt ?? 0) > 0 ? Math.min(input.providerEtaAt!, Date.now() + 7 * 24 * 60 * 60_000) : undefined,
+ providerEtaLabel: typeof input.providerEtaLabel === "string" ? input.providerEtaLabel.slice(0, 80) : undefined
+ };
+}
+
+async function acceptObservation(tabId: number, raw: Observation) {
+ const observation = sanitizeObservation(raw);
+ const result = await mutate(async (value) => {
+ const index = value.activeRuns.findIndex((run) => run.tabId === tabId);
+ if (index < 0) return undefined;
+ const previous = value.activeRuns[index];
+ const updated = applyObservation(previous, observation);
+ value.activeRuns[index] = updated;
+ return { previous, updated };
+ });
+ if (!result) return;
+ const { previous, updated } = result;
+ if (updated.queueLocked && !previous.queueLocked && updated.tabId !== undefined) {
+ await chrome.tabs.update(updated.tabId, { autoDiscardable: false });
+ await notify(updated, "QueueScope safety lock", "Refresh stopped because a queue or challenge signal appeared.", 2);
+ }
+ if (updated.lastObservation?.classification === "available" && previous.lastObservation?.classification !== "available") {
+ await notify(updated, "Configured signal is available", updated.watchName, 2);
+ }
+ if (updated.state === "ADMITTED" && previous.state !== "ADMITTED") {
+ await notify(updated, "Queue admission detected", updated.watchName, 2);
+ const value = await snapshot();
+ if (value.settings.focusOnAdmission && updated.tabId !== undefined) await chrome.tabs.update(updated.tabId, { active: true });
+ }
+ const alertThreshold = (await snapshot()).settings.queueEtaAlertMinutes * 60_000;
+ const remaining = updated.lastObservation?.providerEtaAt ? updated.lastObservation.providerEtaAt - Date.now() : undefined;
+ const previousRemaining = previous.lastObservation?.providerEtaAt ? previous.lastObservation.providerEtaAt - Date.now() : undefined;
+ if (remaining !== undefined && remaining > 0 && remaining <= alertThreshold && (previousRemaining === undefined || previousRemaining > alertThreshold)) {
+ await notify(updated, "Queue turn is approaching", `${updated.watchName} · about ${Math.max(1, Math.ceil(remaining / 60_000))} minutes`, 2);
+ }
+ await scheduleRun(updated);
+}
+
+async function attachTab(tabId: number, rules?: WatchRules): Promise {
+ const tab = await chrome.tabs.get(tabId);
+ if (!tab.url || !isHttpUrl(tab.url)) throw new Error("Only ordinary HTTP or HTTPS tabs can be attached.");
+ const draft = createDefaultWatch();
+ draft.name = tab.title?.slice(0, 100) || new URL(tab.url).hostname;
+ draft.url = canonicalizeUrl(tab.url);
+ draft.profile = "passive";
+ draft.mode = "observe-only";
+ if (rules) draft.rules = rules;
+ const run = await mutate((value) => {
+ const existing = value.activeRuns.find((item) => item.tabId === tabId);
+ if (existing) return existing;
+ const created = { ...createRun(draft, { occurrenceKey: `attached:${Date.now()}`, startAt: Date.now(), stopAt: Date.now() + 12 * 60 * 60_000 }, Date.now(), true), tabId };
+ value.activeRuns.push(created);
+ return created;
+ });
+ await injectObserver(tabId);
+ return run;
+}
+
+async function openQueueLab(): Promise {
+ const url = chrome.runtime.getURL("harness/index.html");
+ const tab = await chrome.tabs.create({ url, active: true });
+ if (tab.id === undefined) throw new Error("Chrome did not open Queue Lab.");
+ const watch = createDefaultWatch();
+ watch.name = "Queue Lab rehearsal";
+ watch.url = url;
+ watch.profile = "passive";
+ watch.mode = "observe-only";
+ const run = await mutate((value) => {
+ const created = { ...createRun(watch, { occurrenceKey: `lab:${Date.now()}`, startAt: Date.now(), stopAt: PASSIVE_STOP_AT }, Date.now(), true), tabId: tab.id };
+ value.activeRuns.push(created);
+ return created;
+ });
+ return run;
+}
+
+async function handle(request: RuntimeRequest, sender: chrome.runtime.MessageSender): Promise {
+ try {
+ if (request.type === "GET_SNAPSHOT") return response(await snapshot());
+ if (request.type === "SAVE_WATCH") {
+ const watch = normalizeWatch(request.watch);
+ await mutate((value) => {
+ const index = value.watches.findIndex((item) => item.id === watch.id);
+ if (index >= 0) value.watches[index] = watch; else value.watches.push(watch);
+ });
+ if (watch.schedule.enabled) await reconcileAutoStart();
+ return response(watch);
+ }
+ if (request.type === "DELETE_WATCH") {
+ await mutate((value) => { value.watches = value.watches.filter((watch) => watch.id !== request.watchId); });
+ return response();
+ }
+ if (request.type === "ARM_WATCH") return response(await armWatchById(request.watchId));
+ if (request.type === "SAVE_SETTINGS") {
+ const settings = normalizeSettings(request.settings);
+ await mutate((value) => {
+ value.settings = settings;
+ const cutoff = Date.now() - settings.retentionDays * 24 * 60 * 60_000;
+ value.history = value.history.filter((run) => (run.endedAt ?? run.createdAt) >= cutoff).slice(0, 500);
+ });
+ return response(settings);
+ }
+ if (request.type === "LIST_ATTACHABLE_TABS") {
+ const value = await snapshot();
+ const tabs = await chrome.tabs.query({});
+ const data: AttachableTab[] = tabs.filter((tab): tab is chrome.tabs.Tab & { id: number; url: string } => tab.id !== undefined && Boolean(tab.url && isHttpUrl(tab.url))).map((tab) => ({ id: tab.id, title: tab.title?.slice(0, 120) || new URL(tab.url).hostname, url: tab.url, active: tab.active, attached: value.activeRuns.some((run) => run.tabId === tab.id) }));
+ return response(data);
+ }
+ if (request.type === "ATTACH_TAB") return response(await attachTab(request.tabId, request.rules));
+ if (request.type === "END_RUN") {
+ const completed = await mutate((value) => {
+ const index = value.activeRuns.findIndex((run) => run.id === request.runId);
+ if (index < 0) throw new Error("Run not found.");
+ const run = endRun(value.activeRuns[index], request.outcome);
+ value.activeRuns.splice(index, 1);
+ value.history.unshift(run);
+ value.history = value.history.slice(0, 500);
+ return run;
+ });
+ await chrome.alarms.clear(runAlarmName(completed.id));
+ if (completed.tabId !== undefined) await chrome.tabs.update(completed.tabId, { autoDiscardable: true });
+ return response(completed);
+ }
+ if (request.type === "FOCUS_RUN_TAB") {
+ const run = (await snapshot()).activeRuns.find((item) => item.id === request.runId);
+ if (!run?.tabId) throw new Error("Run tab is unavailable.");
+ await chrome.tabs.update(run.tabId, { active: true });
+ return response();
+ }
+ if (request.type === "OPEN_DASHBOARD") { await chrome.tabs.create({ url: chrome.runtime.getURL("dashboard.html") }); return response(); }
+ if (request.type === "OPEN_QUEUE_LAB") return response(await openQueueLab());
+ if (request.type === "PREVIEW_TONE") { await playTone(request.tone); return response(); }
+ if (request.type === "CONTENT_READY") { if (sender.tab?.id !== undefined) await injectObserver(sender.tab.id); return response(); }
+ if (request.type === "GET_TAB_CONFIG") {
+ const run = (await snapshot()).activeRuns.find((item) => item.tabId === sender.tab?.id);
+ if (!run) throw new Error("No active run owns this tab.");
+ return response({ rules: run.rules, priorQueueLock: run.queueLocked });
+ }
+ if (request.type === "OBSERVATION") {
+ if (sender.tab?.id === undefined) throw new Error("Observation has no browser tab.");
+ await acceptObservation(sender.tab.id, request.observation);
+ return response();
+ }
+ if (request.type === "LAB_OBSERVATION") {
+ if (!sender.url?.startsWith(chrome.runtime.getURL("harness/"))) throw new Error("Queue Lab observation came from an untrusted page.");
+ const labRun = (await snapshot()).activeRuns.find((run) => run.url === sender.url || run.url === chrome.runtime.getURL("harness/index.html"));
+ if (labRun?.tabId === undefined) throw new Error("Queue Lab run is unavailable.");
+ await acceptObservation(labRun.tabId, request.observation);
+ return response();
+ }
+ return failure("Unsupported request.");
+ } catch (error) {
+ return failure(error);
+ }
+}
+
+chrome.runtime.onMessage.addListener((message: unknown, sender, sendResponse) => {
+ if (!isRuntimeRequest(message)) return false;
+ void handle(message, sender).then(sendResponse);
+ return true;
+});
+
+chrome.alarms.onAlarm.addListener((alarm) => {
+ if (alarm.name === RECONCILE_ALARM) void reconcileAutoStart();
+ if (alarm.name.startsWith(RUN_ALARM_PREFIX)) void processRun(alarm.name.slice(RUN_ALARM_PREFIX.length));
+});
+
+chrome.tabs.onUpdated.addListener((tabId, changeInfo) => {
+ if (changeInfo.status !== "complete") return;
+ void snapshot().then((value) => { if (value.activeRuns.some((run) => run.tabId === tabId && isHttpUrl(run.url))) void injectObserver(tabId); });
+});
+
+chrome.tabs.onRemoved.addListener((tabId) => {
+ void mutate((value) => {
+ const run = value.activeRuns.find((item) => item.tabId === tabId);
+ if (!run) return;
+ run.tabId = undefined;
+ run.error = "Preserved tab was closed.";
+ run.state = "FAILED";
+ });
+});
+
+chrome.notifications.onClicked.addListener((notificationId) => {
+ const runId = notificationId.split(":")[1];
+ void snapshot().then((value) => {
+ const run = value.activeRuns.find((item) => item.id === runId);
+ if (run?.tabId !== undefined) void chrome.tabs.update(run.tabId, { active: true });
+ });
+});
+
+async function initialize() {
+ await chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true });
+ await chrome.alarms.create(RECONCILE_ALARM, { periodInMinutes: 1 });
+ const value = await snapshot();
+ for (const run of value.activeRuns) {
+ if (run.queueLocked && run.tabId !== undefined) await chrome.tabs.update(run.tabId, { autoDiscardable: false });
+ if (run.tabId !== undefined && isHttpUrl(run.url)) await injectObserver(run.tabId);
+ await scheduleRun(run);
+ }
+ await reconcileAutoStart();
+}
+
+chrome.runtime.onInstalled.addListener(() => void initialize());
+chrome.runtime.onStartup.addListener(() => void initialize());
+void initialize();
diff --git a/src/content/observer-standalone.js b/src/content/observer-standalone.js
new file mode 100644
index 0000000..0341b87
--- /dev/null
+++ b/src/content/observer-standalone.js
@@ -0,0 +1,81 @@
+/* QueueScope's self-contained MV3 observer. Keep behavior aligned with src/core/rules.ts. */
+(() => {
+ if (window.__queueScopeObserverInstalled) return;
+ window.__queueScopeObserverInstalled = true;
+ let rules;
+ let priorQueueLock = false;
+ let lastSignature = "";
+ let lastSentAt = 0;
+ let timer;
+
+ const firstMatch = (text, values) => values.find((value) => text.includes(value));
+ const safePattern = (value) => {
+ if (!value || value.length > 200 || /\([^)]*[+*][^)]*\)[+*{]/.test(value)) return undefined;
+ try { return new RegExp(value, "i"); } catch { return undefined; }
+ };
+ const parseDuration = (value) => {
+ const clock = value.match(/^(\d{1,3}):(\d{2}):(\d{2})$/);
+ if (clock) return Number(clock[1]) * 3600 + Number(clock[2]) * 60 + Number(clock[3]);
+ const minutes = value.match(/(\d+)\s*(?:m|min|mins|minute|minutes)\b/i);
+ if (minutes) return Number(minutes[1]) * 60;
+ const seconds = value.match(/(\d+)\s*(?:s|sec|secs|second|seconds)\b/i);
+ return seconds ? Number(seconds[1]) : undefined;
+ };
+ const available = () => {
+ if (!rules?.availableSelector || rules.availableSelector.length > 200 || /[\r\n{}]/.test(rules.availableSelector)) return false;
+ try {
+ const node = document.querySelector(rules.availableSelector);
+ return Boolean(node && !node.hasAttribute("disabled") && node.getAttribute("aria-disabled") !== "true");
+ } catch { return false; }
+ };
+ const classify = () => {
+ const observedAt = Date.now();
+ const text = (document.body?.innerText ?? "").slice(0, 100_000).toLowerCase();
+ const challenge = firstMatch(text, rules.challengeText);
+ const queue = firstMatch(text, rules.queueText);
+ const admitted = priorQueueLock ? firstMatch(text, rules.admittedText) : undefined;
+ const unavailable = firstMatch(text, rules.unavailableText);
+ let classification = "unknown";
+ let evidence = "No configured signal matched.";
+ let confidence = 0.25;
+ if (challenge) { classification = "challenge"; evidence = `Challenge text matched: ${challenge}`; confidence = 0.98; }
+ else if (queue) { classification = "queue"; evidence = `Queue text matched: ${queue}`; confidence = 0.98; }
+ else if (admitted) { classification = "admitted"; evidence = `Post-queue admission text matched: ${admitted}`; confidence = 0.97; }
+ else if (available()) { classification = "available"; evidence = `Availability selector matched: ${rules.availableSelector}`; confidence = 0.94; }
+ else if (unavailable) { classification = "unavailable"; evidence = `Unavailable text matched: ${unavailable}`; confidence = 0.9; }
+ else if (text) { classification = "product"; evidence = "Page loaded; no attention signal matched."; confidence = 0.55; }
+ const observation = { observedAt, url: location.href, title: document.title.slice(0, 180), classification, confidence, evidence: [evidence.slice(0, 240)] };
+ if (classification === "queue") {
+ const positionMatch = safePattern(rules.positionPattern)?.exec(text);
+ const position = positionMatch?.[1] ? Number(positionMatch[1].replaceAll(",", "")) : undefined;
+ if (Number.isFinite(position) && position >= 0 && position <= 1_000_000_000) observation.position = position;
+ const etaMatch = safePattern(rules.etaPattern)?.exec(text);
+ const duration = etaMatch?.[1] ? parseDuration(etaMatch[1]) : undefined;
+ if (duration !== undefined && duration >= 0 && duration <= 604_800) {
+ observation.providerEtaAt = observedAt + duration * 1000;
+ observation.providerEtaLabel = etaMatch[1].slice(0, 80);
+ }
+ }
+ return observation;
+ };
+ const load = async () => {
+ const response = await chrome.runtime.sendMessage({ type: "GET_TAB_CONFIG" });
+ if (response?.ok && response.data) { rules = response.data.rules; priorQueueLock = response.data.priorQueueLock; }
+ };
+ const scan = async () => {
+ if (!rules) await load();
+ if (!rules) return;
+ const observation = classify();
+ if (["queue", "challenge"].includes(observation.classification)) priorQueueLock = true;
+ const signature = JSON.stringify([observation.url, observation.classification, observation.position, observation.providerEtaAt ? Math.round(observation.providerEtaAt / 30_000) : undefined, observation.evidence[0]]);
+ if (signature !== lastSignature || Date.now() - lastSentAt >= 30_000) {
+ lastSignature = signature; lastSentAt = Date.now();
+ await chrome.runtime.sendMessage({ type: "OBSERVATION", observation });
+ }
+ };
+ const schedule = () => { if (timer !== undefined) clearTimeout(timer); timer = setTimeout(() => void scan(), 350); };
+ new MutationObserver(schedule).observe(document.documentElement, { subtree: true, childList: true, attributes: true, characterData: true });
+ addEventListener("pageshow", schedule); addEventListener("popstate", schedule);
+ void chrome.runtime.sendMessage({ type: "CONTENT_READY", url: location.href });
+ schedule();
+})();
diff --git a/src/core/defaults.ts b/src/core/defaults.ts
new file mode 100644
index 0000000..2d8e857
--- /dev/null
+++ b/src/core/defaults.ts
@@ -0,0 +1,68 @@
+import type { AppSettings, AppSnapshot, Watch, WatchRules } from "./types";
+
+export const MAX_EVENT_HISTORY = 200;
+export const PASSIVE_STOP_AT = Date.UTC(9999, 11, 31, 23, 59, 59);
+
+export const DEFAULT_RULES: WatchRules = {
+ queueText: ["you are now in the waiting room"],
+ admittedText: ["access granted. your session is ready"],
+ challengeText: ["please verify you are human"],
+ unavailableText: ["not available yet"],
+ availableSelector: "[data-queuescope-available='true']",
+ positionPattern: "position\\s+(\\d+)",
+ etaPattern: "estimated wait\\s+(\\d{2}:\\d{2}:\\d{2})"
+};
+
+export const DEFAULT_SETTINGS: AppSettings = {
+ notificationsEnabled: true,
+ soundEnabled: true,
+ tone: "signal",
+ focusOnAdmission: true,
+ retentionDays: 30,
+ motion: "full",
+ density: "comfortable",
+ expandRunsByDefault: false,
+ queueEtaAlertMinutes: 10
+};
+
+function timezone(): string {
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
+}
+
+export function createDefaultWatch(now = Date.now()): Watch {
+ const startAt = now + 5 * 60_000;
+ return {
+ id: crypto.randomUUID(),
+ name: "New page watch",
+ url: "http://127.0.0.1:4175/harness/",
+ profile: "scheduled",
+ mode: "guarded-refresh",
+ schedule: {
+ type: "once",
+ enabled: false,
+ timezone: timezone(),
+ weekdays: [],
+ startAt,
+ expectedAt: startAt + 5 * 60_000,
+ stopAt: startAt + 60 * 60_000
+ },
+ cadenceSeconds: 60,
+ activeCadenceSeconds: 15,
+ fastWindowBeforeMinutes: 10,
+ fastWindowAfterMinutes: 10,
+ jitterPercent: 5,
+ rules: structuredClone(DEFAULT_RULES),
+ createdAt: now,
+ updatedAt: now
+ };
+}
+
+export function createDefaultSnapshot(): AppSnapshot {
+ return {
+ version: 1,
+ watches: [],
+ activeRuns: [],
+ history: [],
+ settings: { ...DEFAULT_SETTINGS }
+ };
+}
diff --git a/src/core/rules.ts b/src/core/rules.ts
new file mode 100644
index 0000000..a7dbce3
--- /dev/null
+++ b/src/core/rules.ts
@@ -0,0 +1,104 @@
+import type { Observation, WatchRules } from "./types";
+
+const MAX_TEXT = 100_000;
+const MAX_PATTERN = 200;
+
+export function normalizeTextRules(values: unknown): string[] {
+ if (!Array.isArray(values)) return [];
+ return [...new Set(values.filter((value): value is string => typeof value === "string").map((value) => value.trim().toLowerCase()).filter(Boolean))]
+ .slice(0, 20)
+ .map((value) => value.slice(0, 120));
+}
+
+export function isSafeSelector(value?: string): boolean {
+ if (!value) return true;
+ if (value.length > MAX_PATTERN || /[\r\n{}]/.test(value)) return false;
+ try {
+ if (typeof document !== "undefined") document.createDocumentFragment().querySelector(value);
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+export function compileSafePattern(value?: string): RegExp | undefined {
+ if (!value || value.length > MAX_PATTERN || /\([^)]*[+*][^)]*\)[+*{]/.test(value)) return undefined;
+ try {
+ return new RegExp(value, "i");
+ } catch {
+ return undefined;
+ }
+}
+
+function firstTextMatch(text: string, values: string[]): string | undefined {
+ return values.find((value) => text.includes(value));
+}
+
+export function parseDuration(value: string): number | undefined {
+ const clock = value.match(/^(\d{1,3}):(\d{2}):(\d{2})$/);
+ if (clock) return Number(clock[1]) * 3600 + Number(clock[2]) * 60 + Number(clock[3]);
+ const minutes = value.match(/(\d+)\s*(?:m|min|mins|minute|minutes)\b/i);
+ if (minutes) return Number(minutes[1]) * 60;
+ const seconds = value.match(/(\d+)\s*(?:s|sec|secs|second|seconds)\b/i);
+ return seconds ? Number(seconds[1]) : undefined;
+}
+
+export interface RuleInput {
+ url: string;
+ title: string;
+ text: string;
+ availableSelectorMatched: boolean;
+ priorQueueLock: boolean;
+ observedAt?: number;
+}
+
+export function classifyWithRules(rules: WatchRules, input: RuleInput): Observation {
+ const observedAt = input.observedAt ?? Date.now();
+ const text = input.text.slice(0, MAX_TEXT).toLowerCase();
+ const challenge = firstTextMatch(text, rules.challengeText);
+ const queue = firstTextMatch(text, rules.queueText);
+ const admitted = input.priorQueueLock ? firstTextMatch(text, rules.admittedText) : undefined;
+ const unavailable = firstTextMatch(text, rules.unavailableText);
+ let classification: Observation["classification"] = "unknown";
+ let evidence = "No configured signal matched.";
+ let confidence = 0.25;
+ if (challenge) {
+ classification = "challenge";
+ evidence = `Challenge text matched: ${challenge}`;
+ confidence = 0.98;
+ } else if (queue) {
+ classification = "queue";
+ evidence = `Queue text matched: ${queue}`;
+ confidence = 0.98;
+ } else if (admitted) {
+ classification = "admitted";
+ evidence = `Post-queue admission text matched: ${admitted}`;
+ confidence = 0.97;
+ } else if (input.availableSelectorMatched) {
+ classification = "available";
+ evidence = `Availability selector matched: ${rules.availableSelector}`;
+ confidence = 0.94;
+ } else if (unavailable) {
+ classification = "unavailable";
+ evidence = `Unavailable text matched: ${unavailable}`;
+ confidence = 0.9;
+ } else if (text.length > 0) {
+ classification = "product";
+ evidence = "Page loaded; no attention signal matched.";
+ confidence = 0.55;
+ }
+
+ const observation: Observation = { observedAt, url: input.url, title: input.title.slice(0, 180), classification, confidence, evidence: [evidence.slice(0, 240)] };
+ if (classification === "queue") {
+ const positionMatch = compileSafePattern(rules.positionPattern)?.exec(text);
+ const position = positionMatch?.[1] ? Number(positionMatch[1].replaceAll(",", "")) : undefined;
+ if (Number.isFinite(position) && position !== undefined && position >= 0 && position <= 1_000_000_000) observation.position = position;
+ const etaMatch = compileSafePattern(rules.etaPattern)?.exec(text);
+ const duration = etaMatch?.[1] ? parseDuration(etaMatch[1]) : undefined;
+ if (duration !== undefined && duration >= 0 && duration <= 7 * 24 * 3600) {
+ observation.providerEtaAt = observedAt + duration * 1000;
+ observation.providerEtaLabel = etaMatch?.[1].slice(0, 80);
+ }
+ }
+ return observation;
+}
diff --git a/src/core/schedule.ts b/src/core/schedule.ts
new file mode 100644
index 0000000..961f26c
--- /dev/null
+++ b/src/core/schedule.ts
@@ -0,0 +1,153 @@
+import { PASSIVE_STOP_AT } from "./defaults";
+import type { ScheduleType, Watch, Weekday } from "./types";
+
+export interface RunWindow {
+ occurrenceKey: string;
+ startAt: number;
+ stopAt: number;
+ expectedAt?: number;
+}
+
+export const WEEKDAY_LABELS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] as const;
+export const ALL_DAYS: Weekday[] = [0, 1, 2, 3, 4, 5, 6];
+export const WEEKDAYS: Weekday[] = [1, 2, 3, 4, 5];
+export const WEEKENDS: Weekday[] = [0, 6];
+
+const timePattern = /^([01]\d|2[0-3]):([0-5]\d)$/;
+const datePattern = /^\d{4}-\d{2}-\d{2}$/;
+const formatterCache = new Map();
+
+function formatter(timezone: string): Intl.DateTimeFormat {
+ const cached = formatterCache.get(timezone);
+ if (cached) return cached;
+ const value = new Intl.DateTimeFormat("en-US", {
+ timeZone: timezone,
+ year: "numeric",
+ month: "2-digit",
+ day: "2-digit",
+ hour: "2-digit",
+ minute: "2-digit",
+ second: "2-digit",
+ hourCycle: "h23"
+ });
+ formatterCache.set(timezone, value);
+ return value;
+}
+
+function zonedParts(timestamp: number, timezone: string) {
+ const values: Record = {};
+ for (const part of formatter(timezone).formatToParts(new Date(timestamp))) {
+ if (part.type !== "literal") values[part.type] = Number(part.value);
+ }
+ return values;
+}
+
+export function isTime(value?: string): value is string {
+ return Boolean(value && timePattern.test(value));
+}
+
+export function isDate(value?: string): value is string {
+ return Boolean(value && datePattern.test(value));
+}
+
+export function isTimezone(value: string): boolean {
+ try {
+ formatter(value);
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+export function zonedDateTimeToEpoch(date: string, time: string, timezone: string): number {
+ if (!isDate(date) || !isTime(time) || !isTimezone(timezone)) {
+ throw new Error("Invalid zoned date and time.");
+ }
+ const [year, month, day] = date.split("-").map(Number);
+ const [hour, minute] = time.split(":").map(Number);
+ const target = Date.UTC(year, month - 1, day, hour, minute, 0);
+ let guess = target;
+ for (let attempt = 0; attempt < 4; attempt += 1) {
+ const parts = zonedParts(guess, timezone);
+ const represented = Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second);
+ const difference = target - represented;
+ guess += difference;
+ if (Math.abs(difference) < 1000) break;
+ }
+ return guess;
+}
+
+function dateString(timestamp: number, timezone: string): string {
+ const parts = zonedParts(timestamp, timezone);
+ return `${parts.year.toString().padStart(4, "0")}-${parts.month.toString().padStart(2, "0")}-${parts.day.toString().padStart(2, "0")}`;
+}
+
+function addDays(date: string, days: number): string {
+ const [year, month, day] = date.split("-").map(Number);
+ return new Date(Date.UTC(year, month - 1, day + days)).toISOString().slice(0, 10);
+}
+
+function daysForType(type: ScheduleType, custom: Weekday[]): Weekday[] {
+ if (type === "daily") return ALL_DAYS;
+ if (type === "weekdays") return WEEKDAYS;
+ if (type === "weekends") return WEEKENDS;
+ if (type === "custom") return [...new Set(custom)].sort() as Weekday[];
+ return [];
+}
+
+function occurrenceForDate(watch: Watch, date: string): RunWindow | undefined {
+ const schedule = watch.schedule;
+ if (schedule.type === "once") {
+ if (schedule.startAt === undefined || schedule.stopAt === undefined) return undefined;
+ return { occurrenceKey: `${watch.id}:${schedule.startAt}`, startAt: schedule.startAt, stopAt: schedule.stopAt, expectedAt: schedule.expectedAt };
+ }
+ if (!isDate(date) || !isTime(schedule.startTime) || !isTime(schedule.stopTime) || !isTimezone(schedule.timezone)) return undefined;
+ if (!daysForType(schedule.type, schedule.weekdays).includes(new Date(`${date}T00:00:00Z`).getUTCDay() as Weekday)) return undefined;
+ if (schedule.startsOn && date < schedule.startsOn) return undefined;
+ if (schedule.endsOn && date > schedule.endsOn) return undefined;
+ const startMinutes = Number(schedule.startTime.slice(0, 2)) * 60 + Number(schedule.startTime.slice(3));
+ const stopMinutes = Number(schedule.stopTime.slice(0, 2)) * 60 + Number(schedule.stopTime.slice(3));
+ const crossesMidnight = stopMinutes <= startMinutes;
+ const stopDate = crossesMidnight ? addDays(date, 1) : date;
+ const startAt = zonedDateTimeToEpoch(date, schedule.startTime, schedule.timezone);
+ const stopAt = zonedDateTimeToEpoch(stopDate, schedule.stopTime, schedule.timezone);
+ let expectedAt: number | undefined;
+ if (isTime(schedule.expectedTime)) {
+ const expectedMinutes = Number(schedule.expectedTime.slice(0, 2)) * 60 + Number(schedule.expectedTime.slice(3));
+ expectedAt = zonedDateTimeToEpoch(crossesMidnight && expectedMinutes < startMinutes ? stopDate : date, schedule.expectedTime, schedule.timezone);
+ }
+ return { occurrenceKey: `${watch.id}:${startAt}`, startAt, stopAt, expectedAt };
+}
+
+export function currentOrNextOccurrence(watch: Watch, now = Date.now()): RunWindow | undefined {
+ if (watch.profile === "passive") {
+ return { occurrenceKey: `${watch.id}:passive`, startAt: now, stopAt: PASSIVE_STOP_AT };
+ }
+ if (watch.schedule.type === "once") {
+ const value = occurrenceForDate(watch, "");
+ return value && value.stopAt > now ? value : undefined;
+ }
+ const today = dateString(now, watch.schedule.timezone);
+ const candidates: RunWindow[] = [];
+ for (let offset = -1; offset <= 14; offset += 1) {
+ const value = occurrenceForDate(watch, addDays(today, offset));
+ if (value && value.stopAt > now) candidates.push(value);
+ }
+ return candidates.sort((a, b) => a.startAt - b.startAt)[0];
+}
+
+export function activeOccurrence(watch: Watch, now = Date.now()): RunWindow | undefined {
+ const value = currentOrNextOccurrence(watch, now);
+ return value && value.startAt <= now && now < value.stopAt ? value : undefined;
+}
+
+export function isOlderWatch(watch: Watch, now = Date.now()): boolean {
+ return watch.profile === "scheduled" && currentOrNextOccurrence(watch, now) === undefined;
+}
+
+export function describeSchedule(watch: Watch, now = Date.now()): string {
+ if (watch.profile === "passive") return `${watch.schedule.enabled ? "Armed" : "Paused"} · every ${watch.cadenceSeconds < 60 ? `${watch.cadenceSeconds}s` : `${Math.round(watch.cadenceSeconds / 60)}m`} · until stopped`;
+ const value = currentOrNextOccurrence(watch, now);
+ if (!value) return "No future occurrence";
+ return new Intl.DateTimeFormat("en-US", { timeZone: watch.schedule.timezone, month: "short", day: "numeric", hour: "numeric", minute: "2-digit", timeZoneName: "short" }).format(new Date(value.startAt));
+}
diff --git a/src/core/state-machine.ts b/src/core/state-machine.ts
new file mode 100644
index 0000000..f39bc53
--- /dev/null
+++ b/src/core/state-machine.ts
@@ -0,0 +1,125 @@
+import { MAX_EVENT_HISTORY } from "./defaults";
+import type { RunWindow } from "./schedule";
+import type { Observation, Run, RunEvent, RunState, Watch } from "./types";
+
+function event(type: RunEvent["type"], label: string, at: number, detail?: string): RunEvent {
+ return { id: crypto.randomUUID(), type, label, at, detail };
+}
+
+function append(run: Run, item: RunEvent): RunEvent[] {
+ return [...run.events, item].slice(-MAX_EVENT_HISTORY);
+}
+
+function state(run: Run, next: RunState, now: number): Run {
+ return run.state === next ? run : { ...run, state: next, stateChangedAt: now };
+}
+
+export function cadenceFor(run: Pick, now: number): number {
+ if (!run.expectedAt) return run.cadenceSeconds;
+ const begins = run.expectedAt - run.fastWindowBeforeMinutes * 60_000;
+ const ends = run.expectedAt + run.fastWindowAfterMinutes * 60_000;
+ return now >= begins && now <= ends ? run.activeCadenceSeconds : run.cadenceSeconds;
+}
+
+export function nextCheckAt(run: Pick, now: number, random = Math.random): number {
+ const base = cadenceFor(run, now) * 1000;
+ const jitter = Math.min(Math.max(run.jitterPercent, 0), 50) / 100;
+ return now + Math.max(5_000, Math.round(base * (1 + (random() * 2 - 1) * jitter)));
+}
+
+export function createRun(watch: Watch, window: RunWindow, now = Date.now(), attached = false): Run {
+ const active = attached || now >= window.startAt;
+ return {
+ id: crypto.randomUUID(),
+ watchId: attached ? undefined : watch.id,
+ watchName: watch.name,
+ occurrenceKey: attached ? `attached:${now}` : window.occurrenceKey,
+ url: watch.url,
+ profile: attached ? "passive" : watch.profile,
+ mode: attached ? "observe-only" : watch.mode,
+ state: active ? "ACTIVE_CHECK" : "ARMED",
+ stateChangedAt: now,
+ createdAt: now,
+ startAt: attached ? now : window.startAt,
+ stopAt: attached ? now + 12 * 60 * 60_000 : window.stopAt,
+ expectedAt: window.expectedAt,
+ nextCheckAt: active ? now : window.startAt,
+ checkCount: 0,
+ cadenceSeconds: watch.cadenceSeconds,
+ activeCadenceSeconds: watch.activeCadenceSeconds,
+ fastWindowBeforeMinutes: watch.fastWindowBeforeMinutes,
+ fastWindowAfterMinutes: watch.fastWindowAfterMinutes,
+ jitterPercent: watch.jitterPercent,
+ queueLocked: false,
+ attached,
+ rules: structuredClone(watch.rules),
+ events: [event("armed", attached ? "Attached without refreshing" : active ? "Run armed and checking window open" : "Run armed", now)]
+ };
+}
+
+export function advanceClock(run: Run, now = Date.now()): Run {
+ if (["COMPLETE", "EXPIRED", "FAILED"].includes(run.state)) return run;
+ if (now >= run.stopAt && !run.queueLocked && run.state !== "ADMITTED") {
+ const expired = state(run, "EXPIRED", now);
+ return { ...expired, endedAt: now, events: append(expired, event("ended", "Monitoring window expired", now)) };
+ }
+ if (run.state === "ARMED" && now >= run.startAt) {
+ const active = state(run, "ACTIVE_CHECK", now);
+ return { ...active, nextCheckAt: now, events: append(active, event("started", "Monitoring window opened", now)) };
+ }
+ return run;
+}
+
+export function applyObservation(run: Run, observation: Observation): Run {
+ if (["COMPLETE", "EXPIRED", "FAILED"].includes(run.state)) return run;
+ const now = observation.observedAt;
+ const lock = observation.classification === "queue" || observation.classification === "challenge";
+ let next: RunState = "ACTIVE_CHECK";
+ if (observation.classification === "queue") next = "WAITING";
+ if (["challenge", "available"].includes(observation.classification)) next = "ATTENTION";
+ if (observation.classification === "admitted" && run.queueLocked) next = "ADMITTED";
+ if (run.queueLocked && !["admitted", "challenge"].includes(observation.classification)) next = run.state === "ADMITTED" ? "ADMITTED" : "WAITING";
+ let updated = state(run, next, now);
+ let events = updated.events;
+ if (run.lastObservation?.classification !== observation.classification) {
+ events = [...events, event("observation", `Page classified as ${observation.classification}`, now, observation.evidence[0])];
+ }
+ if (lock && !run.queueLocked) events = [...events, event("queue-lock", "Safety lock engaged", now, `${observation.classification}: refresh inhibited`)];
+ if (observation.classification === "available") events = [...events, event("attention", "Configured availability signal detected", now, observation.evidence[0])];
+ if (observation.classification === "admitted" && run.queueLocked) events = [...events, event("admitted", "Post-queue admission detected", now)];
+ if (observation.classification === "challenge") events = [...events, event("attention", "Manual attention required", now, observation.evidence[0])];
+ updated = {
+ ...updated,
+ queueLocked: run.queueLocked || lock,
+ queueLockReason: run.queueLockReason ?? (lock ? `${observation.classification} detected at ${new Date(now).toLocaleTimeString()}` : undefined),
+ lastObservation: observation,
+ events: events.slice(-MAX_EVENT_HISTORY)
+ };
+ return updated;
+}
+
+export function recordCheck(run: Run, now = Date.now(), random = Math.random): Run {
+ const checkCount = run.checkCount + 1;
+ return {
+ ...run,
+ checkCount,
+ nextCheckAt: nextCheckAt(run, now, random),
+ events: append(run, event("check", run.mode === "observe-only" ? `Observation heartbeat ${checkCount}` : `Guarded refresh ${checkCount} dispatched`, now))
+ };
+}
+
+export function endRun(run: Run, outcome: Run["outcome"] = "unknown", now = Date.now()): Run {
+ const complete = state(run, "COMPLETE", now);
+ return { ...complete, endedAt: now, outcome, events: append(complete, event("ended", "Run ended by user", now, `Outcome: ${outcome}`)) };
+}
+
+export function refreshDecision(run: Run, now = Date.now()): { action: "wait" | "refresh" | "observe" | "stop" | "locked"; reason: string } {
+ if (run.queueLocked) return { action: "locked", reason: "A safety lock is active." };
+ if (now < run.startAt) return { action: "wait", reason: "The monitoring window has not opened." };
+ if (now >= run.stopAt) return { action: "stop", reason: "The monitoring window has ended." };
+ if (run.state !== "ACTIVE_CHECK") return { action: "wait", reason: `State ${run.state} does not permit refresh.` };
+ if (["available", "admitted"].includes(run.lastObservation?.classification ?? "")) return { action: "wait", reason: "An attention state is already present." };
+ if ((run.nextCheckAt ?? run.startAt) > now) return { action: "wait", reason: "The next check is not due." };
+ if (run.mode === "observe-only" || run.attached) return { action: "observe", reason: "Observation heartbeat is due; navigation remains disabled." };
+ return { action: "refresh", reason: "A guarded refresh is due." };
+}
diff --git a/src/core/storage.ts b/src/core/storage.ts
new file mode 100644
index 0000000..d52773a
--- /dev/null
+++ b/src/core/storage.ts
@@ -0,0 +1,24 @@
+import { createDefaultSnapshot } from "./defaults";
+import { normalizeSettings, normalizeWatch } from "./validation";
+import type { AppSnapshot } from "./types";
+
+const STORAGE_KEY = "queuescope.snapshot.v1";
+
+export async function loadSnapshot(): Promise {
+ const stored = await chrome.storage.local.get(STORAGE_KEY);
+ const raw = stored[STORAGE_KEY] as Partial | undefined;
+ if (!raw || raw.version !== 1) return createDefaultSnapshot();
+ return {
+ version: 1,
+ watches: Array.isArray(raw.watches) ? raw.watches.flatMap((watch) => {
+ try { return [normalizeWatch(watch)]; } catch { return []; }
+ }) : [],
+ activeRuns: Array.isArray(raw.activeRuns) ? raw.activeRuns : [],
+ history: Array.isArray(raw.history) ? raw.history : [],
+ settings: normalizeSettings(raw.settings)
+ };
+}
+
+export async function saveSnapshot(snapshot: AppSnapshot): Promise {
+ await chrome.storage.local.set({ [STORAGE_KEY]: snapshot });
+}
diff --git a/src/core/types.ts b/src/core/types.ts
new file mode 100644
index 0000000..0a5b705
--- /dev/null
+++ b/src/core/types.ts
@@ -0,0 +1,172 @@
+export type OperationMode = "guarded-refresh" | "observe-only";
+export type WatchProfile = "scheduled" | "passive";
+export type ScheduleType = "once" | "daily" | "weekdays" | "weekends" | "custom";
+export type Weekday = 0 | 1 | 2 | 3 | 4 | 5 | 6;
+
+export interface WatchSchedule {
+ type: ScheduleType;
+ enabled: boolean;
+ timezone: string;
+ weekdays: Weekday[];
+ startAt?: number;
+ stopAt?: number;
+ expectedAt?: number;
+ startTime?: string;
+ stopTime?: string;
+ expectedTime?: string;
+ startsOn?: string;
+ endsOn?: string;
+}
+
+export interface WatchRules {
+ queueText: string[];
+ admittedText: string[];
+ challengeText: string[];
+ unavailableText: string[];
+ availableSelector?: string;
+ positionPattern?: string;
+ etaPattern?: string;
+}
+
+export interface Watch {
+ id: string;
+ name: string;
+ url: string;
+ profile: WatchProfile;
+ mode: OperationMode;
+ schedule: WatchSchedule;
+ cadenceSeconds: number;
+ activeCadenceSeconds: number;
+ fastWindowBeforeMinutes: number;
+ fastWindowAfterMinutes: number;
+ jitterPercent: number;
+ rules: WatchRules;
+ createdAt: number;
+ updatedAt: number;
+}
+
+export type PageClassification =
+ | "product"
+ | "available"
+ | "unavailable"
+ | "queue"
+ | "challenge"
+ | "admitted"
+ | "unknown";
+
+export interface Observation {
+ observedAt: number;
+ url: string;
+ title: string;
+ classification: PageClassification;
+ confidence: number;
+ evidence: string[];
+ position?: number;
+ providerEtaAt?: number;
+ providerEtaLabel?: string;
+}
+
+export type RunState =
+ | "ARMED"
+ | "ACTIVE_CHECK"
+ | "WAITING"
+ | "ATTENTION"
+ | "ADMITTED"
+ | "COMPLETE"
+ | "EXPIRED"
+ | "FAILED";
+
+export interface RunEvent {
+ id: string;
+ at: number;
+ type: "armed" | "started" | "check" | "observation" | "queue-lock" | "attention" | "admitted" | "ended" | "error";
+ label: string;
+ detail?: string;
+}
+
+export interface Run {
+ id: string;
+ watchId?: string;
+ watchName: string;
+ occurrenceKey: string;
+ url: string;
+ profile: WatchProfile;
+ mode: OperationMode;
+ state: RunState;
+ stateChangedAt: number;
+ createdAt: number;
+ startAt: number;
+ stopAt: number;
+ expectedAt?: number;
+ nextCheckAt?: number;
+ checkCount: number;
+ cadenceSeconds: number;
+ activeCadenceSeconds: number;
+ fastWindowBeforeMinutes: number;
+ fastWindowAfterMinutes: number;
+ jitterPercent: number;
+ queueLocked: boolean;
+ queueLockReason?: string;
+ tabId?: number;
+ attached: boolean;
+ rules: WatchRules;
+ lastObservation?: Observation;
+ events: RunEvent[];
+ endedAt?: number;
+ outcome?: "admitted" | "completed" | "missed" | "abandoned" | "unknown";
+ error?: string;
+}
+
+export type ToneId = "signal" | "soft" | "urgent";
+
+export interface AppSettings {
+ notificationsEnabled: boolean;
+ soundEnabled: boolean;
+ tone: ToneId;
+ focusOnAdmission: boolean;
+ retentionDays: number;
+ motion: "full" | "reduced";
+ density: "comfortable" | "compact";
+ expandRunsByDefault: boolean;
+ queueEtaAlertMinutes: number;
+}
+
+export interface AppSnapshot {
+ version: 1;
+ watches: Watch[];
+ activeRuns: Run[];
+ history: Run[];
+ settings: AppSettings;
+}
+
+export interface AttachableTab {
+ id: number;
+ title: string;
+ url: string;
+ active: boolean;
+ attached: boolean;
+}
+
+export type RuntimeRequest =
+ | { type: "GET_SNAPSHOT" }
+ | { type: "SAVE_WATCH"; watch: Watch }
+ | { type: "DELETE_WATCH"; watchId: string }
+ | { type: "ARM_WATCH"; watchId: string }
+ | { type: "SAVE_SETTINGS"; settings: AppSettings }
+ | { type: "LIST_ATTACHABLE_TABS" }
+ | { type: "ATTACH_TAB"; tabId: number; rules?: WatchRules }
+ | { type: "END_RUN"; runId: string; outcome?: Run["outcome"] }
+ | { type: "FOCUS_RUN_TAB"; runId: string }
+ | { type: "OPEN_DASHBOARD" }
+ | { type: "OPEN_QUEUE_LAB" }
+ | { type: "PREVIEW_TONE"; tone: ToneId }
+ | { type: "CONTENT_READY"; url: string }
+ | { type: "GET_TAB_CONFIG" }
+ | { type: "OBSERVATION"; observation: Observation }
+ | { type: "LAB_OBSERVATION"; observation: Observation };
+
+export interface RuntimeResponse {
+ ok: boolean;
+ data?: T;
+ error?: string;
+}
diff --git a/src/core/validation.ts b/src/core/validation.ts
new file mode 100644
index 0000000..ba33cbe
--- /dev/null
+++ b/src/core/validation.ts
@@ -0,0 +1,110 @@
+import { DEFAULT_RULES, DEFAULT_SETTINGS, PASSIVE_STOP_AT } from "./defaults";
+import { compileSafePattern, isSafeSelector, normalizeTextRules } from "./rules";
+import { isTimezone } from "./schedule";
+import type { AppSettings, RuntimeRequest, Watch, WatchRules } from "./types";
+
+const trackingParams = [/^utm_/i, /^ref$/i, /^referrer$/i, /^source$/i, /^campaign$/i, /^fbclid$/i, /^gclid$/i];
+
+export function canonicalizeUrl(value: string): string {
+ const url = new URL(value);
+ if (!["http:", "https:"].includes(url.protocol)) throw new Error("Only HTTP and HTTPS pages can be watched.");
+ url.username = "";
+ url.password = "";
+ url.hash = "";
+ for (const key of [...url.searchParams.keys()]) {
+ if (trackingParams.some((pattern) => pattern.test(key))) url.searchParams.delete(key);
+ }
+ return url.toString();
+}
+
+function number(value: unknown, fallback: number, min: number, max: number): number {
+ return typeof value === "number" && Number.isFinite(value) ? Math.min(Math.max(value, min), max) : fallback;
+}
+
+export function normalizeRules(value: Partial | undefined): WatchRules {
+ const availableSelector = typeof value?.availableSelector === "string" ? value.availableSelector.trim().slice(0, 200) : DEFAULT_RULES.availableSelector;
+ if (!isSafeSelector(availableSelector)) throw new Error("Availability selector is invalid or too complex.");
+ const positionPattern = typeof value?.positionPattern === "string" ? value.positionPattern.trim().slice(0, 200) : DEFAULT_RULES.positionPattern;
+ const etaPattern = typeof value?.etaPattern === "string" ? value.etaPattern.trim().slice(0, 200) : DEFAULT_RULES.etaPattern;
+ if (positionPattern && !compileSafePattern(positionPattern)) throw new Error("Position pattern is invalid or potentially unsafe.");
+ if (etaPattern && !compileSafePattern(etaPattern)) throw new Error("ETA pattern is invalid or potentially unsafe.");
+ return {
+ queueText: normalizeTextRules(value?.queueText ?? DEFAULT_RULES.queueText),
+ admittedText: normalizeTextRules(value?.admittedText ?? DEFAULT_RULES.admittedText),
+ challengeText: normalizeTextRules(value?.challengeText ?? DEFAULT_RULES.challengeText),
+ unavailableText: normalizeTextRules(value?.unavailableText ?? DEFAULT_RULES.unavailableText),
+ availableSelector: availableSelector || undefined,
+ positionPattern: positionPattern || undefined,
+ etaPattern: etaPattern || undefined
+ };
+}
+
+export function normalizeWatch(input: Watch): Watch {
+ const now = Date.now();
+ const profile = input.profile === "passive" ? "passive" : "scheduled";
+ const timezone = isTimezone(input.schedule?.timezone) ? input.schedule.timezone : Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
+ const watch: Watch = {
+ id: typeof input.id === "string" && input.id ? input.id : crypto.randomUUID(),
+ name: typeof input.name === "string" && input.name.trim() ? input.name.trim().slice(0, 100) : "Untitled watch",
+ url: canonicalizeUrl(input.url),
+ profile,
+ mode: input.mode === "observe-only" ? "observe-only" : "guarded-refresh",
+ schedule: {
+ type: ["once", "daily", "weekdays", "weekends", "custom"].includes(input.schedule?.type) ? input.schedule.type : "once",
+ enabled: input.schedule?.enabled === true,
+ timezone,
+ weekdays: Array.isArray(input.schedule?.weekdays) ? [...new Set(input.schedule.weekdays.filter((day) => Number.isInteger(day) && day >= 0 && day <= 6))] as Watch["schedule"]["weekdays"] : [],
+ startAt: profile === "passive" ? now : input.schedule?.startAt,
+ stopAt: profile === "passive" ? PASSIVE_STOP_AT : input.schedule?.stopAt,
+ expectedAt: input.schedule?.expectedAt,
+ startTime: input.schedule?.startTime,
+ stopTime: input.schedule?.stopTime,
+ expectedTime: input.schedule?.expectedTime,
+ startsOn: input.schedule?.startsOn,
+ endsOn: input.schedule?.endsOn
+ },
+ cadenceSeconds: number(input.cadenceSeconds, 60, 5, 86_400),
+ activeCadenceSeconds: number(input.activeCadenceSeconds, 15, 5, 3_600),
+ fastWindowBeforeMinutes: number(input.fastWindowBeforeMinutes, 10, 0, 1_440),
+ fastWindowAfterMinutes: number(input.fastWindowAfterMinutes, 10, 0, 1_440),
+ jitterPercent: number(input.jitterPercent, 5, 0, 50),
+ rules: normalizeRules(input.rules),
+ createdAt: number(input.createdAt, now, 0, now),
+ updatedAt: now
+ };
+ if (profile === "scheduled" && watch.schedule.type === "once") {
+ if (!Number.isFinite(watch.schedule.startAt) || !Number.isFinite(watch.schedule.stopAt)) throw new Error("One-time watches need start and stop times.");
+ if ((watch.schedule.stopAt ?? 0) <= (watch.schedule.startAt ?? 0)) throw new Error("Stop time must be after start time.");
+ if (watch.schedule.expectedAt && (watch.schedule.expectedAt < (watch.schedule.startAt ?? 0) || watch.schedule.expectedAt > (watch.schedule.stopAt ?? 0))) throw new Error("Expected time must be inside the monitoring window.");
+ }
+ return watch;
+}
+
+export function normalizeSettings(input: Partial | undefined): AppSettings {
+ return {
+ notificationsEnabled: input?.notificationsEnabled !== false,
+ soundEnabled: input?.soundEnabled !== false,
+ tone: ["signal", "soft", "urgent"].includes(input?.tone ?? "") ? input!.tone! : DEFAULT_SETTINGS.tone,
+ focusOnAdmission: input?.focusOnAdmission !== false,
+ retentionDays: number(input?.retentionDays, DEFAULT_SETTINGS.retentionDays, 1, 365),
+ motion: input?.motion === "reduced" ? "reduced" : "full",
+ density: input?.density === "compact" ? "compact" : "comfortable",
+ expandRunsByDefault: input?.expandRunsByDefault === true,
+ queueEtaAlertMinutes: number(input?.queueEtaAlertMinutes, DEFAULT_SETTINGS.queueEtaAlertMinutes, 1, 60)
+ };
+}
+
+export function isRuntimeRequest(value: unknown): value is RuntimeRequest {
+ if (!value || typeof value !== "object" || !("type" in value)) return false;
+ const request = value as Record;
+ if (["GET_SNAPSHOT", "LIST_ATTACHABLE_TABS", "OPEN_DASHBOARD", "OPEN_QUEUE_LAB", "GET_TAB_CONFIG"].includes(String(request.type))) return true;
+ if (["DELETE_WATCH", "ARM_WATCH"].includes(String(request.type))) return typeof request.watchId === "string";
+ if (["END_RUN", "FOCUS_RUN_TAB"].includes(String(request.type))) return typeof request.runId === "string";
+ if (request.type === "ATTACH_TAB") return typeof request.tabId === "number" && request.tabId >= 0;
+ if (request.type === "SAVE_WATCH") return Boolean(request.watch && typeof request.watch === "object");
+ if (request.type === "SAVE_SETTINGS") return Boolean(request.settings && typeof request.settings === "object");
+ if (request.type === "PREVIEW_TONE") return ["signal", "soft", "urgent"].includes(String(request.tone));
+ if (request.type === "CONTENT_READY") return typeof request.url === "string";
+ if (["OBSERVATION", "LAB_OBSERVATION"].includes(String(request.type))) return Boolean(request.observation && typeof request.observation === "object");
+ return false;
+}
diff --git a/src/ui/App.tsx b/src/ui/App.tsx
new file mode 100644
index 0000000..8a6b15d
--- /dev/null
+++ b/src/ui/App.tsx
@@ -0,0 +1,191 @@
+import {
+ Activity, Bell, BookOpen, ChevronDown, Clock3,
+ FlaskConical, Gauge, History, LayoutDashboard, Menu, PanelRightOpen, Plus,
+ Radar, Settings2, ShieldCheck, SlidersHorizontal, Sparkles, Volume2, X
+} from "lucide-react";
+import { useCallback, useEffect, useMemo, useState } from "react";
+import { createDefaultWatch } from "../core/defaults";
+import { isOlderWatch } from "../core/schedule";
+import type { AppSettings, AppSnapshot, AttachableTab, RuntimeRequest, Run, ToneId, Watch } from "../core/types";
+import { EmptyState, AttentionBanner, RunCard, WatchCard } from "./components";
+import { isPanelSurface, onSnapshotChange, requestPageAccess, send } from "./platform";
+import { WatchEditor } from "./WatchEditor";
+
+type Page = "overview" | "runs" | "watches" | "lab" | "settings" | "activity";
+
+const nav: { id: Page; label: string; icon: typeof Activity }[] = [
+ { id: "overview", label: "Command center", icon: LayoutDashboard },
+ { id: "runs", label: "Live runs", icon: Activity },
+ { id: "watches", label: "Watches", icon: Radar },
+ { id: "lab", label: "Queue Lab", icon: FlaskConical },
+ { id: "activity", label: "Activity", icon: History },
+ { id: "settings", label: "Settings", icon: Settings2 }
+];
+
+const panelNav: typeof nav = [nav[1], nav[2], nav[4]];
+
+function readableError(reason: unknown) { return reason instanceof Error ? reason.message : "Something went wrong."; }
+
+export function App() {
+ const panel = isPanelSurface();
+ const [snapshot, setSnapshot] = useState();
+ const [page, setPage] = useState(panel ? "runs" : "overview");
+ const [editor, setEditor] = useState();
+ const [toast, setToast] = useState("");
+ const [busy, setBusy] = useState(false);
+ const [tabs, setTabs] = useState([]);
+ const [attachOpen, setAttachOpen] = useState(false);
+ const [olderOpen, setOlderOpen] = useState(false);
+ const [mobileNav, setMobileNav] = useState(false);
+
+ const refresh = useCallback(async () => {
+ try { setSnapshot(await send({ type: "GET_SNAPSHOT" })); } catch (reason) { setToast(readableError(reason)); }
+ }, []);
+
+ useEffect(() => {
+ const timer = window.setTimeout(() => void refresh(), 0);
+ const unsubscribe = onSnapshotChange(() => void refresh());
+ return () => { window.clearTimeout(timer); unsubscribe(); };
+ }, [refresh]);
+ useEffect(() => {
+ if (!snapshot) return;
+ document.documentElement.dataset.motion = snapshot.settings.motion;
+ document.documentElement.dataset.density = snapshot.settings.density;
+ }, [snapshot]);
+ useEffect(() => {
+ if (!toast) return;
+ const timer = window.setTimeout(() => setToast(""), 4000);
+ return () => window.clearTimeout(timer);
+ }, [toast]);
+
+ const activeIds = useMemo(() => new Set(snapshot?.activeRuns.map((run) => run.watchId).filter(Boolean)), [snapshot]);
+ const attention = snapshot?.activeRuns.filter((run) => ["ATTENTION", "ADMITTED"].includes(run.state)).length || 0;
+
+ async function act(request: RuntimeRequest, success?: string) {
+ setBusy(true);
+ try { await send(request); if (success) setToast(success); await refresh(); }
+ catch (reason) { setToast(readableError(reason)); }
+ finally { setBusy(false); }
+ }
+
+ async function arm(watch: Watch) {
+ const granted = await requestPageAccess(watch.url);
+ if (!granted) throw new Error("Page access was not granted.");
+ await send({ type: "ARM_WATCH", watchId: watch.id });
+ await refresh();
+ }
+
+ async function saveWatch(watch: Watch, armAfter: boolean) {
+ await send({ type: "SAVE_WATCH", watch });
+ if (armAfter) await arm(watch);
+ setEditor(undefined); setToast(armAfter ? "Watch saved and armed." : "Watch saved."); await refresh();
+ }
+
+ async function openAttach() {
+ setBusy(true);
+ try { setTabs(await send({ type: "LIST_ATTACHABLE_TABS" })); setAttachOpen(true); }
+ catch (reason) { setToast(readableError(reason)); }
+ finally { setBusy(false); }
+ }
+
+ async function attach(tab: AttachableTab) {
+ const granted = await requestPageAccess(tab.url);
+ if (!granted) throw new Error("Page access was not granted.");
+ await act({ type: "ATTACH_TAB", tabId: tab.id }, "Tab attached without refreshing.");
+ setAttachOpen(false);
+ }
+
+ function toggleAutoStart(watch: Watch) {
+ void act({ type: "SAVE_WATCH", watch: { ...watch, schedule: { ...watch.schedule, enabled: !watch.schedule.enabled } } }, `Auto-start ${watch.schedule.enabled ? "disabled" : "enabled"}.`);
+ }
+
+ function copyDirect(watch: Watch) {
+ const copy = structuredClone(watch);
+ copy.id = crypto.randomUUID();
+ copy.name = `${watch.name} — copy`;
+ copy.schedule.enabled = false;
+ copy.createdAt = Date.now();
+ void act({ type: "SAVE_WATCH", watch: copy }, "Paused copy saved.");
+ }
+
+ if (!snapshot) return
Establishing local scope…
;
+ if (editor && !panel) return setEditor(undefined)} onSave={saveWatch}/> ;
+
+ const currentNav = panel ? panelNav : nav;
+ return { setPage(next); setMobileNav(false); }} mobileNav={mobileNav} setMobileNav={setMobileNav}>
+ {panel ? void act({ type: "FOCUS_RUN_TAB", runId: run.id })} onEnd={(run) => void act({ type: "END_RUN", runId: run.id, outcome: "abandoned" }, "Run moved to history.")} onArm={(watch) => void arm(watch).catch((reason) => setToast(readableError(reason)))} onCopy={copyDirect} onAutoStart={toggleAutoStart} onDelete={(watch) => { if (window.confirm(`Delete “${watch.name}”?`)) void act({ type: "DELETE_WATCH", watchId: watch.id }, "Watch deleted."); }}/> : <>
+ {page === "overview" && setEditor(createDefaultWatch())} onAttach={() => void openAttach()} onLab={() => void act({ type: "OPEN_QUEUE_LAB" })} onNavigate={setPage} onFocus={(run) => void act({ type: "FOCUS_RUN_TAB", runId: run.id })} onEnd={(run) => void act({ type: "END_RUN", runId: run.id, outcome: "abandoned" }, "Run moved to history.")}/>}
+ {page === "runs" && void openAttach()} onFocus={(run) => void act({ type: "FOCUS_RUN_TAB", runId: run.id })} onEnd={(run) => void act({ type: "END_RUN", runId: run.id, outcome: "abandoned" }, "Run moved to history.")}/>}
+ {page === "watches" && setEditor(createDefaultWatch())} onEdit={setEditor} onArm={(watch) => void arm(watch).catch((reason) => setToast(readableError(reason)))} onCopy={(watch) => { const copy = structuredClone(watch); copy.id = crypto.randomUUID(); copy.name = `${watch.name} — copy`; copy.schedule.enabled = false; copy.createdAt = Date.now(); setEditor(copy); }} onAutoStart={toggleAutoStart} onDelete={(watch) => { if (window.confirm(`Delete “${watch.name}”?`)) void act({ type: "DELETE_WATCH", watchId: watch.id }, "Watch deleted."); }}/>}
+ {page === "lab" && void act({ type: "OPEN_QUEUE_LAB" })}/>}
+ {page === "activity" && }
+ {page === "settings" && void act({ type: "SAVE_SETTINGS", settings }, "Settings saved.")} onTone={(tone) => void act({ type: "PREVIEW_TONE", tone })}/>}
+ >}
+ {attachOpen && setAttachOpen(false)} onAttach={(tab) => void attach(tab).catch((reason) => setToast(readableError(reason)))}/>}
+ {toast && {toast}
}
+ {busy ? "Working" : ""}
+
+ ;
+}
+
+function AppFrame({ panel, page, setPage, mobileNav, setMobileNav, children }: { panel: boolean; page: Page; setPage: (page: Page) => void; mobileNav: boolean; setMobileNav: (open: boolean) => void; children: React.ReactNode }) {
+ if (panel) return setMobileNav(!mobileNav)}/>{mobileNav && {panelNav.map((item) =>
{ setPage(item.id); setMobileNav(false); }}/>) } void send({ type: "OPEN_DASHBOARD" })}>Command center }{children} ;
+ return
+ {nav.map((item) => setPage(item.id)}/>)}
+
+ {children} ;
+}
+
+function Brand() { return ; }
+function PanelHeader({ onMenu }: { onMenu: () => void }) { return void send({ type: "OPEN_DASHBOARD" })} title="Open command center"> ; }
+function NavButton({ item, active, onClick }: { item: typeof nav[number]; active: boolean; onClick: () => void }) { const Icon = item.icon; return {item.label} {item.id === "runs" && } ; }
+
+function PageHeader({ eyebrow, title, copy, actions }: { eyebrow: string; title: string; copy: string; actions?: React.ReactNode }) { return ; }
+
+function Overview({ snapshot, attention, onNew, onAttach, onLab, onNavigate, onFocus, onEnd }: { snapshot: AppSnapshot; attention: number; onNew: () => void; onAttach: () => void; onLab: () => void; onNavigate: (page: Page) => void; onFocus: (run: Run) => void; onEnd: (run: Run) => void }) {
+ return Attach tab New watch >}/>
+
+
OPERATIONAL SCOPE {snapshot.activeRuns.length ? `${snapshot.activeRuns.length} live run${snapshot.activeRuns.length === 1 ? "" : "s"}` : "The field is clear"} {snapshot.activeRuns.some((run) => run.queueLocked) ? "At least one tab is under a navigation safety lock." : "No queue locks are currently engaged."}
onNavigate("runs")}>Open live workspace
+
Run a Queue Lab rehearsal Walk through product → queue → admitted safely.
Attach an open tab Observe a live page without refreshing it.
+ run.queueLocked).length} note="Navigation inhibited" icon={ShieldCheck}/>
+ onNavigate("runs")}/>
+ {snapshot.activeRuns.length ? snapshot.activeRuns.slice(0, 4).map((run) => onFocus(run)} onEnd={() => onEnd(run)}/>) : }
+ ;
+}
+
+function Stat({ label, value, note, icon: Icon }: { label: string; value: number; note: string; icon: typeof Activity }) { return {label} {String(value).padStart(2, "0")} {note}
; }
+function SectionHead({ title, count, action, onAction }: { title: string; count: number; action?: string; onAction?: () => void }) { return
{title} {count} {action &&
{action} }
; }
+
+function RunsPage({ snapshot, onAttach, onFocus, onEnd }: { snapshot: AppSnapshot; onAttach: () => void; onFocus: (run: Run) => void; onEnd: (run: Run) => void }) { return Attach tab}/>{snapshot.activeRuns.length ? snapshot.activeRuns.map((run) => onFocus(run)} onEnd={() => onEnd(run)}/>) : }
; }
+
+function WatchesPage({ snapshot, activeIds, olderOpen, setOlderOpen, onNew, onEdit, onArm, onCopy, onDelete, onAutoStart }: { snapshot: AppSnapshot; activeIds: Set; olderOpen: boolean; setOlderOpen: (open: boolean) => void; onNew: () => void; onEdit: (watch: Watch) => void; onArm: (watch: Watch) => void; onCopy: (watch: Watch) => void; onDelete: (watch: Watch) => void; onAutoStart: (watch: Watch) => void }) {
+ const current = snapshot.watches.filter((watch) => !isOlderWatch(watch));
+ const older = snapshot.watches.filter((watch) => isOlderWatch(watch));
+ const card = (watch: Watch) => onArm(watch)} onCopy={() => onCopy(watch)} onEdit={() => onEdit(watch)} onDelete={() => onDelete(watch)} onAutoStart={() => onAutoStart(watch)}/>;
+ return New watch}/>{current.length ? current.map(card) : }
{older.length > 0 && setOlderOpen(!olderOpen)}>Older watches {older.length}
{olderOpen &&
{older.map(card)}
}
} ;
+}
+
+function LabPage({ onOpen }: { onOpen: () => void }) { return FIVE CONTROLLED STATES Product → queue → challenge → available → admitted Queue Lab runs entirely inside the extension. The queue state also carries a synthetic position and durable provider ETA.
{["Product", "Queue", "Challenge", "Available", "Admitted"].map((item, index) =>
{index + 1} {item}
)}
Launch Queue Lab ; }
+function Principle({ icon: Icon, title, text }: { icon: typeof Activity; title: string; text: string }) { return ; }
+
+function ActivityPage({ snapshot }: { snapshot: AppSnapshot }) { return {snapshot.history.length ? snapshot.history.map((run) =>
{run.outcome || run.state} {run.watchName} {new Date(run.createdAt).toLocaleString()} · {run.checkCount} checks
{run.events.map((event) =>
{new Date(event.at).toLocaleTimeString()} {event.label}
)}
) :
}
; }
+
+function SettingsPage({ settings, onSave, onTone }: { settings: AppSettings; onSave: (settings: AppSettings) => void; onTone: (tone: ToneId) => void }) {
+ const [draft, setDraft] = useState(settings);
+ const toggle = (key: keyof AppSettings) => setDraft((value) => ({ ...value, [key]: !value[key] }));
+ return toggle("notificationsEnabled")}/> toggle("soundEnabled")}/>Sound profile Preview one of three locally generated tones.
{(["signal", "soft", "urgent"] as ToneId[]).map((tone) => { setDraft({ ...draft, tone }); onTone(tone); }}>{tone} )}
toggle("focusOnAdmission")}/>
+
toggle("expandRunsByDefault")}/>Density Comfortable or compact operational cards.
setDraft({ ...draft, density: event.target.value as AppSettings["density"] })}>Comfortable Compact Motion Reduce ambient orbit and status animations.
setDraft({ ...draft, motion: event.target.value as AppSettings["motion"] })}>Full Reduced
+
onSave(draft)}>Save settings
;
+}
+function SettingHeading({ icon: Icon, title, copy }: { icon: typeof Activity; title: string; copy: string }) { return ; }
+function Toggle({ label, copy, checked, onChange }: { label: string; copy: string; checked: boolean; onChange: () => void }) { return {label} {copy}
; }
+
+function AttachDialog({ tabs, busy, onClose, onAttach }: { tabs: AttachableTab[]; busy: boolean; onClose: () => void; onAttach: (tab: AttachableTab) => void }) { return { if (event.currentTarget === event.target) onClose(); }}>
{tabs.length ? tabs.map((tab) =>
onAttach(tab)}>{new URL(tab.url).hostname.slice(0, 1).toUpperCase()}
{tab.title} {new URL(tab.url).hostname}
{tab.attached ? "Attached" : "Attach"} ) :
}
Attached tabs use the default neutral Queue Lab signals until you save a custom watch.
; }
+
+function PanelContent({ page, snapshot, activeIds, olderOpen, setOlderOpen, setPage, onFocus, onEnd, onArm, onCopy, onDelete, onAutoStart }: { page: Page; snapshot: AppSnapshot; activeIds: Set; olderOpen: boolean; setOlderOpen: (open: boolean) => void; setPage: (page: Page) => void; onFocus: (run: Run) => void; onEnd: (run: Run) => void; onArm: (watch: Watch) => void; onCopy: (watch: Watch) => void; onDelete: (watch: Watch) => void; onAutoStart: (watch: Watch) => void }) {
+ if (page === "activity") return <>{snapshot.history.length ? snapshot.history.slice(0, 20).map((run) => {run.outcome || run.state} {run.watchName} {new Date(run.createdAt).toLocaleString()}
) : } >;
+ if (page === "watches") { const current = snapshot.watches.filter((watch) => !isOlderWatch(watch)); const older = snapshot.watches.filter(isOlderWatch); const card = (watch: Watch) => onArm(watch)} onCopy={() => onCopy(watch)} onEdit={() => void send({ type: "OPEN_DASHBOARD" })} onDelete={() => onDelete(watch)} onAutoStart={() => onAutoStart(watch)}/>; return <>{current.map(card)}{!current.length && void send({ type: "OPEN_DASHBOARD" })}/>} {older.length > 0 && setOlderOpen(!olderOpen)}>Older {older.length}
{olderOpen && older.map(card)}
} > }
+ return <>{snapshot.activeRuns.length ? snapshot.activeRuns.map((run) => onFocus(run)} onEnd={() => onEnd(run)}/>) : void send({ type: "OPEN_DASHBOARD" })}/>}
>;
+}
+function PanelTabs({ page, setPage }: { page: Page; setPage: (page: Page) => void }) { return <>{panelNav.map((item) => { const Icon = item.icon; return setPage(item.id)}>{item.label.replace("Live runs", "Live")} ; })}>; }
+function MobileBar({ items, page, setPage }: { items: typeof nav; page: Page; setPage: (page: Page) => void }) { return {items.slice(0, 5).map((item) => { const Icon = item.icon; return setPage(item.id)}>{item.label.split(" ")[0]} ; })} ; }
diff --git a/src/ui/WatchEditor.tsx b/src/ui/WatchEditor.tsx
new file mode 100644
index 0000000..72ae5f3
--- /dev/null
+++ b/src/ui/WatchEditor.tsx
@@ -0,0 +1,76 @@
+import { ArrowLeft, CalendarClock, Check, ChevronRight, Info, Radar, ShieldCheck, SlidersHorizontal } from "lucide-react";
+import { useMemo, useState } from "react";
+import { ALL_DAYS, WEEKDAY_LABELS } from "../core/schedule";
+import type { ScheduleType, Watch, Weekday } from "../core/types";
+
+function localInput(value?: number) {
+ if (!value) return "";
+ const date = new Date(value - new Date(value).getTimezoneOffset() * 60_000);
+ return date.toISOString().slice(0, 16);
+}
+
+function lines(value: string[]) { return value.join("\n"); }
+function list(value: string) { return value.split("\n").map((item) => item.trim()).filter(Boolean); }
+
+export function WatchEditor({ initial, onCancel, onSave }: { initial: Watch; onCancel: () => void; onSave: (watch: Watch, arm: boolean) => Promise }) {
+ const [watch, setWatch] = useState(initial);
+ const [step, setStep] = useState(0);
+ const [busy, setBusy] = useState(false);
+ const [error, setError] = useState("");
+ const steps = useMemo(() => ["Page", "Timing", "Signals", "Review"], []);
+ const update = (patch: Partial) => setWatch((current) => ({ ...current, ...patch }));
+ const updateSchedule = (patch: Partial) => update({ schedule: { ...watch.schedule, ...patch } });
+ const updateRules = (patch: Partial) => update({ rules: { ...watch.rules, ...patch } });
+ const submit = async (arm: boolean) => {
+ setBusy(true); setError("");
+ try { await onSave(watch, arm); } catch (reason) { setError(reason instanceof Error ? reason.message : "Could not save watch."); setBusy(false); }
+ };
+ return
+
WATCH BUILDER
{initial.name === "New page watch" ? "Create a page watch" : "Edit watch"}
+
{steps.map((label, index) => setStep(index)}>{index < step ? : index + 1} {label} )}
+
+
+ {step === 0 &&
}
+
+ {step === 1 &&
}
+
+ {step === 2 &&
}
+
+ {step === 3 &&
Ready to save Everything stays in extension-local storage. QueueScope never checks out or bypasses site controls.
+ PROFILE {watch.profile}
MODE {watch.mode}
CADENCE {watch.cadenceSeconds}s / {watch.activeCadenceSeconds}s fast
AUTO-START {watch.schedule.enabled ? "On" : "Off"}
+ Safety invariant Once configured queue or challenge evidence appears, guarded navigation is inhibited for that run. Admission requires prior queue evidence.
+ }
+ {error &&
{error}
}
+
setStep(step - 1) : onCancel}>{step ? "Back" : "Cancel"} {step < steps.length - 1 ? setStep(step + 1)}>Continue : <> void submit(false)}>Save watch void submit(true)}>Save & arm >}
+
+
;
+}
+
+function InfoTip({ text }: { text: string }) { return {text} ; }
diff --git a/src/ui/components.tsx b/src/ui/components.tsx
new file mode 100644
index 0000000..bc74b19
--- /dev/null
+++ b/src/ui/components.tsx
@@ -0,0 +1,88 @@
+import {
+ BellRing, CalendarCheck2, ChevronDown, CircleStop, Clock3, Copy, ExternalLink, Focus,
+ MoreHorizontal, Play, Radio, ShieldCheck, Sparkles, Trash2
+} from "lucide-react";
+import { useState } from "react";
+import { describeSchedule } from "../core/schedule";
+import type { Run, Watch } from "../core/types";
+
+export function StatePill({ run }: { run: Run }) {
+ const label = run.state === "ADMITTED" ? "ADMITTED" : run.queueLocked ? "LOCKED" : run.state.replace("_", " ");
+ return {label} ;
+}
+
+function time(value?: number) {
+ return value ? new Intl.DateTimeFormat([], { hour: "numeric", minute: "2-digit" }).format(value) : "—";
+}
+
+function countdown(value?: number) {
+ if (!value) return "—";
+ const ms = value - Date.now();
+ if (ms <= 0) return "due now";
+ const minutes = Math.ceil(ms / 60_000);
+ if (minutes < 60) return `${minutes}m`;
+ return `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
+}
+
+export function RunCard({ run, expandedByDefault, onFocus, onEnd }: {
+ run: Run; expandedByDefault: boolean; onFocus: () => void; onEnd: () => void;
+}) {
+ const [open, setOpen] = useState(expandedByDefault);
+ const host = (() => { try { const url = new URL(run.url); return url.protocol === "chrome-extension:" ? "Queue Lab" : url.hostname; } catch { return "attached page"; } })();
+ const observation = run.lastObservation;
+ return
+ setOpen(!open)} aria-expanded={open}>
+ LIVE RUN · {host}
+
+ {run.watchName} {run.queueLocked ? "Navigation safety lock engaged" : observation?.evidence[0] || `Next check ${countdown(run.nextCheckAt)}`}
+
+
+ {open &&
+
+
START {time(run.startAt)}
+
+
EXPECTED {time(run.expectedAt)}
+
+
STOP {run.profile === "passive" ? "MANUAL" : time(run.stopAt)}
+
+
+
Page state {observation?.classification || "Awaiting sample"}
+
Position {observation?.position?.toLocaleString() || "Not exposed"}
+
Provider ETA {observation?.providerEtaLabel || countdown(observation?.providerEtaAt)}
+
Checks {run.checkCount}
+
+
{run.queueLocked ? "Queue lock armed" : "Guarded navigation"} {run.queueLocked ? run.queueLockReason : run.mode === "observe-only" ? "Attached without refreshing" : "Refresh stops on queue or challenge evidence"}
+
+ {run.events.slice(-3).reverse().map((event) =>
{time(event.at)} {event.label}
)}
+
+
Open page End run
+
}
+ ;
+}
+
+export function WatchCard({ watch, active, onArm, onCopy, onEdit, onDelete, onAutoStart }: {
+ watch: Watch; active: boolean; onArm: () => void; onCopy: () => void; onEdit: () => void; onDelete: () => void; onAutoStart: () => void;
+}) {
+ const host = (() => { try { return new URL(watch.url).hostname; } catch { return "Invalid URL"; } })();
+ return
+
+ {watch.profile} · {host}
{watch.name} {describeSchedule(watch)} · {watch.mode === "guarded-refresh" ? "guarded refresh" : "observe only"}
+
+
{active ? : } {active ? "Armed" : "Arm"}
+
+
+
+
+
+ ;
+}
+
+export function EmptyState({ type, action }: { type: "runs" | "watches" | "history"; action?: () => void }) {
+ const copy = type === "runs" ? ["No live runs", "Arm a saved watch or attach an open tab. QueueScope will keep the operational state here."] : type === "watches" ? ["Build your first watch", "Define visible page signals, a schedule, and a safe observation cadence."] : ["The ledger is quiet", "Completed runs and their local event trails will appear here."];
+ return
{copy[0]} {copy[1]}
{action &&
Get started }
;
+}
+
+export function AttentionBanner({ count }: { count: number }) {
+ if (!count) return null;
+ return {count} run{count === 1 ? "" : "s"} need attention A configured signal or manual verification state is active.
;
+}
diff --git a/src/ui/main.tsx b/src/ui/main.tsx
new file mode 100644
index 0000000..1a577e3
--- /dev/null
+++ b/src/ui/main.tsx
@@ -0,0 +1,6 @@
+import { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
+import { App } from "./App";
+import "./styles.css";
+
+createRoot(document.getElementById("root")!).render( );
diff --git a/src/ui/platform.ts b/src/ui/platform.ts
new file mode 100644
index 0000000..b5918e2
--- /dev/null
+++ b/src/ui/platform.ts
@@ -0,0 +1,45 @@
+import { createDefaultSnapshot } from "../core/defaults";
+import type { AppSnapshot, RuntimeRequest, RuntimeResponse } from "../core/types";
+
+const demoKey = "queuescope-demo-snapshot";
+
+function hasExtensionRuntime(): boolean {
+ return typeof chrome !== "undefined" && Boolean(chrome.runtime?.id);
+}
+
+function demoSnapshot(): AppSnapshot {
+ const stored = window.localStorage.getItem(demoKey);
+ if (stored) {
+ try { return JSON.parse(stored) as AppSnapshot; } catch { /* fall through */ }
+ }
+ return createDefaultSnapshot();
+}
+
+export async function send(request: RuntimeRequest): Promise {
+ if (!hasExtensionRuntime()) {
+ if (request.type === "GET_SNAPSHOT") return demoSnapshot() as T;
+ throw new Error("This action is available in the loaded extension.");
+ }
+ const result = await chrome.runtime.sendMessage(request) as RuntimeResponse;
+ if (!result?.ok) throw new Error(result?.error || "QueueScope could not complete the request.");
+ return result.data as T;
+}
+
+export async function requestPageAccess(url: string): Promise {
+ if (!hasExtensionRuntime()) return true;
+ const parsed = new URL(url);
+ return chrome.permissions.request({ origins: [`${parsed.origin}/*`] });
+}
+
+export function onSnapshotChange(callback: () => void): () => void {
+ if (!hasExtensionRuntime()) return () => undefined;
+ const listener = (changes: Record, area: string) => {
+ if (area === "local" && changes["queuescope.snapshot.v1"]) callback();
+ };
+ chrome.storage.onChanged.addListener(listener);
+ return () => chrome.storage.onChanged.removeListener(listener);
+}
+
+export function isPanelSurface(): boolean {
+ return document.documentElement.dataset.surface === "panel";
+}
diff --git a/src/ui/styles.css b/src/ui/styles.css
new file mode 100644
index 0000000..3bfd393
--- /dev/null
+++ b/src/ui/styles.css
@@ -0,0 +1,154 @@
+:root {
+ color-scheme: dark;
+ --bg: #050b09;
+ --bg-2: #08110e;
+ --panel: #0b1512;
+ --panel-2: #0e1b17;
+ --line: #1c322b;
+ --line-bright: #2d5146;
+ --text: #f1f8f5;
+ --muted: #8ca29a;
+ --faint: #5f756d;
+ --mint: #49f5bd;
+ --mint-soft: #173b30;
+ --amber: #ffc865;
+ --red: #ff7c7c;
+ --blue: #8fbaff;
+ font-family: Inter, "Segoe UI Variable", "Segoe UI", system-ui, sans-serif;
+ font-synthesis: none;
+ background: var(--bg);
+ color: var(--text);
+}
+
+* { box-sizing: border-box; }
+html, body, #root { min-height: 100%; margin: 0; }
+body { background: radial-gradient(circle at 72% 0%, rgba(46, 177, 137, .08), transparent 30rem), var(--bg); }
+button, input, textarea, select { font: inherit; }
+button { color: inherit; }
+button:focus-visible, input:focus-visible, textarea:focus-visible, select:focus-visible, a:focus-visible { outline: 2px solid var(--mint); outline-offset: 2px; }
+a { color: inherit; text-decoration: none; }
+
+.app-shell { min-height: 100vh; display: grid; grid-template-columns: 248px minmax(0, 1fr); }
+.sidebar { position: fixed; inset: 0 auto 0 0; width: 248px; border-right: 1px solid var(--line); background: rgba(5, 12, 9, .94); backdrop-filter: blur(20px); display: flex; flex-direction: column; padding: 22px 14px; z-index: 20; }
+.brand { display: flex; align-items: center; gap: 11px; min-width: 0; padding: 0 8px 24px; }
+.brand-mark { position: relative; width: 36px; height: 36px; border: 1px solid #285044; border-radius: 11px; background: #07110e; flex: 0 0 auto; box-shadow: inset 0 0 18px rgba(73, 245, 189, .05); }
+.brand-mark::before { content: ""; position: absolute; width: 16px; height: 16px; border: 2px solid var(--mint); border-right-color: transparent; border-radius: 50%; left: 8px; top: 8px; }
+.brand-mark::after { content: ""; position: absolute; width: 8px; height: 2px; background: var(--mint); left: 19px; top: 23px; transform: rotate(35deg); border-radius: 9px; }
+.brand-mark span { position: absolute; width: 4px; height: 4px; border-radius: 50%; background: var(--mint); left: 25px; top: 10px; box-shadow: 0 0 10px var(--mint); }
+.brand strong { font-size: 16px; letter-spacing: -.03em; display: block; }
+.brand small { display: block; font: 500 8px/1 DM Mono, monospace; letter-spacing: .13em; color: var(--mint); margin-top: 4px; }
+.sidebar nav { display: grid; gap: 5px; }
+.nav-button { width: 100%; border: 0; background: transparent; border-radius: 9px; display: flex; gap: 12px; align-items: center; min-height: 43px; padding: 0 12px; color: var(--muted); cursor: pointer; font-size: 13px; text-align: left; position: relative; }
+.nav-button:hover { color: var(--text); background: #0d1814; }
+.nav-button.active { color: var(--mint); background: linear-gradient(90deg, rgba(73, 245, 189, .13), rgba(73, 245, 189, .03)); }
+.nav-button.active::before { content: ""; width: 3px; position: absolute; inset: 8px auto 8px -2px; background: var(--mint); border-radius: 0 3px 3px 0; box-shadow: 0 0 10px rgba(73, 245, 189, .5); }
+.nav-button i { margin-left: auto; width: 5px; height: 5px; border-radius: 50%; background: var(--mint); box-shadow: 0 0 8px var(--mint); }
+.sidebar-foot { margin-top: auto; display: grid; gap: 14px; padding: 0 4px; }
+.privacy-chip { display: flex; align-items: center; gap: 9px; border: 1px solid var(--line); padding: 10px; border-radius: 9px; color: var(--mint); }
+.privacy-chip div { display: grid; gap: 2px; }.privacy-chip strong { color: var(--text); font-size: 11px; }.privacy-chip span { font-size: 9px; color: var(--muted); }
+.sidebar-foot a { color: var(--muted); font-size: 11px; display: flex; gap: 8px; align-items: center; padding: 6px; }
+.sidebar-foot a:hover { color: var(--mint); }
+.main-content { grid-column: 2; min-width: 0; }
+.page { width: min(1320px, 100%); margin: auto; padding: 52px clamp(28px, 4vw, 70px) 90px; animation: page-in .35s ease both; }
+@keyframes page-in { from { opacity: 0; transform: translateY(8px); } }
+.page-header { display: flex; justify-content: space-between; gap: 40px; align-items: end; margin-bottom: 32px; }
+.page-header > div:first-child { max-width: 740px; }
+.eyebrow { font: 500 10px/1.2 DM Mono, monospace; letter-spacing: .14em; color: var(--mint); text-transform: uppercase; }
+.page-header h1 { margin: 10px 0 10px; font-size: clamp(31px, 4vw, 52px); line-height: .98; letter-spacing: -.055em; max-width: 760px; }
+.page-header p { color: var(--muted); line-height: 1.65; font-size: 14px; margin: 0; max-width: 680px; }
+.page-actions { display: flex; gap: 9px; flex: 0 0 auto; }
+.button { border: 1px solid var(--line-bright); border-radius: 7px; min-height: 38px; padding: 0 14px; background: #0c1814; display: inline-flex; align-items: center; justify-content: center; gap: 8px; font-size: 11px; font-weight: 700; letter-spacing: .02em; cursor: pointer; transition: transform .15s, border-color .15s, background .15s; }
+.button:hover:not(:disabled) { transform: translateY(-1px); border-color: #4e786b; }.button:disabled { opacity: .48; cursor: default; }
+.button.primary { color: #03120d; background: var(--mint); border-color: var(--mint); box-shadow: 0 0 24px rgba(73,245,189,.12); }
+.button.primary:hover:not(:disabled) { background: #75ffcf; }
+.button.ghost { color: var(--text); background: transparent; }
+.button.success { color: var(--mint); background: var(--mint-soft); border-color: #2e6453; }
+.button.danger { color: #ffb4b4; border-color: #59302f; background: #1b100f; }
+.button.signal { margin-left: auto; align-self: end; background: transparent; border-color: rgba(73,245,189,.35); color: var(--mint); }
+.icon-button { border: 1px solid var(--line); background: #0b1512; color: var(--muted); border-radius: 7px; width: 34px; height: 34px; display: inline-grid; place-items: center; cursor: pointer; }
+.icon-button:hover { color: var(--text); border-color: var(--line-bright); }.danger-icon:hover { color: var(--red); border-color: #5b3030; }
+.hero-grid { display: grid; grid-template-columns: minmax(0, 1.8fr) minmax(280px, .8fr); gap: 14px; }
+.signal-hero { min-height: 240px; position: relative; overflow: hidden; border: 1px solid var(--line); background: linear-gradient(135deg, #0c1b16, #08100e 66%); padding: 34px; display: flex; align-items: end; gap: 26px; }
+.signal-hero::after { content: ""; position: absolute; inset: 0; pointer-events: none; background: linear-gradient(90deg, transparent 49.8%, rgba(73,245,189,.025) 50%), linear-gradient(transparent 49.8%, rgba(73,245,189,.025) 50%); background-size: 32px 32px; mask-image: linear-gradient(90deg, black, transparent); }
+.signal-hero h2 { font-size: 30px; letter-spacing: -.045em; margin: 8px 0 6px; }.signal-hero p { color: var(--muted); font-size: 12px; margin: 0; }
+.signal-hero > * { position: relative; z-index: 1; }
+.orbit-visual { width: 112px; height: 112px; border: 1px solid #2c5b4c; border-radius: 50%; position: relative; flex: 0 0 auto; box-shadow: 0 0 40px rgba(73,245,189,.05), inset 0 0 30px rgba(73,245,189,.03); }
+.orbit-visual::before, .orbit-visual::after { content: ""; position: absolute; inset: 17px; border: 1px solid #1c3d33; border-radius: 50%; }.orbit-visual::after { inset: 38px; background: rgba(73,245,189,.08); border-color: var(--mint); box-shadow: 0 0 18px rgba(73,245,189,.16); }
+.orbit-visual span { position: absolute; width: 8px; height: 8px; background: var(--mint); border-radius: 50%; top: 4px; left: 51px; transform-origin: 4px 52px; animation: orbit 5s linear infinite; box-shadow: 0 0 12px var(--mint); }
+.orbit-visual i { position: absolute; inset: 54px -30px auto; height: 1px; background: linear-gradient(90deg, transparent, #2f6956, transparent); }
+.orbit-visual b { position: absolute; inset: -30px auto -30px 55px; width: 1px; background: linear-gradient(transparent, #2f6956, transparent); }
+@keyframes orbit { to { transform: rotate(360deg); } }
+.quick-stack { display: grid; gap: 10px; }
+.quick-stack button { text-align: left; border: 1px solid var(--line); background: #0b1512; padding: 20px; display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 14px; cursor: pointer; color: var(--mint); }
+.quick-stack button:hover { border-color: var(--line-bright); background: #0e1c17; }.quick-stack strong { color: var(--text); font-size: 13px; display: block; }.quick-stack span { color: var(--muted); font-size: 10px; display: block; margin-top: 5px; line-height: 1.45; }
+.stats-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin: 14px 0 42px; }
+.stat-card { border: 1px solid var(--line); background: rgba(11,21,18,.82); padding: 18px; display: grid; grid-template-columns: auto 1fr; column-gap: 10px; align-items: center; }
+.stat-card > svg { color: var(--mint); }.stat-card > span { color: var(--muted); font-size: 10px; text-transform: uppercase; letter-spacing: .08em; }.stat-card strong { grid-column: 1 / -1; font: 500 32px/1 DM Mono, monospace; margin: 16px 0 8px; }.stat-card small { grid-column: 1 / -1; color: var(--faint); font-size: 9px; }
+.section-head { display: flex; justify-content: space-between; align-items: center; margin: 28px 0 12px; }
+.section-head > div { display: flex; align-items: center; gap: 9px; }.section-head h2 { font-size: 15px; margin: 0; letter-spacing: -.02em; }.section-head > div > span { font: 500 9px DM Mono, monospace; color: var(--mint); border: 1px solid #285244; padding: 3px 6px; border-radius: 9px; }
+.section-head > button { border: 0; background: transparent; color: var(--muted); font-size: 10px; display: flex; gap: 5px; align-items: center; cursor: pointer; }.section-head > button:hover { color: var(--mint); }
+.run-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }.run-grid.full { grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); }
+.run-card { --card-accent: var(--mint); border: 1px solid var(--line); background: #0a1310; position: relative; overflow: hidden; min-width: 0; }
+.run-card::before { content: ""; position: absolute; inset: 0 auto 0 0; width: 3px; background: var(--card-accent); opacity: .75; }.run-card.is-locked { --card-accent: var(--amber); border-color: #4b3f27; }
+.run-card-head { width: 100%; display: grid; grid-template-columns: 1fr auto; gap: 7px 15px; text-align: left; padding: 17px 18px 16px 20px; border: 0; background: transparent; cursor: pointer; position: relative; }
+.run-signal { display: flex; align-items: center; gap: 7px; color: var(--card-accent); font: 500 9px/1.2 DM Mono, monospace; letter-spacing: .09em; text-transform: uppercase; min-width: 0; }.run-signal span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.state-pill { display: inline-flex; align-items: center; gap: 6px; border: 1px solid #2a5749; background: #10241e; color: var(--mint); font: 500 8px/1 DM Mono, monospace; letter-spacing: .08em; padding: 7px 8px; border-radius: 5px; white-space: nowrap; }
+.state-waiting { color: var(--amber); border-color: #5d4c27; background: #211c0d; }.state-attention, .state-admitted { color: #fce9a6; border-color: #6b572c; background: #261f10; }.state-failed { color: var(--red); border-color: #663a3a; background: #211111; }
+.pulse-dot { width: 5px; height: 5px; border-radius: 50%; background: currentColor; box-shadow: 0 0 7px currentColor; animation: pulse 1.8s ease-in-out infinite; }
+@keyframes pulse { 50% { opacity: .35; transform: scale(.7); } }
+.run-title-group { min-width: 0; }.run-title-group h3 { font-size: 16px; line-height: 1.25; letter-spacing: -.025em; margin: 2px 0 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }.run-title-group p { color: var(--muted); margin: 0; font-size: 10px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+.run-chevron { align-self: center; justify-self: end; color: var(--faint); transition: transform .2s; }.is-open .run-chevron { transform: rotate(180deg); }
+.run-details { padding: 0 18px 18px 20px; border-top: 1px solid var(--line); animation: reveal .2s ease both; } @keyframes reveal { from { opacity: 0; transform: translateY(-5px); } }
+.time-rail { display: grid; grid-template-columns: auto 1fr auto 1fr auto; align-items: center; gap: 9px; padding: 15px 0; }.time-rail div { display: grid; gap: 4px; }.time-rail span, .metric-grid span { font: 500 8px DM Mono, monospace; color: var(--faint); letter-spacing: .08em; }.time-rail strong { font-size: 10px; }.time-rail i { height: 1px; background: var(--line-bright); position: relative; }.time-rail i::after { content: ""; position: absolute; width: 4px; height: 4px; background: var(--line-bright); border-radius: 50%; right: 0; top: -1.5px; }
+.metric-grid { display: grid; grid-template-columns: repeat(4, 1fr); border: 1px solid var(--line); }.metric-grid > div { padding: 10px; display: grid; gap: 5px; min-width: 0; }.metric-grid > div + div { border-left: 1px solid var(--line); }.metric-grid strong { font-size: 10px; overflow: hidden; text-overflow: ellipsis; text-transform: capitalize; }
+.safety-strip { display: flex; gap: 9px; align-items: center; color: var(--card-accent); margin-top: 10px; background: color-mix(in srgb, var(--card-accent) 7%, transparent); border: 1px solid color-mix(in srgb, var(--card-accent) 25%, transparent); padding: 10px; }.safety-strip div { display: grid; gap: 2px; }.safety-strip strong { color: var(--text); font-size: 9px; }.safety-strip span { color: var(--muted); font-size: 8px; }
+.event-list { padding: 9px 0 0; }.event-list > div { display: grid; grid-template-columns: 62px 1fr; gap: 8px; padding: 5px 0; font-size: 9px; color: var(--muted); }.event-list time { font-family: DM Mono, monospace; color: var(--faint); }
+.card-actions { display: flex; justify-content: flex-end; gap: 7px; margin-top: 10px; }
+.attention-banner { border: 1px solid #5b4a25; background: linear-gradient(90deg, rgba(255,200,101,.12), rgba(255,200,101,.03)); display: flex; align-items: center; gap: 12px; padding: 13px 15px; color: var(--amber); margin: -13px 0 18px; }.attention-banner div { display: grid; gap: 2px; flex: 1; }.attention-banner strong { color: var(--text); font-size: 11px; }.attention-banner span { color: var(--muted); font-size: 9px; }
+.empty-state { grid-column: 1 / -1; min-height: 240px; border: 1px dashed var(--line-bright); display: flex; flex-direction: column; justify-content: center; align-items: center; text-align: center; padding: 30px; background: rgba(8,17,14,.55); }.empty-state h3 { margin: 14px 0 6px; font-size: 16px; }.empty-state p { margin: 0 0 18px; max-width: 420px; color: var(--muted); font-size: 11px; line-height: 1.6; }.empty-orbit { width: 48px; height: 48px; border-radius: 50%; border: 1px solid #285244; display: grid; place-items: center; color: var(--mint); position: relative; }.empty-orbit::after { content: ""; width: 6px; height: 6px; position: absolute; background: var(--mint); border-radius: 50%; top: -3px; }
+.watch-list { display: grid; gap: 8px; }.watch-card { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 14px; align-items: center; padding: 16px; border: 1px solid var(--line); background: #0a1411; }.watch-card:hover { border-color: #2b473e; }.watch-icon { width: 36px; height: 36px; border: 1px solid var(--line-bright); display: grid; place-items: center; color: var(--mint); }.watch-main { min-width: 0; }.watch-main h3 { margin: 4px 0; font-size: 14px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; }.watch-main p { margin: 0; color: var(--muted); font-size: 9px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; }.watch-actions { display: flex; gap: 6px; }.older-section { margin-top: 28px; border-top: 1px solid var(--line); padding-top: 12px; }.older-section > button { width: 100%; border: 0; background: transparent; display: flex; justify-content: space-between; align-items: center; padding: 9px 4px; cursor: pointer; }.older-section > button > div { display: flex; align-items: center; gap: 8px; color: var(--muted); }.older-section > button strong { color: var(--text); font-size: 12px; }.older-section > button span { font: 500 9px DM Mono, monospace; color: var(--mint); }.older-section .rotated { transform: rotate(180deg); }.older-section .watch-list { margin-top: 8px; }
+
+/* Editor */
+.editor-shell { max-width: 940px; margin: auto; padding: 45px 30px 100px; }.editor-top { display: flex; align-items: center; gap: 14px; margin-bottom: 28px; }.editor-top h2 { margin: 5px 0 0; font-size: 24px; letter-spacing: -.035em; }
+.stepper { display: grid; grid-template-columns: repeat(4, 1fr); margin-bottom: 14px; border: 1px solid var(--line); }.stepper button { border: 0; border-right: 1px solid var(--line); background: #09120f; color: var(--muted); min-height: 48px; display: flex; align-items: center; gap: 9px; padding: 0 14px; font-size: 10px; cursor: pointer; }.stepper button:last-child { border-right: 0; }.stepper button span { width: 20px; height: 20px; border-radius: 50%; border: 1px solid var(--line-bright); display: grid; place-items: center; font: 500 8px DM Mono, monospace; }.stepper button.active { color: var(--mint); background: var(--mint-soft); }.stepper button.done { color: #9dcabb; }
+.editor-card { border: 1px solid var(--line); background: #08120f; padding: clamp(22px, 4vw, 42px); }.form-section { display: grid; gap: 18px; min-height: 470px; }.section-heading { display: flex; gap: 13px; margin-bottom: 7px; color: var(--mint); }.section-heading h3 { color: var(--text); margin: 0 0 5px; font-size: 19px; letter-spacing: -.025em; }.section-heading p { color: var(--muted); font-size: 11px; margin: 0; line-height: 1.5; }.form-section label, .settings-page label { display: grid; gap: 7px; font-size: 10px; font-weight: 700; color: #cbd8d3; position: relative; }.form-section small, .settings-page small { font-weight: 400; color: var(--faint); font-size: 8px; }.form-section input, .form-section textarea, .form-section select, .settings-page input, .settings-page select { width: 100%; border: 1px solid var(--line-bright); background: #050c0a; color: var(--text); padding: 11px 12px; border-radius: 5px; font-size: 11px; }.form-section textarea { min-height: 100px; resize: vertical; line-height: 1.5; }.form-section input:focus, .form-section textarea:focus { border-color: var(--mint); }
+.field-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 13px; }.field-grid.three { grid-template-columns: repeat(3, minmax(0, 1fr)); }
+.choice-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 9px; }.choice { text-align: left; border: 1px solid var(--line); background: #091410; padding: 17px; display: grid; grid-template-columns: auto 1fr; gap: 6px 10px; cursor: pointer; color: var(--muted); }.choice > svg { color: var(--mint); grid-row: 1 / span 2; }.choice strong { color: var(--text); font-size: 11px; }.choice span { font-size: 9px; line-height: 1.5; }.choice.active { border-color: #34715d; background: rgba(73,245,189,.07); box-shadow: inset 0 0 20px rgba(73,245,189,.025); }
+.toggle-row { grid-template-columns: 1fr auto !important; align-items: center; border: 1px solid var(--line); background: #0b1612; padding: 13px; }.toggle-row span { display: grid; gap: 4px; }.toggle-row input, .setting-row input[type=checkbox] { appearance: none; width: 36px; height: 20px; border-radius: 12px; background: #1a2924; border: 1px solid #315047; padding: 0; position: relative; cursor: pointer; }.toggle-row input::after, .setting-row input[type=checkbox]::after { content: ""; width: 14px; height: 14px; position: absolute; top: 2px; left: 2px; background: #81938d; border-radius: 50%; transition: .2s; }.toggle-row input:checked, .setting-row input[type=checkbox]:checked { background: var(--mint-soft); border-color: var(--mint); }.toggle-row input:checked::after, .setting-row input[type=checkbox]:checked::after { left: 18px; background: var(--mint); }
+.weekday-row { display: grid; grid-template-columns: repeat(7, 1fr); gap: 5px; }.weekday-row button { border: 1px solid var(--line); background: #08120f; color: var(--muted); border-radius: 5px; padding: 9px 0; font-size: 9px; cursor: pointer; }.weekday-row button.selected { color: var(--mint); border-color: #36705e; background: var(--mint-soft); }
+.callout, .safety-card { display: flex; gap: 12px; padding: 15px; border: 1px solid #2e5a4b; color: var(--mint); background: rgba(73,245,189,.055); }.callout strong, .safety-card strong { color: var(--text); font-size: 11px; }.callout p, .safety-card p { color: var(--muted); font-size: 9px; margin: 4px 0 0; line-height: 1.5; }
+.info-tip { display: inline-flex; color: var(--faint); vertical-align: middle; margin-left: 3px; cursor: help; position: relative; }.info-tip > span { display: none; position: absolute; bottom: calc(100% + 6px); left: -10px; width: 230px; background: #14231e; border: 1px solid var(--line-bright); color: #c5d4cf; padding: 9px; border-radius: 5px; z-index: 10; font-size: 9px; line-height: 1.45; font-weight: 400; box-shadow: 0 8px 25px #000; }.info-tip:hover > span, .info-tip:focus > span { display: block; }
+.review-section { min-height: 370px; }.review-grid { display: grid; grid-template-columns: repeat(4, 1fr); border: 1px solid var(--line); }.review-grid > div { display: grid; gap: 8px; padding: 18px; border-right: 1px solid var(--line); }.review-grid > div:last-child { border-right: 0; }.review-grid span { font: 500 8px DM Mono, monospace; color: var(--faint); }.review-grid strong { font-size: 10px; text-transform: capitalize; }
+.editor-actions { display: flex; justify-content: flex-end; gap: 8px; border-top: 1px solid var(--line); margin-top: 25px; padding-top: 20px; }.form-error { border: 1px solid #683b3b; background: #211313; color: #ffb7b7; font-size: 10px; padding: 10px; margin-top: 12px; }
+
+/* Lab, settings, ledger */
+.lab-stage { border: 1px solid var(--line); background: radial-gradient(circle at 25% 50%, rgba(73,245,189,.08), transparent 21rem), #08120f; min-height: 430px; display: grid; grid-template-columns: .9fr 1.1fr; align-items: center; padding: clamp(30px, 6vw, 70px); gap: 70px; overflow: hidden; }.lab-orbit { --orbit-size: min(300px, 70vw); width: var(--orbit-size); aspect-ratio: 1; border: 1px solid #265042; border-radius: 50%; position: relative; display: grid; place-items: center; justify-self: center; }.lab-orbit::before, .lab-orbit::after { content: ""; position: absolute; inset: 14%; border: 1px solid #1c3b31; border-radius: 50%; }.lab-orbit::after { inset: 30%; background: #0c1a16; box-shadow: 0 0 60px rgba(73,245,189,.07); }.lab-orbit > div { position: relative; z-index: 2; display: grid; place-items: center; gap: 12px; color: var(--mint); }.lab-orbit > div strong { font: 500 8px DM Mono, monospace; letter-spacing: .15em; }.orbiter { width: 10px; height: 10px; border-radius: 50%; background: var(--mint); box-shadow: 0 0 14px var(--mint); position: absolute; top: -5px; left: calc(50% - 5px); transform-origin: 5px calc(var(--orbit-size) / 2 + 5px); animation: lab-orbit 8s linear infinite; z-index: 3; } @keyframes lab-orbit { to { transform: rotate(360deg); } }
+.lab-copy h2 { font-size: 31px; line-height: 1.06; letter-spacing: -.045em; margin: 11px 0 13px; }.lab-copy > p { color: var(--muted); font-size: 11px; line-height: 1.65; }.lab-steps { display: grid; grid-template-columns: repeat(4, 1fr); margin: 24px 0; }.lab-steps > div { display: grid; gap: 7px; position: relative; }.lab-steps > div:not(:last-child)::after { content: ""; position: absolute; top: 9px; left: 27px; right: 8px; height: 1px; background: #305a4c; }.lab-steps span { width: 19px; height: 19px; border: 1px solid #3b715f; color: var(--mint); border-radius: 50%; display: grid; place-items: center; font: 500 8px DM Mono, monospace; }.lab-steps strong { font-size: 8px; color: var(--muted); }.principles-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-top: 12px; }.principles-grid > div { padding: 20px; border: 1px solid var(--line); background: #091310; display: grid; gap: 9px; }.principles-grid svg { color: var(--mint); }.principles-grid strong { font-size: 11px; }.principles-grid p { margin: 0; color: var(--muted); font-size: 9px; line-height: 1.55; }
+.settings-page { max-width: 1050px; }.settings-card { border: 1px solid var(--line); background: #091310; padding: 24px; margin-bottom: 12px; }.setting-heading { display: flex; gap: 12px; color: var(--mint); border-bottom: 1px solid var(--line); padding-bottom: 17px; }.setting-heading h2 { color: var(--text); margin: 0; font-size: 15px; }.setting-heading p { margin: 4px 0 0; color: var(--muted); font-size: 9px; }.setting-row { min-height: 62px; display: flex; justify-content: space-between; align-items: center; gap: 20px; border-bottom: 1px solid #14231e; }.setting-row:last-child { border-bottom: 0; }.setting-row > div:first-child { display: grid; gap: 4px; }.setting-row strong { font-size: 10px; }.setting-row span { color: var(--muted); font-size: 9px; }.setting-row select { width: 170px; border: 1px solid var(--line-bright); background: #050c0a; color: var(--text); padding: 8px; font-size: 10px; }.setting-row.toggle { grid-template-columns: 1fr auto; }.tone-picker { display: flex; gap: 5px; }.tone-picker button { border: 1px solid var(--line); background: #0c1713; color: var(--muted); border-radius: 5px; padding: 7px 9px; display: flex; align-items: center; gap: 5px; font-size: 8px; text-transform: capitalize; cursor: pointer; }.tone-picker button.active { color: var(--mint); border-color: #3b7561; background: var(--mint-soft); }.sticky-save { position: sticky; bottom: 0; background: linear-gradient(transparent, var(--bg) 45%); padding: 30px 0 4px; display: flex; justify-content: flex-end; }
+.ledger { display: grid; gap: 7px; }.ledger details { border: 1px solid var(--line); background: #091310; }.ledger summary { list-style: none; display: grid; grid-template-columns: auto 1fr auto; gap: 14px; align-items: center; padding: 15px; cursor: pointer; }.ledger summary::-webkit-details-marker { display: none; }.ledger summary div { display: grid; gap: 4px; }.ledger summary strong { font-size: 11px; }.ledger summary small { color: var(--muted); font-size: 8px; }.ledger-state { color: var(--mint); border: 1px solid #2d5b4b; padding: 5px 7px; font: 500 8px DM Mono, monospace; text-transform: uppercase; }.ledger details[open] summary svg { transform: rotate(180deg); }.ledger details > div { border-top: 1px solid var(--line); padding: 10px 15px; }.ledger details p { display: grid; grid-template-columns: 100px 1fr; font-size: 9px; color: var(--muted); margin: 0; padding: 5px 0; }.ledger details time { color: var(--faint); font-family: DM Mono, monospace; }
+
+/* Modal */
+.modal-backdrop { position: fixed; inset: 0; background: rgba(0,5,3,.76); backdrop-filter: blur(8px); display: grid; place-items: center; z-index: 100; padding: 20px; animation: page-in .15s ease; }.modal { width: min(630px, 100%); max-height: min(720px, 90vh); overflow: auto; background: #08120f; border: 1px solid var(--line-bright); box-shadow: 0 30px 100px #000; }.modal > header { display: flex; justify-content: space-between; gap: 20px; padding: 24px; border-bottom: 1px solid var(--line); }.modal h2 { margin: 6px 0; font-size: 22px; }.modal p { margin: 0; color: var(--muted); font-size: 10px; }.tab-list { display: grid; padding: 10px; gap: 5px; max-height: 450px; overflow: auto; }.tab-list > button { width: 100%; border: 1px solid var(--line); background: #0a1511; display: grid; grid-template-columns: auto 1fr auto; gap: 11px; align-items: center; padding: 11px; text-align: left; cursor: pointer; }.tab-list > button:hover:not(:disabled) { border-color: #376858; background: #0e1c17; }.tab-list > button:disabled { opacity: .5; }.tab-list > button > div:nth-child(2) { min-width: 0; display: grid; gap: 4px; }.tab-list strong { font-size: 10px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.tab-list span { color: var(--muted); font-size: 8px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.tab-list > button > span:last-child { color: var(--mint); font-weight: 700; }.tab-favicon { width: 30px; height: 30px; display: grid; place-items: center; border: 1px solid var(--line-bright); color: var(--mint); font: 500 10px DM Mono, monospace; }.modal-note { margin: 0 10px 10px; padding: 10px; border: 1px solid var(--line); color: var(--muted); display: flex; gap: 8px; align-items: center; font-size: 8px; }.modal-note svg { color: var(--mint); }
+.toast { position: fixed; bottom: 24px; right: 24px; z-index: 200; border: 1px solid #3d745f; background: #10221c; box-shadow: 0 12px 40px #000; color: #dff9f0; display: flex; align-items: center; gap: 9px; padding: 12px 15px; font-size: 10px; animation: toast-in .25s ease; }.toast svg { color: var(--mint); } @keyframes toast-in { from { transform: translateY(10px); opacity: 0; } }
+.busy-sr { position: fixed; width: 1px; height: 1px; overflow: hidden; clip: rect(0,0,0,0); }
+.mobile-bar { display: none; }
+
+/* Side panel */
+html[data-surface="panel"] body { background: #050b09; min-width: 300px; }
+.panel-shell { min-height: 100vh; display: flex; flex-direction: column; padding-bottom: 36px; }
+.panel-header { height: 66px; flex: 0 0 auto; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid var(--line); padding: 0 10px; background: rgba(5,11,9,.96); position: sticky; top: 0; z-index: 10; }.panel-header .brand { padding: 0; }.panel-header .brand-mark { width: 36px; height: 36px; }.panel-header > div { display: flex; gap: 6px; }
+.panel-tabs { position: sticky; top: 66px; z-index: 9; height: 52px; display: grid; grid-template-columns: repeat(3, 1fr); background: rgba(6,13,10,.96); border-bottom: 1px solid var(--line); }.panel-tabs button { border: 0; border-bottom: 2px solid transparent; background: transparent; color: var(--muted); display: flex; align-items: center; justify-content: center; gap: 7px; font-size: 9px; cursor: pointer; }.panel-tabs button.active { color: var(--mint); border-bottom-color: var(--mint); }
+.panel-content { min-height: 0; flex: 1; }.panel-section { padding: 0 10px 20px; }.panel-section .section-head { margin-top: 14px; }.panel-section .run-grid { grid-template-columns: 1fr; }.panel-section .run-card-head { padding: 14px 14px 13px 16px; }.panel-section .run-title-group h3 { font-size: 14px; }.panel-section .run-details { padding: 0 13px 14px 16px; }.panel-section .metric-grid { grid-template-columns: repeat(2, 1fr); }.panel-section .metric-grid > div:nth-child(3) { border-left: 0; border-top: 1px solid var(--line); }.panel-section .metric-grid > div:nth-child(4) { border-top: 1px solid var(--line); }.panel-section .time-rail { gap: 6px; }.panel-section .watch-card { grid-template-columns: auto minmax(0, 1fr); padding: 12px; }.panel-section .watch-actions { grid-column: 1 / -1; justify-content: flex-end; }.panel-section .watch-actions .danger-icon, .panel-section .watch-actions [title="Copy watch"] { display: none; }.panel-section .empty-state { min-height: 300px; }
+.panel-footer { position: fixed; inset: auto 0 0; height: 36px; display: flex; align-items: center; gap: 6px; padding: 0 12px; color: var(--faint); font: 400 8px DM Mono, monospace; background: #050b09; border-top: 1px solid var(--line); z-index: 20; }.panel-footer svg { color: var(--mint); }.panel-footer span:last-child { margin-left: auto; }.panel-menu { position: fixed; right: 10px; top: 58px; z-index: 30; width: 210px; background: #0b1612; border: 1px solid var(--line-bright); box-shadow: 0 20px 50px #000; padding: 7px; }.panel-menu .nav-button, .panel-menu > button { min-height: 39px; width: 100%; border: 0; background: transparent; color: var(--muted); display: flex; gap: 9px; align-items: center; padding: 0 10px; font-size: 10px; cursor: pointer; }.panel-menu button:hover { background: #112019; color: var(--text); }.panel-ledger { border: 1px solid var(--line); padding: 11px; display: grid; gap: 4px; margin-bottom: 6px; }.panel-ledger span { color: var(--mint); font: 500 8px DM Mono, monospace; text-transform: uppercase; }.panel-ledger strong { font-size: 10px; }.panel-ledger small { color: var(--muted); font-size: 8px; }
+
+@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; } }
+@media (max-width: 1000px) { .hero-grid { grid-template-columns: 1fr; }.quick-stack { grid-template-columns: repeat(2, 1fr); }.stats-row { grid-template-columns: repeat(2, 1fr); }.lab-stage { grid-template-columns: 1fr; gap: 35px; }.run-grid, .run-grid.full { grid-template-columns: 1fr; } }
+@media (max-width: 720px) { .app-shell { display: block; }.sidebar { display: none; }.main-content { grid-column: auto; }.page { padding: 28px 16px 86px; }.page-header { align-items: start; flex-direction: column; gap: 18px; }.page-header h1 { font-size: 36px; }.page-actions { width: 100%; }.page-actions .button { flex: 1; }.stats-row { grid-template-columns: repeat(2, 1fr); }.quick-stack { grid-template-columns: 1fr; }.signal-hero { flex-wrap: wrap; }.button.signal { margin: 0; }.watch-card { grid-template-columns: auto 1fr; }.watch-actions { grid-column: 1 / -1; justify-content: flex-end; }.field-grid, .field-grid.three, .choice-grid, .review-grid, .principles-grid { grid-template-columns: 1fr; }.review-grid > div { border-right: 0; border-bottom: 1px solid var(--line); }.stepper button { justify-content: center; padding: 0; }.stepper button { font-size: 0; }.stepper button span { font-size: 8px; }.lab-stage { padding: 28px 18px; }.mobile-bar { display: grid; grid-template-columns: repeat(5, 1fr); position: fixed; inset: auto 0 0; background: rgba(6,13,10,.96); backdrop-filter: blur(14px); border-top: 1px solid var(--line); z-index: 40; height: 62px; }.mobile-bar button { border: 0; background: transparent; color: var(--muted); display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px; font-size: 8px; }.mobile-bar button.active { color: var(--mint); }.modal-backdrop { padding: 8px; }.tone-picker { flex-wrap: wrap; justify-content: flex-end; } }
+
+html[data-surface="panel"] .mobile-bar { display: none !important; }
+html[data-motion="reduced"] *, html[data-motion="reduced"] *::before, html[data-motion="reduced"] *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; }
+html[data-density="compact"] .run-card-head { padding-top: 12px; padding-bottom: 11px; }
+html[data-density="compact"] .watch-card { padding-top: 11px; padding-bottom: 11px; }
+.lab-steps { grid-template-columns: repeat(5, 1fr); }
+.icon-button.auto-on { color: var(--mint); border-color: #36705e; background: var(--mint-soft); }
+.panel-section .watch-actions .danger-icon, .panel-section .watch-actions [title="Copy watch"] { display: inline-grid; }
diff --git a/tests/rules.test.ts b/tests/rules.test.ts
new file mode 100644
index 0000000..1394ba5
--- /dev/null
+++ b/tests/rules.test.ts
@@ -0,0 +1,19 @@
+import { describe, expect, it } from "vitest";
+import { DEFAULT_RULES } from "../src/core/defaults";
+import { classifyWithRules, compileSafePattern, isSafeSelector, normalizeTextRules, parseDuration } from "../src/core/rules";
+
+describe("generic rule engine", () => {
+ it("classifies queue and captures position plus absolute ETA", () => {
+ const now = Date.UTC(2026, 7, 10, 12);
+ const result = classifyWithRules(DEFAULT_RULES, { url: "https://example.test/item", title: "Example", text: "You are now in the waiting room. Position 428. Estimated wait 00:08:30", availableSelectorMatched: false, priorQueueLock: false, observedAt: now });
+ expect(result.classification).toBe("queue"); expect(result.position).toBe(428); expect(result.providerEtaAt).toBe(now + 510_000);
+ });
+ it("refuses premature admission", () => expect(classifyWithRules(DEFAULT_RULES, { url: "https://example.test", title: "", text: "Access granted. Your session is ready", availableSelectorMatched: false, priorQueueLock: false }).classification).toBe("product"));
+ it("accepts admission after prior queue evidence", () => expect(classifyWithRules(DEFAULT_RULES, { url: "https://example.test", title: "", text: "Access granted. Your session is ready", availableSelectorMatched: false, priorQueueLock: true }).classification).toBe("admitted"));
+ it("prioritizes challenges", () => expect(classifyWithRules(DEFAULT_RULES, { url: "https://example.test", title: "", text: "Please verify you are human", availableSelectorMatched: false, priorQueueLock: false }).classification).toBe("challenge"));
+ it("recognizes an enabled configured selector", () => expect(classifyWithRules(DEFAULT_RULES, { url: "https://example.test", title: "", text: "loaded", availableSelectorMatched: true, priorQueueLock: false }).classification).toBe("available"));
+ it("parses clock and text durations", () => { expect(parseDuration("01:02:03")).toBe(3723); expect(parseDuration("17 minutes")).toBe(1020); expect(parseDuration("9 sec")).toBe(9); });
+ it("normalizes bounded text rules", () => expect(normalizeTextRules([" Queue ", "queue", 1, ""])).toEqual(["queue"]));
+ it("rejects suspicious regular expressions", () => expect(compileSafePattern("(a+)+$")).toBeUndefined());
+ it("rejects multiline selectors", () => expect(isSafeSelector("button\n{}" )).toBe(false));
+});
diff --git a/tests/schedule.test.ts b/tests/schedule.test.ts
new file mode 100644
index 0000000..30820cc
--- /dev/null
+++ b/tests/schedule.test.ts
@@ -0,0 +1,12 @@
+import { describe, expect, it } from "vitest";
+import { createDefaultWatch } from "../src/core/defaults";
+import { activeOccurrence, currentOrNextOccurrence, isOlderWatch, zonedDateTimeToEpoch } from "../src/core/schedule";
+
+describe("schedules", () => {
+ it("converts UTC date time deterministically", () => expect(zonedDateTimeToEpoch("2026-08-10", "09:30", "UTC")).toBe(Date.UTC(2026, 7, 10, 9, 30)));
+ it("finds a current one-time window", () => { const now = Date.UTC(2026, 7, 10, 10); const watch = createDefaultWatch(now); watch.schedule.startAt = now - 1000; watch.schedule.stopAt = now + 1000; expect(activeOccurrence(watch, now)?.startAt).toBe(now - 1000); });
+ it("marks an elapsed one-time watch older", () => { const now = Date.UTC(2026, 7, 10, 10); const watch = createDefaultWatch(now); watch.schedule.startAt = now - 2000; watch.schedule.stopAt = now - 1000; expect(isOlderWatch(watch, now)).toBe(true); });
+ it("gives passive watches an open-ended occurrence", () => { const now = Date.UTC(2026, 7, 10); const watch = createDefaultWatch(now); watch.profile = "passive"; expect(currentOrNextOccurrence(watch, now)?.startAt).toBe(now); });
+ it("supports recurring weekdays", () => { const now = Date.UTC(2026, 7, 10, 8); const watch = createDefaultWatch(now); watch.schedule = { type: "weekdays", enabled: true, timezone: "UTC", weekdays: [], startTime: "09:00", expectedTime: "09:15", stopTime: "10:00" }; expect(currentOrNextOccurrence(watch, now)?.startAt).toBe(Date.UTC(2026, 7, 10, 9)); });
+ it("supports windows crossing midnight", () => { const now = Date.UTC(2026, 7, 10, 23); const watch = createDefaultWatch(now); watch.schedule = { type: "daily", enabled: true, timezone: "UTC", weekdays: [], startTime: "23:30", stopTime: "00:30" }; const result = currentOrNextOccurrence(watch, now)!; expect(result.stopAt - result.startAt).toBe(3_600_000); });
+});
diff --git a/tests/state-machine.test.ts b/tests/state-machine.test.ts
new file mode 100644
index 0000000..0b1e25b
--- /dev/null
+++ b/tests/state-machine.test.ts
@@ -0,0 +1,13 @@
+import { describe, expect, it } from "vitest";
+import { createDefaultWatch } from "../src/core/defaults";
+import { applyObservation, cadenceFor, createRun, refreshDecision } from "../src/core/state-machine";
+
+function run(now = 100_000) { const watch = createDefaultWatch(now); watch.schedule.startAt = now; watch.schedule.stopAt = now + 60_000; watch.schedule.expectedAt = now + 30_000; return createRun(watch, { occurrenceKey: "test", startAt: now, stopAt: now + 60_000, expectedAt: now + 30_000 }, now); }
+describe("run safety state machine", () => {
+ it("uses fast cadence around expected time", () => expect(cadenceFor(run(), 130_000)).toBe(15));
+ it("permits guarded refresh before attention", () => expect(refreshDecision(run(), 100_000).action).toBe("refresh"));
+ it("locks irreversibly on queue evidence", () => { const queued = applyObservation(run(), { observedAt: 101_000, url: "https://example.test", title: "", classification: "queue", confidence: 1, evidence: ["queue"] }); const later = applyObservation(queued, { observedAt: 102_000, url: "https://example.test", title: "", classification: "product", confidence: .5, evidence: ["page"] }); expect(later.queueLocked).toBe(true); expect(refreshDecision(later, 103_000).action).toBe("locked"); });
+ it("does not admit without prior queue evidence", () => expect(applyObservation(run(), { observedAt: 101_000, url: "https://example.test", title: "", classification: "admitted", confidence: 1, evidence: ["ready"] }).state).not.toBe("ADMITTED"));
+ it("admits after a queue lock", () => { const queued = applyObservation(run(), { observedAt: 101_000, url: "https://example.test", title: "", classification: "queue", confidence: 1, evidence: ["queue"] }); expect(applyObservation(queued, { observedAt: 102_000, url: "https://example.test", title: "", classification: "admitted", confidence: 1, evidence: ["ready"] }).state).toBe("ADMITTED"); });
+ it("observe-only runs never request refresh", () => { const value = run(); value.mode = "observe-only"; expect(refreshDecision(value, 100_000).action).toBe("observe"); });
+});
diff --git a/tests/validation.test.ts b/tests/validation.test.ts
new file mode 100644
index 0000000..63b8cc6
--- /dev/null
+++ b/tests/validation.test.ts
@@ -0,0 +1,11 @@
+import { describe, expect, it } from "vitest";
+import { createDefaultWatch } from "../src/core/defaults";
+import { canonicalizeUrl, normalizeSettings, normalizeWatch } from "../src/core/validation";
+
+describe("validation", () => {
+ it("removes common tracking parameters and fragments", () => expect(canonicalizeUrl("https://example.test/item?utm_source=x&id=4#buy")).toBe("https://example.test/item?id=4"));
+ it("rejects non-web protocols", () => expect(() => canonicalizeUrl("file:///secret")).toThrow());
+ it("bounds cadences and jitter", () => { const watch = createDefaultWatch(); watch.cadenceSeconds = 1; watch.activeCadenceSeconds = 99999; watch.jitterPercent = 90; const value = normalizeWatch(watch); expect(value.cadenceSeconds).toBe(5); expect(value.activeCadenceSeconds).toBe(3600); expect(value.jitterPercent).toBe(50); });
+ it("requires stop after start", () => { const watch = createDefaultWatch(); watch.schedule.stopAt = watch.schedule.startAt; expect(() => normalizeWatch(watch)).toThrow("Stop time"); });
+ it("bounds settings", () => { const value = normalizeSettings({ retentionDays: 999, queueEtaAlertMinutes: 0 }); expect(value.retentionDays).toBe(365); expect(value.queueEtaAlertMinutes).toBe(1); });
+});
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..2b39403
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,21 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "useDefineForClassFields": true,
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "types": ["chrome", "vitest/globals"]
+ },
+ "include": ["src", "tests"]
+}
diff --git a/vite.config.mjs b/vite.config.mjs
new file mode 100644
index 0000000..a71ad03
--- /dev/null
+++ b/vite.config.mjs
@@ -0,0 +1,32 @@
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+import { resolve } from "node:path";
+import { fileURLToPath } from "node:url";
+
+const rootDir = fileURLToPath(new URL(".", import.meta.url));
+
+export default defineConfig({
+ plugins: [react()],
+ build: {
+ outDir: "dist",
+ emptyOutDir: true,
+ sourcemap: false,
+ rollupOptions: {
+ input: {
+ dashboard: resolve(rootDir, "dashboard.html"),
+ sidepanel: resolve(rootDir, "sidepanel.html"),
+ offscreen: resolve(rootDir, "offscreen.html"),
+ harness: resolve(rootDir, "harness/index.html"),
+ "service-worker": resolve(rootDir, "src/background/service-worker.ts")
+ },
+ output: {
+ entryFileNames: (chunk) =>
+ chunk.name === "service-worker" || chunk.name === "offscreen"
+ ? "[name].js"
+ : "assets/[name]-[hash].js",
+ chunkFileNames: "assets/[name]-[hash].js",
+ assetFileNames: "assets/[name]-[hash][extname]"
+ }
+ }
+ }
+});